@cliftonc/cfld 0.2.5 → 0.2.6

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/cli.js CHANGED
@@ -3,12 +3,12 @@
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
5
  import { readFileSync as readFileSync7, realpathSync } from "fs";
6
- import { dirname, join as join7 } from "path";
6
+ import { dirname, join as join8 } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import pc7 from "picocolors";
9
9
 
10
10
  // src/commands/run.ts
11
- import { join as join5 } from "path";
11
+ import { join as join6 } from "path";
12
12
 
13
13
  // src/core/state-machine.ts
14
14
  import { existsSync as existsSync2 } from "fs";
@@ -392,6 +392,24 @@ function certHasZone(certPath) {
392
392
  const token = readCertToken(certPath);
393
393
  return Boolean(token?.zoneID);
394
394
  }
395
+ async function certZoneName(certPath) {
396
+ const token = readCertToken(certPath);
397
+ if (!token?.zoneID || !token.apiToken) return void 0;
398
+ try {
399
+ const res = await fetch(
400
+ `https://api.cloudflare.com/client/v4/zones/${token.zoneID}`,
401
+ {
402
+ headers: { Authorization: `Bearer ${token.apiToken}` },
403
+ signal: AbortSignal.timeout(5e3)
404
+ }
405
+ );
406
+ if (!res.ok) return void 0;
407
+ const body = await res.json();
408
+ return body.result?.name?.toLowerCase();
409
+ } catch {
410
+ return void 0;
411
+ }
412
+ }
395
413
 
396
414
  // src/core/cert-store.ts
397
415
  function ensureStoreDir() {
@@ -408,8 +426,19 @@ function listStoredZones() {
408
426
  return [];
409
427
  }
410
428
  }
429
+ function storedZoneClaiming(certPath, except) {
430
+ const zoneID = readCertToken(certPath)?.zoneID;
431
+ if (!zoneID) return void 0;
432
+ return listStoredZones().find(
433
+ (z) => z !== except && readCertToken(certPathForZone(z))?.zoneID === zoneID
434
+ );
435
+ }
411
436
  function resolveCert(zone) {
412
- return storedCertForZone(zone) ?? (certExists(defaultCertPath()) ? defaultCertPath() : void 0);
437
+ const stored = storedCertForZone(zone);
438
+ if (stored) return stored;
439
+ const def = defaultCertPath();
440
+ if (!certExists(def) || storedZoneClaiming(def, zone)) return void 0;
441
+ return def;
413
442
  }
414
443
  function importCert(zone, fromPath = defaultCertPath()) {
415
444
  ensureStoreDir();
@@ -936,7 +965,8 @@ function colorLevel(level) {
936
965
 
937
966
  // src/commands/login.ts
938
967
  import { spawn as spawn4 } from "child_process";
939
- import { existsSync as existsSync8 } from "fs";
968
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync3, renameSync, rmSync } from "fs";
969
+ import { join as join4 } from "path";
940
970
 
941
971
  // src/ui/prompts.ts
942
972
  async function clack() {
@@ -1031,23 +1061,73 @@ function normalizeZone(input) {
1031
1061
  return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
1032
1062
  }
1033
1063
  async function loginCommand(flags) {
1034
- const interactive = isInteractive();
1035
1064
  const haveCert = certExists(defaultCertPath());
1065
+ let certPath = defaultCertPath();
1036
1066
  if (flags.reauth || !haveCert) {
1037
- await runLogin();
1067
+ certPath = await runLogin();
1038
1068
  } else {
1039
1069
  info("A Cloudflare certificate already exists. Use --reauth to authorize another domain.");
1040
1070
  }
1041
- if (!certHasZone(defaultCertPath())) {
1042
- printZeroZoneGuidance();
1071
+ try {
1072
+ if (!certHasZone(certPath)) {
1073
+ printZeroZoneGuidance();
1074
+ throw new CfldError(
1075
+ "Logged in, but your account has no domain to authorize.",
1076
+ "Add a domain (see above), then run `cfld login --reauth`."
1077
+ );
1078
+ }
1079
+ const zone = await saveCert(flags.zone ? normalizeZone(flags.zone) : void 0, certPath);
1080
+ done(`Saved cert for ${zone}. You're ready \u2014 run \`cfld\`.`);
1081
+ } finally {
1082
+ discardLoginCert(certPath);
1083
+ }
1084
+ }
1085
+ async function runLogin() {
1086
+ const bin = await resolveBinary();
1087
+ const def = defaultCertPath();
1088
+ const backup = existsSync8(def) ? `${def}.cfld-backup` : void 0;
1089
+ if (backup) renameSync(def, backup);
1090
+ try {
1091
+ step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
1092
+ note(" Pick the domain you want to use. Waiting for you to finish\u2026");
1093
+ await new Promise((resolve, reject) => {
1094
+ const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
1095
+ child.on("error", reject);
1096
+ child.on("exit", (code) => {
1097
+ if (code === 0 || existsSync8(def)) resolve();
1098
+ else reject(new CfldError("Cloudflare login did not complete.", "Re-run `cfld login`."));
1099
+ });
1100
+ });
1101
+ if (!existsSync8(def)) {
1102
+ throw new CfldError(
1103
+ "Login finished but no certificate was written.",
1104
+ "Re-run `cfld login` and complete the browser step."
1105
+ );
1106
+ }
1107
+ if (!backup) return def;
1108
+ mkdirSync3(cfldDir(), { recursive: true });
1109
+ const fresh = join4(cfldDir(), "login-cert.tmp");
1110
+ copyFileSync2(def, fresh);
1111
+ rmSync(def);
1112
+ return fresh;
1113
+ } finally {
1114
+ if (backup) renameSync(backup, def);
1115
+ }
1116
+ }
1117
+ function discardLoginCert(certPath) {
1118
+ if (certPath !== defaultCertPath()) rmSync(certPath, { force: true });
1119
+ }
1120
+ async function saveCert(expected, certPath) {
1121
+ const actual = await certZoneName(certPath);
1122
+ if (expected && actual && actual !== expected) {
1043
1123
  throw new CfldError(
1044
- "Logged in, but your account has no domain to authorize.",
1045
- "Add a domain (see above), then run `cfld login --reauth`."
1124
+ `You authorized ${actual} in the browser, not ${expected}.`,
1125
+ `Run \`cfld login --reauth --zone ${expected}\` and pick ${expected} this time.`
1046
1126
  );
1047
1127
  }
1048
- let zone = flags.zone ? normalizeZone(flags.zone) : void 0;
1128
+ let zone = expected ?? actual;
1049
1129
  if (!zone) {
1050
- if (!interactive) {
1130
+ if (!isInteractive()) {
1051
1131
  throw new CfldError(
1052
1132
  "Pass --zone to record which domain this cert is for.",
1053
1133
  "e.g. `cfld login --zone example.com`"
@@ -1055,48 +1135,57 @@ async function loginCommand(flags) {
1055
1135
  }
1056
1136
  zone = normalizeZone(await askText("Which domain did you authorize?", "example.com"));
1057
1137
  }
1058
- importCert(zone);
1059
- done(`Saved cert for ${zone}. You're ready \u2014 run \`cfld\`.`);
1060
- }
1061
- async function runLogin() {
1062
- const bin = await resolveBinary();
1063
- step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
1064
- note(" Pick the domain you want to use. Waiting for you to finish\u2026");
1065
- await new Promise((resolve, reject) => {
1066
- const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
1067
- child.on("error", reject);
1068
- child.on("exit", (code) => {
1069
- if (code === 0 || existsSync8(defaultCertPath())) resolve();
1070
- else reject(new CfldError("Cloudflare login did not complete.", "Re-run `cfld login`."));
1071
- });
1072
- });
1073
- if (!existsSync8(defaultCertPath())) {
1138
+ const clash = storedZoneClaiming(certPath, zone);
1139
+ if (clash && !actual) {
1074
1140
  throw new CfldError(
1075
- "Login finished but no certificate was written.",
1076
- "Re-run `cfld login` and complete the browser step."
1141
+ `That cert is for the same domain as your saved ${clash} cert, not ${zone}.`,
1142
+ `Run \`cfld login --reauth --zone ${zone}\` and pick ${zone} in the browser.`
1077
1143
  );
1078
1144
  }
1145
+ importCert(zone, certPath);
1146
+ return zone;
1079
1147
  }
1080
1148
  async function ensureCert(zone, opts = { interactive: true }) {
1081
- const existing = resolveCert(zone);
1082
- if (existing) return existing;
1149
+ const usable = await usableCert(zone);
1150
+ if (usable) return usable;
1083
1151
  if (!opts.interactive) {
1084
1152
  throw new CfldError(
1085
1153
  `No Cloudflare certificate found for ${zone}.`,
1086
- "Run `cfld login` first (needs a browser), or set TUNNEL_ORIGIN_CERT."
1154
+ `Run \`cfld login --reauth --zone ${zone}\` first (needs a browser), or set TUNNEL_ORIGIN_CERT.`
1087
1155
  );
1088
1156
  }
1089
- await runLogin();
1090
- if (!certHasZone(defaultCertPath())) {
1091
- printZeroZoneGuidance();
1092
- throw new CfldError(
1093
- "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1094
- "Add a domain (see above), then re-run. Or use `cfld --quick` for a throwaway URL now."
1095
- );
1157
+ info(`No certificate for ${zone} yet \u2014 authorize it in the browser.`);
1158
+ const fresh = await runLogin();
1159
+ try {
1160
+ if (!certHasZone(fresh)) {
1161
+ printZeroZoneGuidance();
1162
+ throw new CfldError(
1163
+ "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1164
+ "Add a domain (see above), then re-run. Or use `cfld --quick` for a throwaway URL now."
1165
+ );
1166
+ }
1167
+ await saveCert(zone, fresh);
1168
+ } finally {
1169
+ discardLoginCert(fresh);
1096
1170
  }
1097
- const stored = importCert(zone);
1098
1171
  info(`Saved cert for ${zone}.`);
1099
- return stored;
1172
+ return certPathForZone(zone);
1173
+ }
1174
+ async function usableCert(zone) {
1175
+ const stored = storedCertForZone(zone);
1176
+ if (stored) {
1177
+ if (!storedZoneClaiming(stored, zone)) return stored;
1178
+ const actual2 = await certZoneName(stored);
1179
+ if (!actual2 || actual2 === zone) return stored;
1180
+ warn(`The saved cert for ${zone} is actually for ${actual2} \u2014 discarding it.`);
1181
+ rmSync(stored);
1182
+ }
1183
+ const def = defaultCertPath();
1184
+ if (!certHasZone(def)) return void 0;
1185
+ const actual = await certZoneName(def);
1186
+ if (actual === zone) return importCert(zone, def);
1187
+ if (!actual && !storedZoneClaiming(def, zone)) return def;
1188
+ return void 0;
1100
1189
  }
1101
1190
  function printZeroZoneGuidance() {
1102
1191
  warn("cfld can't create a permanent custom URL yet \u2014 your account has no domains.");
@@ -1123,11 +1212,11 @@ function printZeroZoneGuidance() {
1123
1212
 
1124
1213
  // src/util/gitignore.ts
1125
1214
  import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1126
- import { join as join4 } from "path";
1215
+ import { join as join5 } from "path";
1127
1216
  function ensureGitignored(cwd, pattern) {
1128
- const gitignorePath = join4(cwd, ".gitignore");
1217
+ const gitignorePath = join5(cwd, ".gitignore");
1129
1218
  const hasGitignore = existsSync9(gitignorePath);
1130
- const inRepo = hasGitignore || existsSync9(join4(cwd, ".git"));
1219
+ const inRepo = hasGitignore || existsSync9(join5(cwd, ".git"));
1131
1220
  if (!inRepo) return false;
1132
1221
  const existing = hasGitignore ? readFileSync6(gitignorePath, "utf8") : "";
1133
1222
  const present = existing.split("\n").map((l) => l.trim()).includes(pattern);
@@ -1309,7 +1398,7 @@ ${opts.reason}`);
1309
1398
  );
1310
1399
  process.once("SIGHUP", () => shutdown({ reason: "Terminal closed \u2014 stopping." }));
1311
1400
  if (interactive) {
1312
- const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
1401
+ const { startDashboard } = await import("./dashboard-Z4AN7H5V.js");
1313
1402
  dashboard = startDashboard(
1314
1403
  {
1315
1404
  url,
@@ -1376,7 +1465,7 @@ ${opts.reason}`);
1376
1465
  if (!up) warn(`Nothing is listening on :${ctx.port} yet \u2014 going live anyway.`);
1377
1466
  }
1378
1467
  if (!ctx.noEnv) {
1379
- const { changed } = upsertEnv(join5(ctx.cwd, ".env"), ctx.envKey, url);
1468
+ const { changed } = upsertEnv(join6(ctx.cwd, ".env"), ctx.envKey, url);
1380
1469
  if (changed && !dashboard) info(`Wrote ${ctx.envKey}=${url} to .env`);
1381
1470
  }
1382
1471
  if (dashboard) {
@@ -1553,7 +1642,7 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1553
1642
  });
1554
1643
  }
1555
1644
  if (interactive) {
1556
- const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
1645
+ const { startDashboard } = await import("./dashboard-Z4AN7H5V.js");
1557
1646
  dashboard = startDashboard({
1558
1647
  url,
1559
1648
  target,
@@ -1753,20 +1842,26 @@ async function setupCommand(flags) {
1753
1842
  );
1754
1843
  return;
1755
1844
  }
1756
- await runLogin();
1757
- if (!certHasZone(defaultCertPath())) {
1758
- printZeroZoneGuidance();
1759
- if (await askConfirm("Start a temporary quick tunnel now instead?")) {
1760
- await quickCommand(flags);
1761
- return;
1845
+ const fresh = await runLogin();
1846
+ let zone;
1847
+ try {
1848
+ if (!certHasZone(fresh)) {
1849
+ printZeroZoneGuidance();
1850
+ if (await askConfirm("Start a temporary quick tunnel now instead?")) {
1851
+ await quickCommand(flags);
1852
+ return;
1853
+ }
1854
+ throw new CfldError(
1855
+ "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1856
+ "Add a domain (see above), then run `cfld setup` again."
1857
+ );
1762
1858
  }
1763
- throw new CfldError(
1764
- "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1765
- "Add a domain (see above), then run `cfld setup` again."
1766
- );
1859
+ const detected = await certZoneName(fresh);
1860
+ zone = await saveCert(detected ?? await askAuthorizedZone(knownZones), fresh);
1861
+ } finally {
1862
+ discardLoginCert(fresh);
1767
1863
  }
1768
- const zone = await askAuthorizedZone(knownZones);
1769
- const certPath = importCert(zone);
1864
+ const certPath = certPathForZone(zone);
1770
1865
  info(`Saved cert for ${zone}.`);
1771
1866
  await configureProject(flags, { zone, certPath, cwd });
1772
1867
  await offerGoLive(flags, zone);
@@ -1997,9 +2092,9 @@ function resolveCurrentEntry2() {
1997
2092
  import pc5 from "picocolors";
1998
2093
 
1999
2094
  // src/core/service.ts
2000
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync6 } from "fs";
2095
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
2001
2096
  import { homedir as homedir2, platform } from "os";
2002
- import { join as join6 } from "path";
2097
+ import { join as join7 } from "path";
2003
2098
  function serviceSlug(name) {
2004
2099
  return name.replace(/^cfld-/, "");
2005
2100
  }
@@ -2007,16 +2102,16 @@ function launchAgentLabel(name) {
2007
2102
  return `com.cfld.${serviceSlug(name)}`;
2008
2103
  }
2009
2104
  function launchAgentPath(name) {
2010
- return join6(homedir2(), "Library", "LaunchAgents", `${launchAgentLabel(name)}.plist`);
2105
+ return join7(homedir2(), "Library", "LaunchAgents", `${launchAgentLabel(name)}.plist`);
2011
2106
  }
2012
2107
  function systemdUnitName(name) {
2013
2108
  return `cfld-${serviceSlug(name)}.service`;
2014
2109
  }
2015
2110
  function systemdUnitPath(name) {
2016
- return join6(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
2111
+ return join7(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
2017
2112
  }
2018
2113
  function logDir() {
2019
- return join6(cfldDir(), "logs");
2114
+ return join7(cfldDir(), "logs");
2020
2115
  }
2021
2116
  function renderLaunchAgent(spec) {
2022
2117
  const args = [
@@ -2030,8 +2125,8 @@ function renderLaunchAgent(spec) {
2030
2125
  spec.name
2031
2126
  ];
2032
2127
  const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
2033
- const out = join6(logDir(), `${serviceSlug(spec.name)}.out.log`);
2034
- const err = join6(logDir(), `${serviceSlug(spec.name)}.err.log`);
2128
+ const out = join7(logDir(), `${serviceSlug(spec.name)}.out.log`);
2129
+ const err = join7(logDir(), `${serviceSlug(spec.name)}.err.log`);
2035
2130
  return `<?xml version="1.0" encoding="UTF-8"?>
2036
2131
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2037
2132
  <plist version="1.0">
@@ -2096,10 +2191,10 @@ function assertSupported() {
2096
2191
  }
2097
2192
  async function installService(spec) {
2098
2193
  const p = assertSupported();
2099
- mkdirSync3(logDir(), { recursive: true });
2194
+ mkdirSync4(logDir(), { recursive: true });
2100
2195
  if (p === "darwin") {
2101
2196
  const path = launchAgentPath(spec.name);
2102
- mkdirSync3(join6(homedir2(), "Library", "LaunchAgents"), { recursive: true });
2197
+ mkdirSync4(join7(homedir2(), "Library", "LaunchAgents"), { recursive: true });
2103
2198
  writeFileSync6(path, renderLaunchAgent(spec), "utf8");
2104
2199
  await exec("launchctl", ["unload", path]);
2105
2200
  const res = await exec("launchctl", ["load", "-w", path]);
@@ -2111,7 +2206,7 @@ async function installService(spec) {
2111
2206
  }
2112
2207
  } else {
2113
2208
  const path = systemdUnitPath(spec.name);
2114
- mkdirSync3(join6(homedir2(), ".config", "systemd", "user"), { recursive: true });
2209
+ mkdirSync4(join7(homedir2(), ".config", "systemd", "user"), { recursive: true });
2115
2210
  writeFileSync6(path, renderSystemdUnit(spec), "utf8");
2116
2211
  await exec("systemctl", ["--user", "daemon-reload"]);
2117
2212
  const res = await exec("systemctl", [
@@ -2134,13 +2229,13 @@ async function uninstallService(name) {
2134
2229
  const path = launchAgentPath(name);
2135
2230
  if (existsSync10(path)) {
2136
2231
  await exec("launchctl", ["unload", "-w", path]);
2137
- rmSync(path, { force: true });
2232
+ rmSync2(path, { force: true });
2138
2233
  }
2139
2234
  } else {
2140
2235
  const unit = systemdUnitName(name);
2141
2236
  await exec("systemctl", ["--user", "disable", "--now", unit]);
2142
2237
  const path = systemdUnitPath(name);
2143
- if (existsSync10(path)) rmSync(path, { force: true });
2238
+ if (existsSync10(path)) rmSync2(path, { force: true });
2144
2239
  await exec("systemctl", ["--user", "daemon-reload"]);
2145
2240
  }
2146
2241
  }
@@ -2260,6 +2355,14 @@ async function doctorCommand() {
2260
2355
  const zones = listStoredZones();
2261
2356
  if (zones.length) out(` ${ok} cert store: ${zones.join(", ")}`);
2262
2357
  else out(` ${pc6.dim("\xB7")} cert store empty`);
2358
+ for (const zone of zones) {
2359
+ const actual = await certZoneName(certPathForZone(zone));
2360
+ if (actual && actual !== zone) {
2361
+ out(
2362
+ ` ${bad} ${zone}.pem is really a cert for ${actual} \u2014 fix with \`cfld login --reauth --zone ${zone}\``
2363
+ );
2364
+ }
2365
+ }
2263
2366
  const entries = listEntries();
2264
2367
  out(` ${ok} registry: ${entries.length} tunnel(s)`);
2265
2368
  out("");
@@ -2269,7 +2372,7 @@ async function doctorCommand() {
2269
2372
  function readVersion() {
2270
2373
  try {
2271
2374
  const here = dirname(fileURLToPath(import.meta.url));
2272
- const pkg = JSON.parse(readFileSync7(join7(here, "../package.json"), "utf8"));
2375
+ const pkg = JSON.parse(readFileSync7(join8(here, "../package.json"), "utf8"));
2273
2376
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
2274
2377
  } catch {
2275
2378
  return "0.0.0";
@@ -94,7 +94,12 @@ function LogColumn({
94
94
  borderLeft: Boolean(divider),
95
95
  children: [
96
96
  title ? /* @__PURE__ */ jsx(Text, { color: titleColor, bold: true, wrap: "truncate-end", children: title }) : null,
97
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-end", overflow: "hidden", children: lines.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : lines.slice(-rows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: line }, i)) })
97
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-end", overflow: "hidden", children: lines.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : (
98
+ // Wrap rather than truncate so long lines (e.g. magic-link / OAuth
99
+ // URLs) stay fully visible and selectable instead of ending in "…".
100
+ // The bottom-pinned box clips older wrapped lines off the top.
101
+ lines.slice(-rows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: line }, i))
102
+ ) })
98
103
  ]
99
104
  }
100
105
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Zero-token, persistent Cloudflare tunnel CLI for local dev — same public HTTPS URL every run.",
5
5
  "type": "module",
6
6
  "bin": {