rechrome 1.35.0 → 1.35.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 +53 -13
  3. package/rechrome.ts +53 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.35.0",
3
+ "version": "1.35.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rechrome.js CHANGED
@@ -1509,11 +1509,7 @@ async function shareSet(snapshot: string[], opts: { listener?: string; local?: b
1509
1509
  // A one-profile link names its profile, so the other machine needs no --profile.
1510
1510
  const only = snapshot.length === 1 ? `?profile=${encodeURIComponent(snapshot[0])}` : "";
1511
1511
  const local = `http://${listener.key}@${listenerAddress(listener)}${normalizePrefix(listener.prefix)}${only}`;
1512
- let ready = false;
1513
- for (let i = 0; i < 20 && !ready; i++) {
1514
- ready = await fetch(serviceUrl(local, "ping"), { headers: { Authorization: `Bearer ${listener.key}` }, signal: AbortSignal.timeout(1000) }).then(r => r.ok).catch(() => false);
1515
- if (!ready) await Bun.sleep(250);
1516
- }
1512
+ const ready = await waitForListener(listener);
1517
1513
  if (!ready) console.error(`[rech] warning: listener "${name}" is not answering yet; check the daemon with rech status.`);
1518
1514
  if (opts.tailscaleServe && !listener.publicUrl) {
1519
1515
  const publicUrl = await exposeWithTailscaleServe(listener);
@@ -1570,6 +1566,32 @@ async function exposeWithTailscaleServe(listener: Listener): Promise<string | nu
1570
1566
  return (await detectTailscaleServe(listener.port, prefix)).routeUrl;
1571
1567
  }
1572
1568
 
1569
+ /** The daemon reloads listeners.json about every second: wait (up to ~5s) until this listener answers. */
1570
+ async function waitForListener(listener: Listener): Promise<boolean> {
1571
+ const url = `http://${listener.key}@${listenerAddress(listener)}${normalizePrefix(listener.prefix)}`;
1572
+ for (let i = 0; i < 20; i++) {
1573
+ if (await fetch(serviceUrl(url, "ping"), { headers: { Authorization: `Bearer ${listener.key}` }, signal: AbortSignal.timeout(1000) }).then(r => r.ok).catch(() => false)) return true;
1574
+ await Bun.sleep(250);
1575
+ }
1576
+ return false;
1577
+ }
1578
+
1579
+ /**
1580
+ * Ask how other machines will reach a link: "tailscale-serve", a detected network address
1581
+ * (plain HTTP), "127.0.0.1" for the user's own proxy, or `extra` choices; null when cancelled.
1582
+ */
1583
+ async function askReachability(question: string, extra: { label: string; value: string }[] = []): Promise<string | null> {
1584
+ const serve = !!tailscaleBinary();
1585
+ // With Tailscale Serve on offer, a plain-HTTP Tailscale IP would only be a worse duplicate.
1586
+ const networks = (await detectListenChoices()).filter(c => c.kind !== "local" && !(serve && c.kind === "tailscale"));
1587
+ return promptChoice(question, [
1588
+ ...(serve ? [{ label: "Tailscale: HTTPS, only your tailnet", value: "tailscale-serve" }] : []),
1589
+ ...networks.map(c => ({ label: `${c.label.split(" — ")[0]} network: http://${c.address}, plain HTTP`, value: c.address })),
1590
+ { label: "My own reverse proxy", value: "127.0.0.1" },
1591
+ ...extra,
1592
+ ], 0);
1593
+ }
1594
+
1573
1595
  /**
1574
1596
  * Give one profile its own link. In a terminal, ask how the other machines will reach it
1575
1597
  * (Tailscale Serve, a detected network, or the user's own proxy); without one, create a
@@ -1580,14 +1602,7 @@ async function shareNewLink(profile: string, opts: { local?: boolean; save?: boo
1580
1602
  const name = uniqueShareName(profileSlug(profile), (await requireListeners()).listeners, served);
1581
1603
  let host = "127.0.0.1", tailscaleServe = false;
1582
1604
  if (isInteractive()) {
1583
- const serve = !!tailscaleBinary();
1584
- // With Tailscale Serve on offer, a plain-HTTP Tailscale IP would only be a worse duplicate.
1585
- const networks = (await detectListenChoices()).filter(c => c.kind !== "local" && !(serve && c.kind === "tailscale"));
1586
- const how = await promptChoice(`How will your other machines reach it?`, [
1587
- ...(serve ? [{ label: "Tailscale: HTTPS, only your tailnet", value: "tailscale-serve" }] : []),
1588
- ...networks.map(c => ({ label: `${c.label.split(" — ")[0]} network: http://${c.address}, plain HTTP`, value: c.address })),
1589
- { label: "My own reverse proxy", value: "127.0.0.1" },
1590
- ], 0);
1605
+ const how = await askReachability(`How will your other machines reach it?`);
1591
1606
  if (!how) throw new Error("Cancelled; nothing shared.");
1592
1607
  tailscaleServe = how === "tailscale-serve";
1593
1608
  host = tailscaleServe ? "127.0.0.1" : how;
@@ -1726,6 +1741,31 @@ async function printProfileUri(selector?: string, listener?: string, opts: { loc
1726
1741
  const serve = await detectTailscaleServe(pending.port, normalizePrefix(pending.prefix));
1727
1742
  if (serve.routeUrl && !serve.funnel) { pending.publicUrl = serve.routeUrl; await writeListeners(config); }
1728
1743
  }
1744
+ // Still only reachable from this machine (no proxy recorded or detected): a link like that is
1745
+ // useless to hand out, so ask how other machines reach it, or say how to expose it.
1746
+ if (config && pending && pending.profiles !== "*" && !pending.publicUrl && isLoopback(pending.host) && !opts.local) {
1747
+ const steps = () => { for (const line of listenerNextSteps(pending, profile)) console.error(line); };
1748
+ if (!interactive) {
1749
+ console.error(`[rech] this link only works on this machine: listener "${pending.name}" is on ${listenerAddress(pending)} and no proxy is recorded.`);
1750
+ steps();
1751
+ } else {
1752
+ const KEEP = "\0keep";
1753
+ const how = await askReachability(`Link "${pending.name}" only works on this machine (${listenerAddress(pending)}). How will your other machines reach it?`,
1754
+ [{ label: "Nothing: keep it on this machine", value: KEEP }]);
1755
+ if (!how) throw cancelled();
1756
+ if (how === "tailscale-serve") {
1757
+ const routeUrl = await exposeWithTailscaleServe(pending);
1758
+ if (routeUrl) { pending.publicUrl = routeUrl; await writeListeners(config); }
1759
+ } else if (how === "127.0.0.1") steps();
1760
+ else if (how !== KEEP) {
1761
+ // Same key and prefix, now bound on that network; links already handed out change host.
1762
+ pending.host = how;
1763
+ await writeListeners(config);
1764
+ if (!(await waitForListener(pending))) console.error(`[rech] warning: listener "${pending.name}" is not answering on ${listenerAddress(pending)} yet; check the daemon with rech status.`);
1765
+ uri = profileConnectionUri(profile, undefined, config.listeners, pending.name);
1766
+ }
1767
+ }
1768
+ }
1729
1769
  const publicUrl = pending?.publicUrl;
1730
1770
  if (publicUrl && !opts.local) uri = rebaseConnectionUrl(publicUrl, uri);
1731
1771
  console.log(uri);
package/rechrome.ts CHANGED
@@ -1509,11 +1509,7 @@ async function shareSet(snapshot: string[], opts: { listener?: string; local?: b
1509
1509
  // A one-profile link names its profile, so the other machine needs no --profile.
1510
1510
  const only = snapshot.length === 1 ? `?profile=${encodeURIComponent(snapshot[0])}` : "";
1511
1511
  const local = `http://${listener.key}@${listenerAddress(listener)}${normalizePrefix(listener.prefix)}${only}`;
1512
- let ready = false;
1513
- for (let i = 0; i < 20 && !ready; i++) {
1514
- ready = await fetch(serviceUrl(local, "ping"), { headers: { Authorization: `Bearer ${listener.key}` }, signal: AbortSignal.timeout(1000) }).then(r => r.ok).catch(() => false);
1515
- if (!ready) await Bun.sleep(250);
1516
- }
1512
+ const ready = await waitForListener(listener);
1517
1513
  if (!ready) console.error(`[rech] warning: listener "${name}" is not answering yet; check the daemon with rech status.`);
1518
1514
  if (opts.tailscaleServe && !listener.publicUrl) {
1519
1515
  const publicUrl = await exposeWithTailscaleServe(listener);
@@ -1570,6 +1566,32 @@ async function exposeWithTailscaleServe(listener: Listener): Promise<string | nu
1570
1566
  return (await detectTailscaleServe(listener.port, prefix)).routeUrl;
1571
1567
  }
1572
1568
 
1569
+ /** The daemon reloads listeners.json about every second: wait (up to ~5s) until this listener answers. */
1570
+ async function waitForListener(listener: Listener): Promise<boolean> {
1571
+ const url = `http://${listener.key}@${listenerAddress(listener)}${normalizePrefix(listener.prefix)}`;
1572
+ for (let i = 0; i < 20; i++) {
1573
+ if (await fetch(serviceUrl(url, "ping"), { headers: { Authorization: `Bearer ${listener.key}` }, signal: AbortSignal.timeout(1000) }).then(r => r.ok).catch(() => false)) return true;
1574
+ await Bun.sleep(250);
1575
+ }
1576
+ return false;
1577
+ }
1578
+
1579
+ /**
1580
+ * Ask how other machines will reach a link: "tailscale-serve", a detected network address
1581
+ * (plain HTTP), "127.0.0.1" for the user's own proxy, or `extra` choices; null when cancelled.
1582
+ */
1583
+ async function askReachability(question: string, extra: { label: string; value: string }[] = []): Promise<string | null> {
1584
+ const serve = !!tailscaleBinary();
1585
+ // With Tailscale Serve on offer, a plain-HTTP Tailscale IP would only be a worse duplicate.
1586
+ const networks = (await detectListenChoices()).filter(c => c.kind !== "local" && !(serve && c.kind === "tailscale"));
1587
+ return promptChoice(question, [
1588
+ ...(serve ? [{ label: "Tailscale: HTTPS, only your tailnet", value: "tailscale-serve" }] : []),
1589
+ ...networks.map(c => ({ label: `${c.label.split(" — ")[0]} network: http://${c.address}, plain HTTP`, value: c.address })),
1590
+ { label: "My own reverse proxy", value: "127.0.0.1" },
1591
+ ...extra,
1592
+ ], 0);
1593
+ }
1594
+
1573
1595
  /**
1574
1596
  * Give one profile its own link. In a terminal, ask how the other machines will reach it
1575
1597
  * (Tailscale Serve, a detected network, or the user's own proxy); without one, create a
@@ -1580,14 +1602,7 @@ async function shareNewLink(profile: string, opts: { local?: boolean; save?: boo
1580
1602
  const name = uniqueShareName(profileSlug(profile), (await requireListeners()).listeners, served);
1581
1603
  let host = "127.0.0.1", tailscaleServe = false;
1582
1604
  if (isInteractive()) {
1583
- const serve = !!tailscaleBinary();
1584
- // With Tailscale Serve on offer, a plain-HTTP Tailscale IP would only be a worse duplicate.
1585
- const networks = (await detectListenChoices()).filter(c => c.kind !== "local" && !(serve && c.kind === "tailscale"));
1586
- const how = await promptChoice(`How will your other machines reach it?`, [
1587
- ...(serve ? [{ label: "Tailscale: HTTPS, only your tailnet", value: "tailscale-serve" }] : []),
1588
- ...networks.map(c => ({ label: `${c.label.split(" — ")[0]} network: http://${c.address}, plain HTTP`, value: c.address })),
1589
- { label: "My own reverse proxy", value: "127.0.0.1" },
1590
- ], 0);
1605
+ const how = await askReachability(`How will your other machines reach it?`);
1591
1606
  if (!how) throw new Error("Cancelled; nothing shared.");
1592
1607
  tailscaleServe = how === "tailscale-serve";
1593
1608
  host = tailscaleServe ? "127.0.0.1" : how;
@@ -1726,6 +1741,31 @@ async function printProfileUri(selector?: string, listener?: string, opts: { loc
1726
1741
  const serve = await detectTailscaleServe(pending.port, normalizePrefix(pending.prefix));
1727
1742
  if (serve.routeUrl && !serve.funnel) { pending.publicUrl = serve.routeUrl; await writeListeners(config); }
1728
1743
  }
1744
+ // Still only reachable from this machine (no proxy recorded or detected): a link like that is
1745
+ // useless to hand out, so ask how other machines reach it, or say how to expose it.
1746
+ if (config && pending && pending.profiles !== "*" && !pending.publicUrl && isLoopback(pending.host) && !opts.local) {
1747
+ const steps = () => { for (const line of listenerNextSteps(pending, profile)) console.error(line); };
1748
+ if (!interactive) {
1749
+ console.error(`[rech] this link only works on this machine: listener "${pending.name}" is on ${listenerAddress(pending)} and no proxy is recorded.`);
1750
+ steps();
1751
+ } else {
1752
+ const KEEP = "\0keep";
1753
+ const how = await askReachability(`Link "${pending.name}" only works on this machine (${listenerAddress(pending)}). How will your other machines reach it?`,
1754
+ [{ label: "Nothing: keep it on this machine", value: KEEP }]);
1755
+ if (!how) throw cancelled();
1756
+ if (how === "tailscale-serve") {
1757
+ const routeUrl = await exposeWithTailscaleServe(pending);
1758
+ if (routeUrl) { pending.publicUrl = routeUrl; await writeListeners(config); }
1759
+ } else if (how === "127.0.0.1") steps();
1760
+ else if (how !== KEEP) {
1761
+ // Same key and prefix, now bound on that network; links already handed out change host.
1762
+ pending.host = how;
1763
+ await writeListeners(config);
1764
+ if (!(await waitForListener(pending))) console.error(`[rech] warning: listener "${pending.name}" is not answering on ${listenerAddress(pending)} yet; check the daemon with rech status.`);
1765
+ uri = profileConnectionUri(profile, undefined, config.listeners, pending.name);
1766
+ }
1767
+ }
1768
+ }
1729
1769
  const publicUrl = pending?.publicUrl;
1730
1770
  if (publicUrl && !opts.local) uri = rebaseConnectionUrl(publicUrl, uri);
1731
1771
  console.log(uri);