rechrome 1.38.0 → 1.38.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 (3) hide show
  1. package/package.json +1 -1
  2. package/rechrome.js +46 -3
  3. package/rechrome.ts +46 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.38.0",
3
+ "version": "1.38.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rechrome.js CHANGED
@@ -1493,6 +1493,43 @@ export function chooseShareListener(profile: string, listeners: Listener[]): str
1493
1493
  ].filter(Boolean).join("\n"), "none", scoped);
1494
1494
  }
1495
1495
 
1496
+ /**
1497
+ * Why a saved link to one of this machine's listeners stopped working, with the command that
1498
+ * replaces it; null when it still matches its listener or points at another machine.
1499
+ * A removed listener's port goes to the next one created (`freeListenerPort`), so a stale
1500
+ * link can reach a different listener: its path is not served there and its key is unknown.
1501
+ */
1502
+ export function staleLinkHint(url: string, listeners: Listener[], source?: string): string | null {
1503
+ let parsed: ReturnType<typeof parseUrl>;
1504
+ try { parsed = parseUrl(url); } catch { return null; }
1505
+ const loopback = (host: string) => isLoopback(host) || host === "localhost";
1506
+ const urlHost = parsed.host.replace(/^\[|\]$/g, "");
1507
+ const at = listeners.find(l => l.port === parsed.port && (l.host === urlHost || l.host === "0.0.0.0" || loopback(l.host) && loopback(urlHost)));
1508
+ const owner = listeners.find(l => l.key === parsed.key);
1509
+ if (!at && !owner && !loopback(urlHost)) return null; // another machine's daemon: only its owner knows
1510
+ if (owner && owner === at && normalizePrefix(owner.prefix) === parsed.prefix) return null;
1511
+ const profile = parsed.profileDirectory;
1512
+ let fix = profile ? `rech share ${JSON.stringify(profile)} --save` : "rech share --save";
1513
+ if (profile) {
1514
+ try {
1515
+ const name = owner && owner.profiles !== "*" ? owner.name : chooseShareListener(profile, listeners);
1516
+ fix = `rech share ${JSON.stringify(profile)} --listener ${name}${owner?.publicUrl || listeners.find(l => l.name === name)?.publicUrl ? "" : " --local"} --save`;
1517
+ } catch { /* no single listener shares it: the default creates a fresh link */ }
1518
+ }
1519
+ const lines = [owner
1520
+ ? `this link's listener "${owner.name}" moved: it now serves ${listenerAddress(owner)}${normalizePrefix(owner.prefix)}, not ${parsed.host}:${parsed.port}${parsed.prefix}.`
1521
+ : `this link's key matches no listener on this machine: its listener was removed or its key rotated.${at ? ` ${listenerAddress(at)} belongs to listener "${at.name}" (${normalizePrefix(at.prefix)}), which this link is not for.` : ""}`];
1522
+ if (source && source !== "environment") lines.push(`The stale RECHROME_URL is saved in ${source}.`);
1523
+ lines.push(`Replace it with a current link: ${fix}`);
1524
+ return lines.join("\n ");
1525
+ }
1526
+
1527
+ /** staleLinkHint against this machine's listeners.json; null when it can't be read. */
1528
+ async function localStaleLinkHint(url: string): Promise<string | null> {
1529
+ const config = await readListeners().catch(() => null);
1530
+ return config ? staleLinkHint(url, config.listeners, url === process.env[ENV_KEY] ? rechromeUrlSource : undefined) : null;
1531
+ }
1532
+
1496
1533
  /** A loopback port no listener uses and nothing else is bound to, from the management port + 2. */
1497
1534
  function freeListenerPort(listeners: Listener[]): number {
1498
1535
  const used = new Set(listeners.map(l => l.port));
@@ -1918,8 +1955,9 @@ async function callServe(
1918
1955
  process.exit(1);
1919
1956
  });
1920
1957
  if (res.status === 401) {
1921
- if (throwOnFailure) throw new Error("The daemon rejected its connection key. Run setup again to refresh it.");
1922
- console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
1958
+ const stale = await localStaleLinkHint(url);
1959
+ if (throwOnFailure) throw new Error(stale ? `The daemon rejected this link's key: ${stale}` : "The daemon rejected its connection key. Run setup again to refresh it.");
1960
+ console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]${stale ? `\n ${stale}` : ""}`);
1923
1961
  process.exit(1);
1924
1962
  }
1925
1963
  const text = await res.text();
@@ -1930,6 +1968,8 @@ async function callServe(
1930
1968
  let detail = `HTTP ${res.status} from ${serviceUrl(url, "run")} is not a rechrome daemon response: ${text.slice(0, 200).trim()}`;
1931
1969
  if (res.status === 502 || res.status === 503 || res.status === 504)
1932
1970
  detail += `\n The proxy is up but nothing answers behind it: this link's listener was likely removed or moved. Ask its owner for a fresh link (\`rech share\`), then \`rechrome connect <link>\`.`;
1971
+ const stale = await localStaleLinkHint(url);
1972
+ if (stale) detail += `\n ${stale}`;
1933
1973
  if (throwOnFailure) throw new Error(detail);
1934
1974
  console.error(`[rech] rech-client -> ${serviceUrl(url, "run")}\n -x: ${detail}`);
1935
1975
  process.exit(1);
@@ -4135,7 +4175,8 @@ async function status(): Promise<void> {
4135
4175
  // Show the URL this client connects to; through a proxy, the daemon's bind is on another port.
4136
4176
  const details = [pingBody?.listener && `listener ${pingBody.listener}`, pingBody?.bind && `bind ${pingBody.bind}`].filter(Boolean).join(", ");
4137
4177
  // Any HTTP answer is not the daemon: a proxy's 404/502 means the link's listener is gone or moved.
4138
- const answered = pingResponse?.ok || pingResponse?.status === 401 || (pingResponse?.status === 403 && !!pingBody?.error);
4178
+ // A 200 that isn't the /ping JSON is another listener's catch-all: its port was reused.
4179
+ const answered = (pingResponse?.ok && !!pingBody) || pingResponse?.status === 401 || (pingResponse?.status === 403 && !!pingBody?.error);
4139
4180
  console.log(`serve: ${!ping ? `not reachable at ${serviceUrl(url)}`
4140
4181
  : answered ? `running ${serviceUrl(url)}${details ? ` (${details})` : ""}`
4141
4182
  : `✗ ${serviceUrl(url)} answers HTTP ${pingResponse?.status ?? ping.status}, not a rechrome daemon — this link's listener was likely removed or moved; ask its host for a fresh link (\`rech share <profile>\`), then \`rech connect '<url>'\``}`);
@@ -4147,6 +4188,8 @@ async function status(): Promise<void> {
4147
4188
  if (pingResponse?.status === 403 && pingBody?.error) console.log(`auth: ✗ ${pingBody.error}`);
4148
4189
  if (pingResponse?.status === 401)
4149
4190
  console.log(`auth: ✗ key rejected — ask the host for a fresh URL (\`rech share <profile>\`), then \`rech connect '<url>'\``);
4191
+ const stale = ping && !pingBody ? await localStaleLinkHint(url) : null;
4192
+ if (stale) console.log(`stale: ✗ ${stale}`);
4150
4193
  // daemonManager().id — there is no PM_BIN constant. Referencing one threw a
4151
4194
  // ReferenceError that took down the whole of `rech status`, so the one command
4152
4195
  // that reports "the relay is wedged" died exactly when the relay was wedged,
package/rechrome.ts CHANGED
@@ -1493,6 +1493,43 @@ export function chooseShareListener(profile: string, listeners: Listener[]): str
1493
1493
  ].filter(Boolean).join("\n"), "none", scoped);
1494
1494
  }
1495
1495
 
1496
+ /**
1497
+ * Why a saved link to one of this machine's listeners stopped working, with the command that
1498
+ * replaces it; null when it still matches its listener or points at another machine.
1499
+ * A removed listener's port goes to the next one created (`freeListenerPort`), so a stale
1500
+ * link can reach a different listener: its path is not served there and its key is unknown.
1501
+ */
1502
+ export function staleLinkHint(url: string, listeners: Listener[], source?: string): string | null {
1503
+ let parsed: ReturnType<typeof parseUrl>;
1504
+ try { parsed = parseUrl(url); } catch { return null; }
1505
+ const loopback = (host: string) => isLoopback(host) || host === "localhost";
1506
+ const urlHost = parsed.host.replace(/^\[|\]$/g, "");
1507
+ const at = listeners.find(l => l.port === parsed.port && (l.host === urlHost || l.host === "0.0.0.0" || loopback(l.host) && loopback(urlHost)));
1508
+ const owner = listeners.find(l => l.key === parsed.key);
1509
+ if (!at && !owner && !loopback(urlHost)) return null; // another machine's daemon: only its owner knows
1510
+ if (owner && owner === at && normalizePrefix(owner.prefix) === parsed.prefix) return null;
1511
+ const profile = parsed.profileDirectory;
1512
+ let fix = profile ? `rech share ${JSON.stringify(profile)} --save` : "rech share --save";
1513
+ if (profile) {
1514
+ try {
1515
+ const name = owner && owner.profiles !== "*" ? owner.name : chooseShareListener(profile, listeners);
1516
+ fix = `rech share ${JSON.stringify(profile)} --listener ${name}${owner?.publicUrl || listeners.find(l => l.name === name)?.publicUrl ? "" : " --local"} --save`;
1517
+ } catch { /* no single listener shares it: the default creates a fresh link */ }
1518
+ }
1519
+ const lines = [owner
1520
+ ? `this link's listener "${owner.name}" moved: it now serves ${listenerAddress(owner)}${normalizePrefix(owner.prefix)}, not ${parsed.host}:${parsed.port}${parsed.prefix}.`
1521
+ : `this link's key matches no listener on this machine: its listener was removed or its key rotated.${at ? ` ${listenerAddress(at)} belongs to listener "${at.name}" (${normalizePrefix(at.prefix)}), which this link is not for.` : ""}`];
1522
+ if (source && source !== "environment") lines.push(`The stale RECHROME_URL is saved in ${source}.`);
1523
+ lines.push(`Replace it with a current link: ${fix}`);
1524
+ return lines.join("\n ");
1525
+ }
1526
+
1527
+ /** staleLinkHint against this machine's listeners.json; null when it can't be read. */
1528
+ async function localStaleLinkHint(url: string): Promise<string | null> {
1529
+ const config = await readListeners().catch(() => null);
1530
+ return config ? staleLinkHint(url, config.listeners, url === process.env[ENV_KEY] ? rechromeUrlSource : undefined) : null;
1531
+ }
1532
+
1496
1533
  /** A loopback port no listener uses and nothing else is bound to, from the management port + 2. */
1497
1534
  function freeListenerPort(listeners: Listener[]): number {
1498
1535
  const used = new Set(listeners.map(l => l.port));
@@ -1918,8 +1955,9 @@ async function callServe(
1918
1955
  process.exit(1);
1919
1956
  });
1920
1957
  if (res.status === 401) {
1921
- if (throwOnFailure) throw new Error("The daemon rejected its connection key. Run setup again to refresh it.");
1922
- console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
1958
+ const stale = await localStaleLinkHint(url);
1959
+ if (throwOnFailure) throw new Error(stale ? `The daemon rejected this link's key: ${stale}` : "The daemon rejected its connection key. Run setup again to refresh it.");
1960
+ console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]${stale ? `\n ${stale}` : ""}`);
1923
1961
  process.exit(1);
1924
1962
  }
1925
1963
  const text = await res.text();
@@ -1930,6 +1968,8 @@ async function callServe(
1930
1968
  let detail = `HTTP ${res.status} from ${serviceUrl(url, "run")} is not a rechrome daemon response: ${text.slice(0, 200).trim()}`;
1931
1969
  if (res.status === 502 || res.status === 503 || res.status === 504)
1932
1970
  detail += `\n The proxy is up but nothing answers behind it: this link's listener was likely removed or moved. Ask its owner for a fresh link (\`rech share\`), then \`rechrome connect <link>\`.`;
1971
+ const stale = await localStaleLinkHint(url);
1972
+ if (stale) detail += `\n ${stale}`;
1933
1973
  if (throwOnFailure) throw new Error(detail);
1934
1974
  console.error(`[rech] rech-client -> ${serviceUrl(url, "run")}\n -x: ${detail}`);
1935
1975
  process.exit(1);
@@ -4135,7 +4175,8 @@ async function status(): Promise<void> {
4135
4175
  // Show the URL this client connects to; through a proxy, the daemon's bind is on another port.
4136
4176
  const details = [pingBody?.listener && `listener ${pingBody.listener}`, pingBody?.bind && `bind ${pingBody.bind}`].filter(Boolean).join(", ");
4137
4177
  // Any HTTP answer is not the daemon: a proxy's 404/502 means the link's listener is gone or moved.
4138
- const answered = pingResponse?.ok || pingResponse?.status === 401 || (pingResponse?.status === 403 && !!pingBody?.error);
4178
+ // A 200 that isn't the /ping JSON is another listener's catch-all: its port was reused.
4179
+ const answered = (pingResponse?.ok && !!pingBody) || pingResponse?.status === 401 || (pingResponse?.status === 403 && !!pingBody?.error);
4139
4180
  console.log(`serve: ${!ping ? `not reachable at ${serviceUrl(url)}`
4140
4181
  : answered ? `running ${serviceUrl(url)}${details ? ` (${details})` : ""}`
4141
4182
  : `✗ ${serviceUrl(url)} answers HTTP ${pingResponse?.status ?? ping.status}, not a rechrome daemon — this link's listener was likely removed or moved; ask its host for a fresh link (\`rech share <profile>\`), then \`rech connect '<url>'\``}`);
@@ -4147,6 +4188,8 @@ async function status(): Promise<void> {
4147
4188
  if (pingResponse?.status === 403 && pingBody?.error) console.log(`auth: ✗ ${pingBody.error}`);
4148
4189
  if (pingResponse?.status === 401)
4149
4190
  console.log(`auth: ✗ key rejected — ask the host for a fresh URL (\`rech share <profile>\`), then \`rech connect '<url>'\``);
4191
+ const stale = ping && !pingBody ? await localStaleLinkHint(url) : null;
4192
+ if (stale) console.log(`stale: ✗ ${stale}`);
4150
4193
  // daemonManager().id — there is no PM_BIN constant. Referencing one threw a
4151
4194
  // ReferenceError that took down the whole of `rech status`, so the one command
4152
4195
  // that reports "the relay is wedged" died exactly when the relay was wedged,