clivly 0.10.5 → 0.10.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.
Files changed (3) hide show
  1. package/index.cjs +64 -8
  2. package/index.mjs +64 -8
  3. package/package.json +1 -1
package/index.cjs CHANGED
@@ -480,23 +480,38 @@ function cloudflaredMissingMessage() {
480
480
  * Warning someone away from a tunnel that was fine is worse than staying quiet,
481
481
  * for the same reason the port probe defaults to assuming the port is alive.
482
482
  */
483
- /** How long to wait before giving up and returning `unknown`. */
483
+ /** How long any single probe attempt may take. */
484
484
  const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
485
485
  /**
486
- * Probe the published tunnel URL and report who is on the other end.
486
+ * How long to keep retrying before giving up.
487
487
  *
488
- * `fetchImpl` is injectable so tests do not need a network or a live tunnel.
488
+ * The probe fires the moment `cloudflared` publishes a URL, which on a
489
+ * Vite/Nuxt app is *before* the dev server has finished building its route
490
+ * table. The first attempt therefore reaches a server that is up but does not
491
+ * yet serve the tick route — a cold start, not a wrong port. The condition
492
+ * being detected (a tunnel aimed at the wrong process) does not resolve
493
+ * itself, so waiting costs nothing and a premature verdict costs a lot.
489
494
  */
490
- async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
495
+ const IDENTITY_RETRY_BUDGET_MS = 2e4;
496
+ const IDENTITY_RETRY_DELAY_MS = 2e3;
497
+ const sleep = (ms) => new Promise((resolve) => {
498
+ setTimeout(resolve, ms);
499
+ });
500
+ /** One attempt. Separated so the retry loop stays readable. */
501
+ async function attempt(triggerUrl, fetchImpl) {
491
502
  const controller = new AbortController();
492
503
  const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
493
504
  try {
494
- return (await fetchImpl(triggerUrl, {
505
+ const response = await fetchImpl(triggerUrl, {
495
506
  method: "POST",
496
507
  headers: { "content-type": "application/json" },
497
508
  body: "{}",
498
509
  signal: controller.signal
499
- })).status === 401 ? "clivly" : "mismatch";
510
+ });
511
+ if (response.status === 401) return "clivly";
512
+ if (response.status === 403) return "host-rejected";
513
+ if (response.status === 404 || response.status >= 500) return "unknown";
514
+ return "mismatch";
500
515
  } catch {
501
516
  return "unknown";
502
517
  } finally {
@@ -504,6 +519,40 @@ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
504
519
  }
505
520
  }
506
521
  /**
522
+ * Probe the published tunnel URL and report who is on the other end.
523
+ *
524
+ * Retries while the answer is inconclusive, because the first attempt lands
525
+ * during the dev server's boot. Only a confident contradiction is reported.
526
+ *
527
+ * `fetchImpl` and `now` are injectable so tests need no network or clock.
528
+ */
529
+ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch, now = Date.now) {
530
+ const deadline = now() + IDENTITY_RETRY_BUDGET_MS;
531
+ let last = "unknown";
532
+ do {
533
+ last = await attempt(triggerUrl, fetchImpl);
534
+ if (last !== "unknown") return last;
535
+ if (now() >= deadline) break;
536
+ await sleep(IDENTITY_RETRY_DELAY_MS);
537
+ } while (now() < deadline);
538
+ return last;
539
+ }
540
+ /**
541
+ * What to tell someone whose dev server refused the tunnel's Host header.
542
+ *
543
+ * `clivly dev` already prints the allowedHosts hint for Vite frameworks before
544
+ * the tunnel opens; this fires when the tunnel proves it was not applied.
545
+ */
546
+ function hostRejectedMessage() {
547
+ return [
548
+ "Warning: your dev server refused the tunnel's Host header (403).",
549
+ " Vite rejects hostnames it does not recognise, so Clivly's request never",
550
+ " reaches your tick route. Add the tunnel domain to your Vite config:",
551
+ " server: { allowedHosts: ['.trycloudflare.com'] }",
552
+ " (in nuxt.config.ts, nest it under `vite:`), then restart the dev server."
553
+ ].join("\n");
554
+ }
555
+ /**
507
556
  * What to tell someone whose tunnel reaches the wrong process.
508
557
  *
509
558
  * Names the likely cause rather than only the symptom: the overwhelmingly
@@ -526,6 +575,12 @@ const UNREACHABLE_RESULT = {
526
575
  ok: false,
527
576
  kind: "unreachable"
528
577
  };
578
+ /** The line to print for a probe verdict, or null when there is nothing to say. */
579
+ function tunnelWarning(identity, port) {
580
+ if (identity === "mismatch") return tunnelMismatchMessage(port);
581
+ if (identity === "host-rejected") return hostRejectedMessage();
582
+ return null;
583
+ }
529
584
  function flagValue$1(argv, flag) {
530
585
  const i = argv.indexOf(flag);
531
586
  return i >= 0 ? argv[i + 1] : void 0;
@@ -570,9 +625,10 @@ async function runDev(options) {
570
625
  if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
571
626
  }
572
627
  const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
573
- if (identity === "mismatch") deps.out(tunnelMismatchMessage(port));
628
+ const warning = tunnelWarning(identity, port);
629
+ if (warning) deps.out(warning);
574
630
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
575
- if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
631
+ if (result.ok) deps.out(identity === "mismatch" || identity === "host-rejected" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
576
632
  else deps.out(overrideFailureMessage(result.kind, result.status));
577
633
  });
578
634
  const stopWatching = deps.watchConfig?.(options.cwd, () => {
package/index.mjs CHANGED
@@ -479,23 +479,38 @@ function cloudflaredMissingMessage() {
479
479
  * Warning someone away from a tunnel that was fine is worse than staying quiet,
480
480
  * for the same reason the port probe defaults to assuming the port is alive.
481
481
  */
482
- /** How long to wait before giving up and returning `unknown`. */
482
+ /** How long any single probe attempt may take. */
483
483
  const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
484
484
  /**
485
- * Probe the published tunnel URL and report who is on the other end.
485
+ * How long to keep retrying before giving up.
486
486
  *
487
- * `fetchImpl` is injectable so tests do not need a network or a live tunnel.
487
+ * The probe fires the moment `cloudflared` publishes a URL, which on a
488
+ * Vite/Nuxt app is *before* the dev server has finished building its route
489
+ * table. The first attempt therefore reaches a server that is up but does not
490
+ * yet serve the tick route — a cold start, not a wrong port. The condition
491
+ * being detected (a tunnel aimed at the wrong process) does not resolve
492
+ * itself, so waiting costs nothing and a premature verdict costs a lot.
488
493
  */
489
- async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
494
+ const IDENTITY_RETRY_BUDGET_MS = 2e4;
495
+ const IDENTITY_RETRY_DELAY_MS = 2e3;
496
+ const sleep = (ms) => new Promise((resolve) => {
497
+ setTimeout(resolve, ms);
498
+ });
499
+ /** One attempt. Separated so the retry loop stays readable. */
500
+ async function attempt(triggerUrl, fetchImpl) {
490
501
  const controller = new AbortController();
491
502
  const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
492
503
  try {
493
- return (await fetchImpl(triggerUrl, {
504
+ const response = await fetchImpl(triggerUrl, {
494
505
  method: "POST",
495
506
  headers: { "content-type": "application/json" },
496
507
  body: "{}",
497
508
  signal: controller.signal
498
- })).status === 401 ? "clivly" : "mismatch";
509
+ });
510
+ if (response.status === 401) return "clivly";
511
+ if (response.status === 403) return "host-rejected";
512
+ if (response.status === 404 || response.status >= 500) return "unknown";
513
+ return "mismatch";
499
514
  } catch {
500
515
  return "unknown";
501
516
  } finally {
@@ -503,6 +518,40 @@ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
503
518
  }
504
519
  }
505
520
  /**
521
+ * Probe the published tunnel URL and report who is on the other end.
522
+ *
523
+ * Retries while the answer is inconclusive, because the first attempt lands
524
+ * during the dev server's boot. Only a confident contradiction is reported.
525
+ *
526
+ * `fetchImpl` and `now` are injectable so tests need no network or clock.
527
+ */
528
+ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch, now = Date.now) {
529
+ const deadline = now() + IDENTITY_RETRY_BUDGET_MS;
530
+ let last = "unknown";
531
+ do {
532
+ last = await attempt(triggerUrl, fetchImpl);
533
+ if (last !== "unknown") return last;
534
+ if (now() >= deadline) break;
535
+ await sleep(IDENTITY_RETRY_DELAY_MS);
536
+ } while (now() < deadline);
537
+ return last;
538
+ }
539
+ /**
540
+ * What to tell someone whose dev server refused the tunnel's Host header.
541
+ *
542
+ * `clivly dev` already prints the allowedHosts hint for Vite frameworks before
543
+ * the tunnel opens; this fires when the tunnel proves it was not applied.
544
+ */
545
+ function hostRejectedMessage() {
546
+ return [
547
+ "Warning: your dev server refused the tunnel's Host header (403).",
548
+ " Vite rejects hostnames it does not recognise, so Clivly's request never",
549
+ " reaches your tick route. Add the tunnel domain to your Vite config:",
550
+ " server: { allowedHosts: ['.trycloudflare.com'] }",
551
+ " (in nuxt.config.ts, nest it under `vite:`), then restart the dev server."
552
+ ].join("\n");
553
+ }
554
+ /**
506
555
  * What to tell someone whose tunnel reaches the wrong process.
507
556
  *
508
557
  * Names the likely cause rather than only the symptom: the overwhelmingly
@@ -525,6 +574,12 @@ const UNREACHABLE_RESULT = {
525
574
  ok: false,
526
575
  kind: "unreachable"
527
576
  };
577
+ /** The line to print for a probe verdict, or null when there is nothing to say. */
578
+ function tunnelWarning(identity, port) {
579
+ if (identity === "mismatch") return tunnelMismatchMessage(port);
580
+ if (identity === "host-rejected") return hostRejectedMessage();
581
+ return null;
582
+ }
528
583
  function flagValue$1(argv, flag) {
529
584
  const i = argv.indexOf(flag);
530
585
  return i >= 0 ? argv[i + 1] : void 0;
@@ -569,9 +624,10 @@ async function runDev(options) {
569
624
  if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
570
625
  }
571
626
  const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
572
- if (identity === "mismatch") deps.out(tunnelMismatchMessage(port));
627
+ const warning = tunnelWarning(identity, port);
628
+ if (warning) deps.out(warning);
573
629
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
574
- if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
630
+ if (result.ok) deps.out(identity === "mismatch" || identity === "host-rejected" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
575
631
  else deps.out(overrideFailureMessage(result.kind, result.status));
576
632
  });
577
633
  const stopWatching = deps.watchConfig?.(options.cwd, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",