@iamken/cloudtunnel 0.11.3 → 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.
@@ -29,6 +29,14 @@ async function createCname(token, zoneId, name, tunnelId) {
29
29
  }
30
30
  return env.result;
31
31
  }
32
+ async function findDnsRecords(token, zoneId, fqdn) {
33
+ const env = await cfRequest(
34
+ token,
35
+ "GET",
36
+ `/zones/${zoneId}/dns_records?name=${encodeURIComponent(fqdn)}`
37
+ );
38
+ return env.result ?? [];
39
+ }
32
40
  async function findCname(token, zoneId, fqdn) {
33
41
  const env = await cfRequest(
34
42
  token,
@@ -55,8 +63,9 @@ export {
55
63
  cargoTarget,
56
64
  isManagedDns,
57
65
  createCname,
66
+ findDnsRecords,
58
67
  findCname,
59
68
  listCargoCnames,
60
69
  deleteDnsRecord
61
70
  };
62
- //# sourceMappingURL=chunk-BIGZFMQ6.js.map
71
+ //# sourceMappingURL=chunk-FILVDOPL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cloudflare/dns.ts"],"sourcesContent":["import { cfPaginate, cfRequest } from \"./client.js\";\nimport type { DnsRecord } from \"./types.js\";\nimport { CliError } from \"../ui/errors.js\";\n\n/** DNS records created by cloudtunnel carry this comment (ownership marker). */\nexport const MANAGED_DNS_COMMENT = \"managed-by:cloudtunnel\";\n\nexport function cargoTarget(tunnelId: string): string {\n return `${tunnelId}.cfargotunnel.com`;\n}\n\nexport function isManagedDns(record: DnsRecord): boolean {\n return record.comment === MANAGED_DNS_COMMENT;\n}\n\n/** Create the proxied CNAME that routes a subdomain to the tunnel. Asserts the\n * record came back orange-clouded (grey-cloud would silently break routing). */\nexport async function createCname(\n token: string,\n zoneId: string,\n name: string,\n tunnelId: string,\n): Promise<DnsRecord> {\n const env = await cfRequest<DnsRecord>(token, \"POST\", `/zones/${zoneId}/dns_records`, {\n type: \"CNAME\",\n name,\n content: cargoTarget(tunnelId),\n proxied: true,\n ttl: 1,\n comment: MANAGED_DNS_COMMENT,\n });\n if (env.result.proxied !== true) {\n throw new CliError(`DNS record for ${name} was created grey-clouded (not proxied).`, {\n hint: \"cfargotunnel routing requires a proxied (orange-cloud) record\",\n });\n }\n return env.result;\n}\n\nexport async function findDnsRecords(\n token: string,\n zoneId: string,\n fqdn: string,\n): Promise<DnsRecord[]> {\n const env = await cfRequest<DnsRecord[]>(\n token,\n \"GET\",\n `/zones/${zoneId}/dns_records?name=${encodeURIComponent(fqdn)}`,\n );\n return env.result ?? [];\n}\n\nexport async function findCname(\n token: string,\n zoneId: string,\n fqdn: string,\n): Promise<DnsRecord | undefined> {\n const env = await cfRequest<DnsRecord[]>(\n token,\n \"GET\",\n `/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(fqdn)}`,\n );\n return env.result?.[0];\n}\n\n/** CNAMEs in the zone that point at a cfargotunnel target (any tool's). */\nexport async function listCargoCnames(token: string, zoneId: string): Promise<DnsRecord[]> {\n const all = await cfPaginate<DnsRecord>(token, `/zones/${zoneId}/dns_records?type=CNAME`);\n return all.filter((r) => r.content.endsWith(\".cfargotunnel.com\"));\n}\n\nexport async function deleteDnsRecord(token: string, zoneId: string, recordId: string): Promise<void> {\n try {\n await cfRequest<unknown>(token, \"DELETE\", `/zones/${zoneId}/dns_records/${recordId}`);\n } catch (err) {\n if (err instanceof CliError && err.status === 404) return;\n throw err;\n }\n}\n"],"mappings":";;;;;;;;AAKO,IAAM,sBAAsB;AAE5B,SAAS,YAAY,UAA0B;AACpD,SAAO,GAAG,QAAQ;AACpB;AAEO,SAAS,aAAa,QAA4B;AACvD,SAAO,OAAO,YAAY;AAC5B;AAIA,eAAsB,YACpB,OACA,QACA,MACA,UACoB;AACpB,QAAM,MAAM,MAAM,UAAqB,OAAO,QAAQ,UAAU,MAAM,gBAAgB;AAAA,IACpF,MAAM;AAAA,IACN;AAAA,IACA,SAAS,YAAY,QAAQ;AAAA,IAC7B,SAAS;AAAA,IACT,KAAK;AAAA,IACL,SAAS;AAAA,EACX,CAAC;AACD,MAAI,IAAI,OAAO,YAAY,MAAM;AAC/B,UAAM,IAAI,SAAS,kBAAkB,IAAI,4CAA4C;AAAA,MACnF,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,SAAO,IAAI;AACb;AAEA,eAAsB,eACpB,OACA,QACA,MACsB;AACtB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA;AAAA,IACA,UAAU,MAAM,qBAAqB,mBAAmB,IAAI,CAAC;AAAA,EAC/D;AACA,SAAO,IAAI,UAAU,CAAC;AACxB;AAEA,eAAsB,UACpB,OACA,QACA,MACgC;AAChC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA;AAAA,IACA,UAAU,MAAM,gCAAgC,mBAAmB,IAAI,CAAC;AAAA,EAC1E;AACA,SAAO,IAAI,SAAS,CAAC;AACvB;AAGA,eAAsB,gBAAgB,OAAe,QAAsC;AACzF,QAAM,MAAM,MAAM,WAAsB,OAAO,UAAU,MAAM,yBAAyB;AACxF,SAAO,IAAI,OAAO,CAAC,MAAM,EAAE,QAAQ,SAAS,mBAAmB,CAAC;AAClE;AAEA,eAAsB,gBAAgB,OAAe,QAAgB,UAAiC;AACpG,MAAI;AACF,UAAM,UAAmB,OAAO,UAAU,UAAU,MAAM,gBAAgB,QAAQ,EAAE;AAAA,EACtF,SAAS,KAAK;AACZ,QAAI,eAAe,YAAY,IAAI,WAAW,IAAK;AACnD,UAAM;AAAA,EACR;AACF;","names":[]}
@@ -5,9 +5,10 @@ import {
5
5
  createCname,
6
6
  deleteDnsRecord,
7
7
  findCname,
8
+ findDnsRecords,
8
9
  isManagedDns,
9
10
  listCargoCnames
10
- } from "./chunk-BIGZFMQ6.js";
11
+ } from "./chunk-FILVDOPL.js";
11
12
  import "./chunk-7XACBYL7.js";
12
13
  export {
13
14
  MANAGED_DNS_COMMENT,
@@ -15,7 +16,8 @@ export {
15
16
  createCname,
16
17
  deleteDnsRecord,
17
18
  findCname,
19
+ findDnsRecords,
18
20
  isManagedDns,
19
21
  listCargoCnames
20
22
  };
21
- //# sourceMappingURL=dns-3WHXRL6V.js.map
23
+ //# sourceMappingURL=dns-BNDEFUCM.js.map
package/dist/index.js CHANGED
@@ -7,8 +7,9 @@ import {
7
7
  createCname,
8
8
  deleteDnsRecord,
9
9
  findCname,
10
+ findDnsRecords,
10
11
  isManagedDns
11
- } from "./chunk-BIGZFMQ6.js";
12
+ } from "./chunk-FILVDOPL.js";
12
13
  import {
13
14
  CliError,
14
15
  DEFAULT_API_BASE,
@@ -246,11 +247,13 @@ function assertSupported() {
246
247
  }
247
248
  function install(d) {
248
249
  assertSupported();
249
- const tmp = join2(tmpdir(), label(d.fqdn));
250
+ const name = label(d.fqdn);
251
+ const tmp = join2(tmpdir(), name);
250
252
  writeFileSync(tmp, buildUnit(d), { mode: 420 });
251
253
  privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
252
254
  privileged(["systemctl", "daemon-reload"]);
253
- privileged(["systemctl", "enable", "--now", label(d.fqdn)]);
255
+ privileged(["systemctl", "enable", name]);
256
+ privileged(["systemctl", "restart", name]);
254
257
  }
255
258
  function uninstall(fqdn) {
256
259
  try {
@@ -388,7 +391,7 @@ function buildTaskXml(d) {
388
391
  " <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
389
392
  " <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>",
390
393
  " <Enabled>true</Enabled>",
391
- " </Settings>",
394
+ "</Settings>",
392
395
  ' <Actions Context="Author">',
393
396
  ` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
394
397
  " </Actions>",
@@ -408,9 +411,11 @@ function assertSupported3() {
408
411
  }
409
412
  function install3(d) {
410
413
  const file = join4(tmpdir2(), `${d.slug}.task.xml`);
414
+ const name = label3(d.fqdn);
411
415
  writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
412
- execFileSync3("schtasks", ["/Create", "/TN", label3(d.fqdn), "/XML", file, "/F"], { stdio: "inherit" });
413
- 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]);
414
419
  }
415
420
  function uninstall3(fqdn) {
416
421
  schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
@@ -940,17 +945,115 @@ function extractTgz(bytes) {
940
945
  });
941
946
  }
942
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
+
943
1045
  // src/core/up-runner.ts
944
1046
  import { join as join7 } from "path";
945
1047
  import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
946
1048
  import * as clack2 from "@clack/prompts";
947
1049
 
948
1050
  // src/connector/process.ts
949
- import { execFileSync as execFileSync6, spawn as spawn2 } from "child_process";
1051
+ import { execFileSync as execFileSync7, spawn as spawn2 } from "child_process";
950
1052
  import { openSync } from "fs";
951
1053
 
952
1054
  // src/connector/registry.ts
953
1055
  import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
1056
+ import { execFileSync as execFileSync6 } from "child_process";
954
1057
  import { readFile } from "fs/promises";
955
1058
  import os3 from "os";
956
1059
  import lockfile from "proper-lockfile";
@@ -961,6 +1064,13 @@ function currentBootId() {
961
1064
  try {
962
1065
  return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
963
1066
  } catch {
1067
+ if (process.platform === "darwin") {
1068
+ try {
1069
+ const out = execFileSync6("sysctl", ["-n", "kern.boottime"], { encoding: "utf8" }).trim();
1070
+ return `darwin-${out}-${os3.hostname()}`;
1071
+ } catch {
1072
+ }
1073
+ }
964
1074
  const bootMinute = Math.floor((Date.now() - os3.uptime() * 1e3) / 6e4);
965
1075
  return `boot-${bootMinute}-${os3.hostname()}`;
966
1076
  }
@@ -1047,6 +1157,14 @@ async function isOurConnector(entry) {
1047
1157
  return false;
1048
1158
  }
1049
1159
  }
1160
+ if (process.platform === "darwin") {
1161
+ try {
1162
+ const comm = execFileSync6("ps", ["-p", String(entry.pid), "-o", "command="], { encoding: "utf8" });
1163
+ return comm.includes("cloudflared");
1164
+ } catch {
1165
+ return false;
1166
+ }
1167
+ }
1050
1168
  return true;
1051
1169
  }
1052
1170
  async function reconcile() {
@@ -1067,7 +1185,7 @@ async function reconcile() {
1067
1185
  }
1068
1186
 
1069
1187
  // src/connector/process.ts
1070
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1188
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1071
1189
  function startConnector(opts) {
1072
1190
  const args = ["tunnel", "--metrics", "127.0.0.1:0", "run"];
1073
1191
  if (opts.protocol) args.push("--protocol", opts.protocol);
@@ -1088,7 +1206,7 @@ async function stopConnector(entry) {
1088
1206
  const pid = entry.pid;
1089
1207
  if (process.platform === "win32") {
1090
1208
  try {
1091
- execFileSync6("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1209
+ execFileSync7("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1092
1210
  } catch {
1093
1211
  return false;
1094
1212
  }
@@ -1099,7 +1217,7 @@ async function stopConnector(entry) {
1099
1217
  } catch {
1100
1218
  return false;
1101
1219
  }
1102
- await sleep(3e3);
1220
+ await sleep2(3e3);
1103
1221
  if (await isOurConnector(entry)) {
1104
1222
  try {
1105
1223
  process.kill(pid, "SIGKILL");
@@ -1109,103 +1227,6 @@ async function stopConnector(entry) {
1109
1227
  return true;
1110
1228
  }
1111
1229
 
1112
- // src/cloudflare/tunnels.ts
1113
- var MANAGED_TUNNEL_PREFIX = "ct-";
1114
- function isManagedTunnel(tunnel) {
1115
- return tunnel.name.startsWith(MANAGED_TUNNEL_PREFIX);
1116
- }
1117
- async function createTunnel(cf, name) {
1118
- const env = await cfRequest(cf.token, "POST", `/accounts/${cf.accountId}/cfd_tunnel`, {
1119
- name,
1120
- config_src: "cloudflare"
1121
- });
1122
- return env.result;
1123
- }
1124
- function listTunnels(cf) {
1125
- return cfPaginate(cf.token, `/accounts/${cf.accountId}/cfd_tunnel?is_deleted=false`);
1126
- }
1127
- async function getTunnel(cf, id) {
1128
- return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
1129
- }
1130
- async function deleteTunnel(cf, id) {
1131
- await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
1132
- }
1133
- async function cleanupConnections(cf, id) {
1134
- await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
1135
- }
1136
- async function deleteTunnelWithConnections(cf, id) {
1137
- try {
1138
- await deleteTunnel(cf, id);
1139
- } catch (err) {
1140
- if (err instanceof CliError && /active connections/i.test(err.message)) {
1141
- await cleanupConnections(cf, id);
1142
- await deleteTunnel(cf, id);
1143
- } else {
1144
- throw err;
1145
- }
1146
- }
1147
- }
1148
- async function getTunnelToken(cf, id) {
1149
- return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}/token`)).result;
1150
- }
1151
- async function putIngress(cf, id, ingress) {
1152
- await cfRequest(cf.token, "PUT", `/accounts/${cf.accountId}/cfd_tunnel/${id}/configurations`, {
1153
- config: { ingress }
1154
- });
1155
- }
1156
- async function getConnections(cf, id) {
1157
- const env = await cfRequest(
1158
- cf.token,
1159
- "GET",
1160
- `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
1161
- );
1162
- return env.result ?? [];
1163
- }
1164
-
1165
- // src/connector/health.ts
1166
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1167
- async function waitHealthy(cf, tunnelId, opts = {}) {
1168
- const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
1169
- while (Date.now() < deadline) {
1170
- if (opts.signal?.aborted) return "dead";
1171
- try {
1172
- const connections = await getConnections(cf, tunnelId);
1173
- if (connections.length > 0) return "healthy";
1174
- } catch {
1175
- }
1176
- await sleep2(2e3);
1177
- }
1178
- return opts.signal?.aborted ? "dead" : "provisioning";
1179
- }
1180
- function extractMetricsPort(log) {
1181
- const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
1182
- if (!matches.length) return void 0;
1183
- const last = matches[matches.length - 1];
1184
- const port = Number(last?.[1]);
1185
- return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
1186
- }
1187
- async function checkLocalReady(port, opts = {}) {
1188
- const controller = new AbortController();
1189
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
1190
- try {
1191
- const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
1192
- if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
1193
- const data = await res.json().catch(() => ({}));
1194
- const readyConnections = Number(data.readyConnections) || 0;
1195
- return { healthy: readyConnections > 0, readyConnections, status: res.status };
1196
- } catch {
1197
- return { healthy: false, readyConnections: 0 };
1198
- } finally {
1199
- clearTimeout(timer);
1200
- }
1201
- }
1202
- async function probeEntryState(entry, gone) {
1203
- if (gone || entry.state !== "running" || !entry.pid) return "down";
1204
- if (!entry.metricsPort) return "up";
1205
- const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
1206
- return ready.healthy ? "up" : "reconnecting";
1207
- }
1208
-
1209
1230
  // src/core/orchestrator-create.ts
1210
1231
  import { randomInt as randomInt2 } from "crypto";
1211
1232
 
@@ -1278,9 +1299,9 @@ var tunnelIdFromCname = (content) => content.replace(/\.cfargotunnel\.com\.?$/,
1278
1299
  async function createTunnelSubdomain(cf, opts) {
1279
1300
  const host = resolveHostSpec(opts, opts.defaultZone);
1280
1301
  const zone = await resolveZone(cf.token, host.zone);
1281
- const existing = await findCname(cf.token, zone.id, host.hostname);
1282
- if (existing) {
1283
- const isTunnelRecord = existing.content.endsWith(".cfargotunnel.com");
1302
+ const existingList = await findDnsRecords(cf.token, zone.id, host.hostname);
1303
+ if (existingList.length > 0) {
1304
+ const isTunnelRecord = existingList.every((r) => r.content.endsWith(".cfargotunnel.com"));
1284
1305
  if (!isTunnelRecord && !opts.force) {
1285
1306
  throw new CliError(`${host.hostname} is taken by a non-tunnel DNS record.`, {
1286
1307
  hint: "pick another --subdomain/--hostname, or pass -f/--force to replace it"
@@ -1292,7 +1313,14 @@ async function createTunnelSubdomain(cf, opts) {
1292
1313
  throw new CliError("Cancelled.", { exitCode: 130 });
1293
1314
  }
1294
1315
  }
1295
- await releaseHostname(cf, zone.id, existing);
1316
+ const prev = getEntry(host.hostname);
1317
+ if (prev) await stopConnector(prev);
1318
+ for (const rec of existingList) {
1319
+ await releaseHostname(cf, zone.id, rec);
1320
+ }
1321
+ } else {
1322
+ const prev = getEntry(host.hostname);
1323
+ if (prev) await stopConnector(prev);
1296
1324
  }
1297
1325
  await upsertEntry(host.hostname, {
1298
1326
  subdomain: host.subdomain,
@@ -1301,7 +1329,9 @@ async function createTunnelSubdomain(cf, opts) {
1301
1329
  port: opts.port,
1302
1330
  proto: opts.proto,
1303
1331
  host: opts.host,
1304
- state: "provisioning"
1332
+ state: "provisioning",
1333
+ pid: void 0,
1334
+ metricsPort: void 0
1305
1335
  });
1306
1336
  let tunnelId;
1307
1337
  let dnsRecordId;
@@ -1466,7 +1496,7 @@ async function resolveRemoteTarget(cf, target) {
1466
1496
  }
1467
1497
  const tunnel = matches[0];
1468
1498
  if (!tunnel) return null;
1469
- const { listCargoCnames } = await import("./dns-3WHXRL6V.js");
1499
+ const { listCargoCnames } = await import("./dns-BNDEFUCM.js");
1470
1500
  const { listZones: listZones3 } = await import("./zones-XNCXYCB4.js");
1471
1501
  for (const zone of await listZones3(cf.token)) {
1472
1502
  const rec = (await listCargoCnames(cf.token, zone.id)).find((r) => tunnelIdFromCname2(r.content) === tunnel.id);
@@ -1505,7 +1535,7 @@ async function listAll(cf, opts = {}) {
1505
1535
  };
1506
1536
  }));
1507
1537
  if (opts.all) {
1508
- const { listCargoCnames } = await import("./dns-3WHXRL6V.js");
1538
+ const { listCargoCnames } = await import("./dns-BNDEFUCM.js");
1509
1539
  const { listZones: listZones3 } = await import("./zones-XNCXYCB4.js");
1510
1540
  const tracked = new Set(entries.map(entryFqdn));
1511
1541
  const unmanaged = [];
@@ -1838,7 +1868,7 @@ async function runUp(specArgs, opts) {
1838
1868
  }
1839
1869
  await startTunnels(cf, bin, items, { detach: opts.detach, protocol, keepAlive: opts.keepAlive });
1840
1870
  }
1841
- var SERVICE_UP_TIMEOUT_MS = 2e4;
1871
+ var SERVICE_UP_TIMEOUT_MS = 3e4;
1842
1872
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1843
1873
  async function registerServices(cf, items, domain, proto, protocol) {
1844
1874
  assertServiceSupported();
@@ -1850,6 +1880,7 @@ async function registerServices(cf, items, domain, proto, protocol) {
1850
1880
  for (const item of items) {
1851
1881
  const subdomain = item.name;
1852
1882
  const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
1883
+ await patchEntry(fqdn, { state: "provisioning", pid: void 0, metricsPort: void 0 });
1853
1884
  installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
1854
1885
  fqdns.push(fqdn);
1855
1886
  }
@@ -1874,11 +1905,21 @@ async function waitServicesUp(fqdns, timeoutMs) {
1874
1905
  while (pending.size > 0) {
1875
1906
  for (const fqdn of [...pending]) {
1876
1907
  const entry = getEntry(fqdn);
1877
- 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
+ }
1878
1913
  }
1879
1914
  if (pending.size === 0 || Date.now() >= deadline) break;
1880
1915
  await delay(500);
1881
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
+ }
1882
1923
  return [...pending];
1883
1924
  }
1884
1925
  function registerUp(program) {