@corenel/sidecar 0.1.4 → 0.1.5

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 (2) hide show
  1. package/dist/cli.js +133 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -713,10 +713,20 @@ function extractTunnelUrl(s) {
713
713
  const m = s.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
714
714
  return m ? m[0] : null;
715
715
  }
716
+ function isRegisteredLine(s) {
717
+ return /Registered tunnel connection/.test(s);
718
+ }
719
+ function isTunnelGoneLine(s) {
720
+ return /Unauthorized: Tunnel not found/i.test(s);
721
+ }
722
+ var GONE_LINE_LIMIT = 3;
716
723
  var START_TIMEOUT_MS2 = 25e3;
717
724
  function tunnelArgs(port, host = "localhost") {
718
725
  return ["tunnel", "--no-autoupdate", "--protocol", "http2", "--url", `http://${host}:${port}`];
719
726
  }
727
+ function namedTunnelArgs(name, port, host = "localhost") {
728
+ return ["tunnel", "--no-autoupdate", "--protocol", "http2", "run", "--url", `http://${host}:${port}`, name];
729
+ }
720
730
  var tail2 = (s, n = 6) => s.trim().split("\n").slice(-n).join("\n");
721
731
  function cloudflaredBins() {
722
732
  const env = process.env.CLOUDFLARED_BIN?.trim();
@@ -747,24 +757,34 @@ function stopChild2(child) {
747
757
  }
748
758
  });
749
759
  }
750
- function startWith2(bin, port, host, log) {
760
+ function startWith2(bin, mode, log, onDown) {
751
761
  return new Promise((resolveHandle, reject) => {
752
- const child = spawn3(bin, tunnelArgs(port, host), { stdio: ["ignore", "pipe", "pipe"] });
762
+ const child = spawn3(bin, mode.argv, { stdio: ["ignore", "pipe", "pipe"] });
753
763
  let out = "";
754
764
  let settled = false;
755
765
  let tailBuf = "";
766
+ let goneLines = 0;
767
+ let downFired = false;
756
768
  const finish = (fn) => {
757
769
  if (settled) return;
758
770
  settled = true;
759
771
  clearTimeout(timer);
760
772
  fn();
761
773
  };
774
+ const fireDown = (reason) => {
775
+ if (downFired) return;
776
+ downFired = true;
777
+ onDown?.(reason);
778
+ };
762
779
  const onData = (b) => {
763
780
  const text = b.toString();
764
781
  if (!settled) {
765
782
  out += text;
766
- const url = extractTunnelUrl(out);
767
- if (url) finish(() => resolveHandle({ url, stop: () => stopChild2(child) }));
783
+ const url = mode.ready(out);
784
+ if (url) finish(() => resolveHandle({ url, stop: () => {
785
+ downFired = true;
786
+ return stopChild2(child);
787
+ } }));
768
788
  return;
769
789
  }
770
790
  tailBuf += text;
@@ -772,7 +792,20 @@ function startWith2(bin, port, host, log) {
772
792
  while ((nl = tailBuf.indexOf("\n")) >= 0) {
773
793
  const line = tailBuf.slice(0, nl).replace(/\r$/, "").trim();
774
794
  tailBuf = tailBuf.slice(nl + 1);
775
- if (line) log?.(`cloudflared: ${line}`);
795
+ if (!line) continue;
796
+ log?.(`cloudflared: ${line}`);
797
+ if (isRegisteredLine(line)) {
798
+ goneLines = 0;
799
+ continue;
800
+ }
801
+ if (isTunnelGoneLine(line)) {
802
+ goneLines++;
803
+ if (goneLines >= GONE_LINE_LIMIT && !downFired) {
804
+ log?.(`cloudflared: tunnel deregistered by the edge (${goneLines}x "Tunnel not found") \u2014 it can never re-register; replacing it`);
805
+ void stopChild2(child);
806
+ fireDown("quick tunnel deregistered by the edge");
807
+ }
808
+ }
776
809
  }
777
810
  };
778
811
  child.stdout?.on("data", onData);
@@ -781,14 +814,15 @@ function startWith2(bin, port, host, log) {
781
814
  child.on("exit", (code) => {
782
815
  if (settled) {
783
816
  log?.(`cloudflared exited (code ${code}) \u2014 the tunnel is down`);
817
+ fireDown(`cloudflared exited (code ${code})`);
784
818
  return;
785
819
  }
786
- finish(() => reject(new Error(`${bin} exited (code ${code}) before a tunnel URL appeared.
820
+ finish(() => reject(new Error(`${bin} exited (code ${code}) before the tunnel came up.
787
821
  ${tail2(out)}`)));
788
822
  });
789
823
  const timer = setTimeout(() => finish(() => {
790
824
  void stopChild2(child);
791
- reject(new Error(`${bin} produced no tunnel URL within ${START_TIMEOUT_MS2 / 1e3}s.
825
+ reject(new Error(`${bin} did not come up within ${START_TIMEOUT_MS2 / 1e3}s.
792
826
  ${tail2(out)}`));
793
827
  }), START_TIMEOUT_MS2);
794
828
  });
@@ -796,8 +830,21 @@ ${tail2(out)}`));
796
830
  var cloudflaredProvider = {
797
831
  id: "cloudflared",
798
832
  aliases: ["cloudflare"],
799
- description: "Cloudflare quick tunnel (needs the `cloudflared` CLI installed \u2014 https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ \u2014 no account, no interstitial; URL is ephemeral)",
800
- start({ port, host, log }) {
833
+ description: "Cloudflare tunnel (needs the `cloudflared` CLI installed \u2014 https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/). Default: anonymous quick tunnel (no account, ephemeral URL). With --tunnel-name + --tunnel-hostname: your own NAMED tunnel (stable URL, auto-reconnects).",
834
+ start({ port, host, name, hostname, log, onDown }) {
835
+ const origin = host || "localhost";
836
+ let mode;
837
+ if (name) {
838
+ if (!hostname) {
839
+ return Promise.reject(new Error(
840
+ `a named cloudflared tunnel needs --tunnel-hostname <public-host> \u2014 the DNS route you created with \`cloudflared tunnel route dns ${name} <public-host>\` (one-time setup: \`cloudflared tunnel login\` then \`cloudflared tunnel create ${name}\`).`
841
+ ));
842
+ }
843
+ const url = `https://${hostname.replace(/^https?:\/\//, "")}`;
844
+ mode = { argv: namedTunnelArgs(name, port, origin), ready: (s) => isRegisteredLine(s) ? url : null };
845
+ } else {
846
+ mode = { argv: tunnelArgs(port, origin), ready: extractTunnelUrl };
847
+ }
801
848
  const bins = cloudflaredBins();
802
849
  const tryAt = (i) => {
803
850
  if (i >= bins.length) {
@@ -805,7 +852,7 @@ var cloudflaredProvider = {
805
852
  `cloudflared not found (tried: ${bins.join(", ")}). Install the cloudflared CLI (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/), or set CLOUDFLARED_BIN to the binary name/path.`
806
853
  ));
807
854
  }
808
- return startWith2(bins[i], port, host || "localhost", log).catch((e) => {
855
+ return startWith2(bins[i], mode, log, onDown).catch((e) => {
809
856
  if (e?.code === "ENOENT") return tryAt(i + 1);
810
857
  throw e;
811
858
  });
@@ -818,6 +865,66 @@ var cloudflaredProvider = {
818
865
  registerTunnel(cloudflaredProvider);
819
866
  registerTunnel(zrokProvider);
820
867
 
868
+ // src/tunnel/supervisor.ts
869
+ var DEFAULT_BACKOFF = [2e3, 5e3, 1e4, 3e4];
870
+ async function superviseTunnel(opts) {
871
+ const backoff = opts.backoffMs ?? DEFAULT_BACKOFF;
872
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
873
+ const log = opts.start.log;
874
+ let stopped = false;
875
+ let restarting = false;
876
+ let current = null;
877
+ let incarnation = 0;
878
+ const startOnce = () => {
879
+ const mine = ++incarnation;
880
+ return opts.provider.start({
881
+ ...opts.start,
882
+ onDown: (reason) => {
883
+ if (mine === incarnation) void restart(reason);
884
+ }
885
+ });
886
+ };
887
+ async function restart(reason) {
888
+ if (stopped || restarting) return;
889
+ restarting = true;
890
+ current = null;
891
+ log?.(`tunnel down (${reason}) \u2014 restarting`);
892
+ try {
893
+ for (let attempt = 0; !stopped; attempt++) {
894
+ await sleep(backoff[Math.min(attempt, backoff.length - 1)]);
895
+ if (stopped) return;
896
+ try {
897
+ const h = await startOnce();
898
+ if (stopped) {
899
+ void h.stop().catch(() => {
900
+ });
901
+ return;
902
+ }
903
+ current = h;
904
+ opts.onUp(h.url, true);
905
+ return;
906
+ } catch (e) {
907
+ log?.(`tunnel restart failed (${e instanceof Error ? e.message : String(e)}) \u2014 retrying`);
908
+ }
909
+ }
910
+ } finally {
911
+ restarting = false;
912
+ }
913
+ }
914
+ current = await startOnce();
915
+ opts.onUp(current.url, false);
916
+ return {
917
+ stop: async () => {
918
+ stopped = true;
919
+ incarnation++;
920
+ const c = current;
921
+ current = null;
922
+ if (c) await c.stop().catch(() => {
923
+ });
924
+ }
925
+ };
926
+ }
927
+
821
928
  // src/cli.ts
822
929
  function parseArgs(argv) {
823
930
  const out = { port: 4858, host: "127.0.0.1", roots: [], allowOrigins: [], allowShell: false, allowLan: false, selfSigned: false, open: false, appUrl: "https://prompd.app" };
@@ -834,6 +941,7 @@ function parseArgs(argv) {
834
941
  else if (a === "--self-signed") out.selfSigned = true;
835
942
  else if (a === "--tunnel") out.tunnel = argv[++i];
836
943
  else if (a === "--tunnel-name") out.tunnelName = argv[++i];
944
+ else if (a === "--tunnel-hostname") out.tunnelHostname = argv[++i];
837
945
  else if (a === "--open") out.open = true;
838
946
  else if (a === "--app-url") out.appUrl = argv[++i];
839
947
  else if (a === "--sidecar" || a === "start" || a === "--") {
@@ -854,6 +962,7 @@ var KNOWN_FLAGS = [
854
962
  "--self-signed",
855
963
  "--tunnel",
856
964
  "--tunnel-name",
965
+ "--tunnel-hostname",
857
966
  "--open",
858
967
  "--app-url"
859
968
  ];
@@ -920,13 +1029,22 @@ async function main() {
920
1029
  log: slog,
921
1030
  allowLan: args.allowLan
922
1031
  });
923
- let tunnel = null;
1032
+ let tunnelSup = null;
924
1033
  let tunnelWss = null;
925
1034
  if (tunnelProvider) {
926
- process.stdout.write(`starting ${tunnelProvider.id} tunnel${args.tunnelName ? ` (reserved name "${args.tunnelName}")` : ""}\u2026
1035
+ process.stdout.write(`starting ${tunnelProvider.id} tunnel${args.tunnelName ? ` (name "${args.tunnelName}")` : ""}\u2026
927
1036
  `);
928
- tunnel = await tunnelProvider.start({ port: handle.port, host: handle.host, name: args.tunnelName, log: slog });
929
- tunnelWss = tunnel.url.replace(/^http/, "ws");
1037
+ tunnelSup = await superviseTunnel({
1038
+ provider: tunnelProvider,
1039
+ start: { port: handle.port, host: handle.host, name: args.tunnelName, hostname: args.tunnelHostname, log: slog },
1040
+ onUp: (url, restarted) => {
1041
+ const wss = url.replace(/^http/, "ws");
1042
+ const changed = tunnelWss !== null && wss !== tunnelWss;
1043
+ tunnelWss = wss;
1044
+ if (!restarted) return;
1045
+ slog(changed ? `tunnel restored at a NEW URL: ${wss} \u2014 re-pair the app with this URL (the token is unchanged)` : "tunnel restored \u2014 same URL, existing pairings reconnect automatically");
1046
+ }
1047
+ });
930
1048
  }
931
1049
  const scheme = handle.secure ? "wss" : "ws";
932
1050
  const exposed = handle.host !== "127.0.0.1" && handle.host !== "localhost";
@@ -955,7 +1073,7 @@ async function main() {
955
1073
  }
956
1074
  const shutdown = () => {
957
1075
  const closeServer = () => handle.close().then(() => process.exit(0));
958
- if (tunnel) void tunnel.stop().catch(() => {
1076
+ if (tunnelSup) void tunnelSup.stop().catch(() => {
959
1077
  }).then(closeServer);
960
1078
  else void closeServer();
961
1079
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/sidecar",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "license": "Elastic-2.0",
6
6
  "description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",