@iamken/cloudtunnel 0.11.4 → 0.11.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.
package/dist/index.js CHANGED
@@ -247,11 +247,13 @@ function assertSupported() {
247
247
  }
248
248
  function install(d) {
249
249
  assertSupported();
250
- const tmp = join2(tmpdir(), label(d.fqdn));
250
+ const name = label(d.fqdn);
251
+ const tmp = join2(tmpdir(), name);
251
252
  writeFileSync(tmp, buildUnit(d), { mode: 420 });
252
253
  privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
253
254
  privileged(["systemctl", "daemon-reload"]);
254
- privileged(["systemctl", "enable", "--now", label(d.fqdn)]);
255
+ privileged(["systemctl", "enable", name]);
256
+ privileged(["systemctl", "restart", name]);
255
257
  }
256
258
  function uninstall(fqdn) {
257
259
  try {
@@ -389,7 +391,7 @@ function buildTaskXml(d) {
389
391
  " <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
390
392
  " <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>",
391
393
  " <Enabled>true</Enabled>",
392
- " </Settings>",
394
+ "</Settings>",
393
395
  ' <Actions Context="Author">',
394
396
  ` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
395
397
  " </Actions>",
@@ -409,9 +411,11 @@ function assertSupported3() {
409
411
  }
410
412
  function install3(d) {
411
413
  const file = join4(tmpdir2(), `${d.slug}.task.xml`);
414
+ const name = label3(d.fqdn);
412
415
  writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
413
- execFileSync3("schtasks", ["/Create", "/TN", label3(d.fqdn), "/XML", file, "/F"], { stdio: "inherit" });
414
- schtasks(["/Run", "/TN", label3(d.fqdn)]);
416
+ schtasks(["/End", "/TN", name]);
417
+ execFileSync3("schtasks", ["/Create", "/TN", name, "/XML", file, "/F"], { stdio: "inherit" });
418
+ schtasks(["/Run", "/TN", name]);
415
419
  }
416
420
  function uninstall3(fqdn) {
417
421
  schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
@@ -941,6 +945,103 @@ function extractTgz(bytes) {
941
945
  });
942
946
  }
943
947
 
948
+ // src/cloudflare/tunnels.ts
949
+ var MANAGED_TUNNEL_PREFIX = "ct-";
950
+ function isManagedTunnel(tunnel) {
951
+ return tunnel.name.startsWith(MANAGED_TUNNEL_PREFIX);
952
+ }
953
+ async function createTunnel(cf, name) {
954
+ const env = await cfRequest(cf.token, "POST", `/accounts/${cf.accountId}/cfd_tunnel`, {
955
+ name,
956
+ config_src: "cloudflare"
957
+ });
958
+ return env.result;
959
+ }
960
+ function listTunnels(cf) {
961
+ return cfPaginate(cf.token, `/accounts/${cf.accountId}/cfd_tunnel?is_deleted=false`);
962
+ }
963
+ async function getTunnel(cf, id) {
964
+ return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
965
+ }
966
+ async function deleteTunnel(cf, id) {
967
+ await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
968
+ }
969
+ async function cleanupConnections(cf, id) {
970
+ await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
971
+ }
972
+ async function deleteTunnelWithConnections(cf, id) {
973
+ try {
974
+ await deleteTunnel(cf, id);
975
+ } catch (err) {
976
+ if (err instanceof CliError && /active connections/i.test(err.message)) {
977
+ await cleanupConnections(cf, id);
978
+ await deleteTunnel(cf, id);
979
+ } else {
980
+ throw err;
981
+ }
982
+ }
983
+ }
984
+ async function getTunnelToken(cf, id) {
985
+ return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}/token`)).result;
986
+ }
987
+ async function putIngress(cf, id, ingress) {
988
+ await cfRequest(cf.token, "PUT", `/accounts/${cf.accountId}/cfd_tunnel/${id}/configurations`, {
989
+ config: { ingress }
990
+ });
991
+ }
992
+ async function getConnections(cf, id) {
993
+ const env = await cfRequest(
994
+ cf.token,
995
+ "GET",
996
+ `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
997
+ );
998
+ return env.result ?? [];
999
+ }
1000
+
1001
+ // src/connector/health.ts
1002
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1003
+ async function waitHealthy(cf, tunnelId, opts = {}) {
1004
+ const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
1005
+ while (Date.now() < deadline) {
1006
+ if (opts.signal?.aborted) return "dead";
1007
+ try {
1008
+ const connections = await getConnections(cf, tunnelId);
1009
+ if (connections.length > 0) return "healthy";
1010
+ } catch {
1011
+ }
1012
+ await sleep(2e3);
1013
+ }
1014
+ return opts.signal?.aborted ? "dead" : "provisioning";
1015
+ }
1016
+ function extractMetricsPort(log) {
1017
+ const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
1018
+ if (!matches.length) return void 0;
1019
+ const last = matches[matches.length - 1];
1020
+ const port = Number(last?.[1]);
1021
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
1022
+ }
1023
+ async function checkLocalReady(port, opts = {}) {
1024
+ const controller = new AbortController();
1025
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
1026
+ try {
1027
+ const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
1028
+ if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
1029
+ const data = await res.json().catch(() => ({}));
1030
+ const readyConnections = Number(data.readyConnections) || 0;
1031
+ return { healthy: readyConnections > 0, readyConnections, status: res.status };
1032
+ } catch {
1033
+ return { healthy: false, readyConnections: 0 };
1034
+ } finally {
1035
+ clearTimeout(timer);
1036
+ }
1037
+ }
1038
+ async function probeEntryState(entry, gone) {
1039
+ if (gone || entry.state !== "running" || !entry.pid) return "down";
1040
+ if (!entry.metricsPort) return "up";
1041
+ const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
1042
+ return ready.healthy ? "up" : "reconnecting";
1043
+ }
1044
+
944
1045
  // src/core/up-runner.ts
945
1046
  import { join as join7 } from "path";
946
1047
  import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
@@ -1084,7 +1185,7 @@ async function reconcile() {
1084
1185
  }
1085
1186
 
1086
1187
  // src/connector/process.ts
1087
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1188
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1088
1189
  function startConnector(opts) {
1089
1190
  const args = ["tunnel", "--metrics", "127.0.0.1:0", "run"];
1090
1191
  if (opts.protocol) args.push("--protocol", opts.protocol);
@@ -1116,7 +1217,7 @@ async function stopConnector(entry) {
1116
1217
  } catch {
1117
1218
  return false;
1118
1219
  }
1119
- await sleep(3e3);
1220
+ await sleep2(3e3);
1120
1221
  if (await isOurConnector(entry)) {
1121
1222
  try {
1122
1223
  process.kill(pid, "SIGKILL");
@@ -1126,103 +1227,6 @@ async function stopConnector(entry) {
1126
1227
  return true;
1127
1228
  }
1128
1229
 
1129
- // src/cloudflare/tunnels.ts
1130
- var MANAGED_TUNNEL_PREFIX = "ct-";
1131
- function isManagedTunnel(tunnel) {
1132
- return tunnel.name.startsWith(MANAGED_TUNNEL_PREFIX);
1133
- }
1134
- async function createTunnel(cf, name) {
1135
- const env = await cfRequest(cf.token, "POST", `/accounts/${cf.accountId}/cfd_tunnel`, {
1136
- name,
1137
- config_src: "cloudflare"
1138
- });
1139
- return env.result;
1140
- }
1141
- function listTunnels(cf) {
1142
- return cfPaginate(cf.token, `/accounts/${cf.accountId}/cfd_tunnel?is_deleted=false`);
1143
- }
1144
- async function getTunnel(cf, id) {
1145
- return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
1146
- }
1147
- async function deleteTunnel(cf, id) {
1148
- await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
1149
- }
1150
- async function cleanupConnections(cf, id) {
1151
- await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
1152
- }
1153
- async function deleteTunnelWithConnections(cf, id) {
1154
- try {
1155
- await deleteTunnel(cf, id);
1156
- } catch (err) {
1157
- if (err instanceof CliError && /active connections/i.test(err.message)) {
1158
- await cleanupConnections(cf, id);
1159
- await deleteTunnel(cf, id);
1160
- } else {
1161
- throw err;
1162
- }
1163
- }
1164
- }
1165
- async function getTunnelToken(cf, id) {
1166
- return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}/token`)).result;
1167
- }
1168
- async function putIngress(cf, id, ingress) {
1169
- await cfRequest(cf.token, "PUT", `/accounts/${cf.accountId}/cfd_tunnel/${id}/configurations`, {
1170
- config: { ingress }
1171
- });
1172
- }
1173
- async function getConnections(cf, id) {
1174
- const env = await cfRequest(
1175
- cf.token,
1176
- "GET",
1177
- `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
1178
- );
1179
- return env.result ?? [];
1180
- }
1181
-
1182
- // src/connector/health.ts
1183
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1184
- async function waitHealthy(cf, tunnelId, opts = {}) {
1185
- const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
1186
- while (Date.now() < deadline) {
1187
- if (opts.signal?.aborted) return "dead";
1188
- try {
1189
- const connections = await getConnections(cf, tunnelId);
1190
- if (connections.length > 0) return "healthy";
1191
- } catch {
1192
- }
1193
- await sleep2(2e3);
1194
- }
1195
- return opts.signal?.aborted ? "dead" : "provisioning";
1196
- }
1197
- function extractMetricsPort(log) {
1198
- const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
1199
- if (!matches.length) return void 0;
1200
- const last = matches[matches.length - 1];
1201
- const port = Number(last?.[1]);
1202
- return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
1203
- }
1204
- async function checkLocalReady(port, opts = {}) {
1205
- const controller = new AbortController();
1206
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
1207
- try {
1208
- const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
1209
- if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
1210
- const data = await res.json().catch(() => ({}));
1211
- const readyConnections = Number(data.readyConnections) || 0;
1212
- return { healthy: readyConnections > 0, readyConnections, status: res.status };
1213
- } catch {
1214
- return { healthy: false, readyConnections: 0 };
1215
- } finally {
1216
- clearTimeout(timer);
1217
- }
1218
- }
1219
- async function probeEntryState(entry, gone) {
1220
- if (gone || entry.state !== "running" || !entry.pid) return "down";
1221
- if (!entry.metricsPort) return "up";
1222
- const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
1223
- return ready.healthy ? "up" : "reconnecting";
1224
- }
1225
-
1226
1230
  // src/core/orchestrator-create.ts
1227
1231
  import { randomInt as randomInt2 } from "crypto";
1228
1232
 
@@ -1309,9 +1313,14 @@ async function createTunnelSubdomain(cf, opts) {
1309
1313
  throw new CliError("Cancelled.", { exitCode: 130 });
1310
1314
  }
1311
1315
  }
1316
+ const prev = getEntry(host.hostname);
1317
+ if (prev) await stopConnector(prev);
1312
1318
  for (const rec of existingList) {
1313
1319
  await releaseHostname(cf, zone.id, rec);
1314
1320
  }
1321
+ } else {
1322
+ const prev = getEntry(host.hostname);
1323
+ if (prev) await stopConnector(prev);
1315
1324
  }
1316
1325
  await upsertEntry(host.hostname, {
1317
1326
  subdomain: host.subdomain,
@@ -1320,7 +1329,9 @@ async function createTunnelSubdomain(cf, opts) {
1320
1329
  port: opts.port,
1321
1330
  proto: opts.proto,
1322
1331
  host: opts.host,
1323
- state: "provisioning"
1332
+ state: "provisioning",
1333
+ pid: void 0,
1334
+ metricsPort: void 0
1324
1335
  });
1325
1336
  let tunnelId;
1326
1337
  let dnsRecordId;
@@ -1857,7 +1868,7 @@ async function runUp(specArgs, opts) {
1857
1868
  }
1858
1869
  await startTunnels(cf, bin, items, { detach: opts.detach, protocol, keepAlive: opts.keepAlive });
1859
1870
  }
1860
- var SERVICE_UP_TIMEOUT_MS = 2e4;
1871
+ var SERVICE_UP_TIMEOUT_MS = 3e4;
1861
1872
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1862
1873
  async function registerServices(cf, items, domain, proto, protocol) {
1863
1874
  assertServiceSupported();
@@ -1869,6 +1880,7 @@ async function registerServices(cf, items, domain, proto, protocol) {
1869
1880
  for (const item of items) {
1870
1881
  const subdomain = item.name;
1871
1882
  const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
1883
+ await patchEntry(fqdn, { state: "provisioning", pid: void 0, metricsPort: void 0 });
1872
1884
  installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
1873
1885
  fqdns.push(fqdn);
1874
1886
  }
@@ -1893,11 +1905,21 @@ async function waitServicesUp(fqdns, timeoutMs) {
1893
1905
  while (pending.size > 0) {
1894
1906
  for (const fqdn of [...pending]) {
1895
1907
  const entry = getEntry(fqdn);
1896
- if (entry?.state === "running" && entry.pid) pending.delete(fqdn);
1908
+ if (entry?.state === "running" && entry.pid && await isOurConnector(entry)) {
1909
+ if (!entry.metricsPort || (await checkLocalReady(entry.metricsPort, { timeoutMs: 500 })).healthy) {
1910
+ pending.delete(fqdn);
1911
+ }
1912
+ }
1897
1913
  }
1898
1914
  if (pending.size === 0 || Date.now() >= deadline) break;
1899
1915
  await delay(500);
1900
1916
  }
1917
+ for (const fqdn of [...pending]) {
1918
+ const entry = getEntry(fqdn);
1919
+ if (entry?.state === "running" && entry.pid && await isOurConnector(entry)) {
1920
+ pending.delete(fqdn);
1921
+ }
1922
+ }
1901
1923
  return [...pending];
1902
1924
  }
1903
1925
  function registerUp(program) {