@envpilot/cli 1.6.0 → 1.6.1

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.
@@ -4,7 +4,7 @@ import {
4
4
  getTopLevelCommandCatalog,
5
5
  getUser,
6
6
  isAuthenticated
7
- } from "./chunk-RUYGAIH5.js";
7
+ } from "./chunk-TPYA3DO2.js";
8
8
 
9
9
  // src/ui/app.tsx
10
10
  import {
@@ -7,7 +7,7 @@ function initSentry() {
7
7
  Sentry.init({
8
8
  dsn,
9
9
  environment: "cli",
10
- release: true ? "1.6.0" : "0.0.0",
10
+ release: true ? "1.6.1" : "0.0.0",
11
11
  // Free tier: disable performance monitoring
12
12
  tracesSampleRate: 0,
13
13
  beforeSend(event) {
@@ -49,6 +49,26 @@ async function flushSentry() {
49
49
  // src/lib/config.ts
50
50
  import Conf from "conf";
51
51
  var DEFAULT_API_URL = "https://www.envpilot.dev";
52
+ var HOST_CANONICALIZATION = {
53
+ "envpilot.dev": "www.envpilot.dev"
54
+ };
55
+ function normalizeApiUrl(raw) {
56
+ let value = raw.trim();
57
+ if (!/^https?:\/\//i.test(value)) {
58
+ value = `https://${value}`;
59
+ }
60
+ try {
61
+ const parsed = new URL(value);
62
+ const canonicalHost = HOST_CANONICALIZATION[parsed.hostname.toLowerCase()];
63
+ if (canonicalHost) {
64
+ parsed.hostname = canonicalHost;
65
+ }
66
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
67
+ return parsed.toString().replace(/\/$/, "");
68
+ } catch {
69
+ return value;
70
+ }
71
+ }
52
72
  var config = new Conf({
53
73
  projectName: "envpilot",
54
74
  defaults: {
@@ -57,7 +77,7 @@ var config = new Conf({
57
77
  });
58
78
  function getConfig() {
59
79
  return {
60
- apiUrl: config.get("apiUrl") ?? DEFAULT_API_URL,
80
+ apiUrl: normalizeApiUrl(config.get("apiUrl") ?? DEFAULT_API_URL),
61
81
  accessToken: config.get("accessToken"),
62
82
  refreshToken: config.get("refreshToken"),
63
83
  activeProjectId: config.get("activeProjectId"),
@@ -67,10 +87,12 @@ function getConfig() {
67
87
  };
68
88
  }
69
89
  function getApiUrl() {
70
- return config.get("apiUrl") ?? DEFAULT_API_URL;
90
+ const stored = config.get("apiUrl");
91
+ const raw = stored ?? DEFAULT_API_URL;
92
+ return normalizeApiUrl(raw);
71
93
  }
72
94
  function setApiUrl(url) {
73
- config.set("apiUrl", url);
95
+ config.set("apiUrl", normalizeApiUrl(url));
74
96
  }
75
97
  function getAccessToken() {
76
98
  return config.get("accessToken");
@@ -157,7 +179,7 @@ import { jsx } from "react/jsx-runtime";
157
179
  async function openTUI() {
158
180
  const [{ render }, { CLIApp }] = await Promise.all([
159
181
  import("ink"),
160
- import("./app-UJRUWQKL.js")
182
+ import("./app-LNI6WTJZ.js")
161
183
  ]);
162
184
  while (true) {
163
185
  let selectedArgv = null;
@@ -439,6 +461,12 @@ import chalk4 from "chalk";
439
461
  import { hostname } from "os";
440
462
 
441
463
  // src/lib/api.ts
464
+ function registrableDomain(hostname2) {
465
+ const parts = hostname2.toLowerCase().split(".").filter(Boolean);
466
+ if (parts.length <= 2) return parts.join(".");
467
+ return parts.slice(-2).join(".");
468
+ }
469
+ var MAX_MANUAL_REDIRECTS = 5;
442
470
  var APIError = class extends Error {
443
471
  constructor(message, statusCode, code) {
444
472
  super(message);
@@ -474,7 +502,59 @@ var APIClient = class {
474
502
  const finalUrl = response.url || "";
475
503
  const contentType = response.headers.get("content-type") || "";
476
504
  const preview = (bodyText || "").slice(0, 512).toLowerCase();
477
- return response.redirected || location.includes("authkit") || finalUrl.includes("authkit") || contentType.includes("text/html") && (preview.includes("authorization_session_id") || preview.includes("client_id=") || preview.includes("<!doctype html"));
505
+ return location.includes("authkit") || finalUrl.includes("authkit") || contentType.includes("text/html") && (preview.includes("authorization_session_id") || preview.includes("client_id=") || preview.includes("<!doctype html"));
506
+ }
507
+ /**
508
+ * Perform a fetch that follows 3xx redirects manually, re-attaching the
509
+ * Authorization header when the redirect stays inside the same registrable
510
+ * domain (eTLD+1). This defends against the apex→www redirect case where
511
+ * Node's default redirect follower drops the Authorization header on any
512
+ * hostname change and the resulting request comes back as a bogus 401 —
513
+ * which used to wipe the user's credentials.
514
+ *
515
+ * Cross-site redirects (different registrable domain) are followed without
516
+ * the Authorization header, matching browser/fetch security semantics.
517
+ */
518
+ async fetchWithSafeRedirects(initialUrl, init2) {
519
+ let currentUrl = initialUrl;
520
+ let currentInit = { ...init2, redirect: "manual" };
521
+ for (let hop = 0; hop < MAX_MANUAL_REDIRECTS; hop++) {
522
+ const response = await fetch(currentUrl, currentInit);
523
+ if (response.status < 300 || response.status >= 400) {
524
+ return response;
525
+ }
526
+ const location = response.headers.get("location");
527
+ if (!location) return response;
528
+ const nextUrl = new URL(location, currentUrl);
529
+ const prevHost = new URL(currentUrl).hostname;
530
+ const sameSite = registrableDomain(nextUrl.hostname) === registrableDomain(prevHost);
531
+ const headers = new Headers(currentInit.headers);
532
+ if (!sameSite) {
533
+ headers.delete("Authorization");
534
+ }
535
+ let nextMethod = (currentInit.method || "GET").toUpperCase();
536
+ let nextBody = currentInit.body;
537
+ if (response.status === 301 || response.status === 302 || response.status === 303) {
538
+ if (nextMethod !== "GET" && nextMethod !== "HEAD") {
539
+ nextMethod = "GET";
540
+ nextBody = void 0;
541
+ headers.delete("Content-Type");
542
+ }
543
+ }
544
+ currentUrl = nextUrl.toString();
545
+ currentInit = {
546
+ ...currentInit,
547
+ method: nextMethod,
548
+ headers,
549
+ body: nextBody,
550
+ redirect: "manual"
551
+ };
552
+ }
553
+ throw new APIError(
554
+ `Too many redirects while calling ${initialUrl}`,
555
+ 0,
556
+ "TOO_MANY_REDIRECTS"
557
+ );
478
558
  }
479
559
  /**
480
560
  * Make a GET request
@@ -486,10 +566,9 @@ var APIClient = class {
486
566
  url.searchParams.set(key, value);
487
567
  }
488
568
  }
489
- const response = await fetch(url.toString(), {
569
+ const response = await this.fetchWithSafeRedirects(url.toString(), {
490
570
  method: "GET",
491
- headers: this.getHeaders(),
492
- redirect: "follow"
571
+ headers: this.getHeaders()
493
572
  });
494
573
  return this.handleResponse(response);
495
574
  }
@@ -498,11 +577,10 @@ var APIClient = class {
498
577
  */
499
578
  async post(path, body) {
500
579
  const url = new URL(path, this.baseUrl);
501
- const response = await fetch(url.toString(), {
580
+ const response = await this.fetchWithSafeRedirects(url.toString(), {
502
581
  method: "POST",
503
582
  headers: this.getHeaders(),
504
- body: body ? JSON.stringify(body) : void 0,
505
- redirect: "follow"
583
+ body: body ? JSON.stringify(body) : void 0
506
584
  });
507
585
  return this.handleResponse(response);
508
586
  }
@@ -511,11 +589,10 @@ var APIClient = class {
511
589
  */
512
590
  async put(path, body) {
513
591
  const url = new URL(path, this.baseUrl);
514
- const response = await fetch(url.toString(), {
592
+ const response = await this.fetchWithSafeRedirects(url.toString(), {
515
593
  method: "PUT",
516
594
  headers: this.getHeaders(),
517
- body: body ? JSON.stringify(body) : void 0,
518
- redirect: "follow"
595
+ body: body ? JSON.stringify(body) : void 0
519
596
  });
520
597
  return this.handleResponse(response);
521
598
  }
@@ -524,11 +601,10 @@ var APIClient = class {
524
601
  */
525
602
  async patch(path, body) {
526
603
  const url = new URL(path, this.baseUrl);
527
- const response = await fetch(url.toString(), {
604
+ const response = await this.fetchWithSafeRedirects(url.toString(), {
528
605
  method: "PATCH",
529
606
  headers: this.getHeaders(),
530
- body: body ? JSON.stringify(body) : void 0,
531
- redirect: "follow"
607
+ body: body ? JSON.stringify(body) : void 0
532
608
  });
533
609
  return this.handleResponse(response);
534
610
  }
@@ -537,10 +613,9 @@ var APIClient = class {
537
613
  */
538
614
  async delete(path) {
539
615
  const url = new URL(path, this.baseUrl);
540
- const response = await fetch(url.toString(), {
616
+ const response = await this.fetchWithSafeRedirects(url.toString(), {
541
617
  method: "DELETE",
542
- headers: this.getHeaders(),
543
- redirect: "follow"
618
+ headers: this.getHeaders()
544
619
  });
545
620
  if (!response.ok) {
546
621
  await this.handleError(response);
@@ -557,7 +632,6 @@ var APIClient = class {
557
632
  if (!contentType.includes("application/json")) {
558
633
  const body2 = await response.text();
559
634
  if (this.isAuthRedirect(response, body2)) {
560
- clearAuth();
561
635
  throw new APIError(
562
636
  "Your CLI session is not authorized for this endpoint. Please run `envpilot login` and try again.",
563
637
  401,
@@ -594,20 +668,12 @@ var APIClient = class {
594
668
  code = data.code;
595
669
  } catch {
596
670
  }
597
- if (response.status >= 300 && response.status < 400 && this.isAuthRedirect(response)) {
598
- clearAuth();
599
- throw new APIError(
600
- "Your CLI session expired or the server redirected this request to browser sign-in. Please run `envpilot login`.",
601
- 401,
602
- "AUTH_REDIRECT"
603
- );
604
- }
605
671
  if (response.status === 401) {
606
672
  clearAuth();
607
673
  throw new APIError(
608
- "Authentication required. Please run `envpilot login`.",
674
+ "Authentication failed. Please run `envpilot login`.",
609
675
  401,
610
- "UNAUTHORIZED"
676
+ code || "UNAUTHORIZED"
611
677
  );
612
678
  }
613
679
  if (response.status === 403 && code === "TIER_LIMIT_REACHED") {
@@ -636,7 +702,7 @@ var APIClient = class {
636
702
  * Get current user info
637
703
  */
638
704
  async getCurrentUser() {
639
- return this.get("/api/cli/auth/me");
705
+ return this.get("/api/cli/auth", { action: "me" });
640
706
  }
641
707
  /**
642
708
  * Get tier info for the active organization
@@ -752,25 +818,25 @@ var APIClient = class {
752
818
  * Initiate CLI authentication flow
753
819
  */
754
820
  async initiateAuth(deviceName) {
755
- return this.post("/api/cli/auth/initiate", { deviceName });
821
+ return this.post("/api/cli/auth?action=initiate", { deviceName });
756
822
  }
757
823
  /**
758
824
  * Poll for authentication status
759
825
  */
760
826
  async pollAuth(code) {
761
- return this.get("/api/cli/auth/poll", { code });
827
+ return this.get("/api/cli/auth", { action: "poll", code });
762
828
  }
763
829
  /**
764
830
  * Refresh access token
765
831
  */
766
832
  async refreshToken(refreshToken) {
767
- return this.post("/api/cli/auth/refresh", { refreshToken });
833
+ return this.post("/api/cli/auth?action=refresh", { refreshToken });
768
834
  }
769
835
  /**
770
836
  * Revoke access token (logout)
771
837
  */
772
838
  async revokeToken() {
773
- return this.post("/api/cli/auth/revoke", {});
839
+ return this.post("/api/cli/auth?action=revoke", {});
774
840
  }
775
841
  };
776
842
  function createAPIClient() {
@@ -792,7 +858,7 @@ async function performLogin(options) {
792
858
  spinner.start();
793
859
  let initResponse;
794
860
  try {
795
- initResponse = await api.post("/api/cli/auth?action=initiate", { deviceName });
861
+ initResponse = await api.initiateAuth(deviceName);
796
862
  } catch (error2) {
797
863
  spinner.stop();
798
864
  throw error2;
@@ -818,10 +884,7 @@ async function performLogin(options) {
818
884
  await sleep(POLL_INTERVAL_MS);
819
885
  let pollResponse;
820
886
  try {
821
- pollResponse = await api.get("/api/cli/auth", {
822
- action: "poll",
823
- code: initResponse.code
824
- });
887
+ pollResponse = await api.pollAuth(initResponse.code);
825
888
  consecutiveErrors = 0;
826
889
  } catch {
827
890
  consecutiveErrors++;
@@ -834,12 +897,13 @@ async function performLogin(options) {
834
897
  }
835
898
  if (pollResponse.status === "authenticated") {
836
899
  pollSpinner.stop();
837
- if (pollResponse.accessToken) {
838
- setAccessToken(pollResponse.accessToken);
839
- }
840
- if (pollResponse.refreshToken) {
841
- setRefreshToken(pollResponse.refreshToken);
900
+ if (!pollResponse.accessToken || !pollResponse.refreshToken) {
901
+ throw new Error(
902
+ "Authentication succeeded but the server did not return session tokens. This is likely a server-side issue. Please try again or contact support."
903
+ );
842
904
  }
905
+ setAccessToken(pollResponse.accessToken);
906
+ setRefreshToken(pollResponse.refreshToken);
843
907
  if (pollResponse.user) {
844
908
  setUser({
845
909
  id: pollResponse.user.id,
@@ -862,7 +926,7 @@ async function performLogin(options) {
862
926
  }
863
927
 
864
928
  // src/commands/login.ts
865
- var loginCommand = new Command("login").description("Authenticate with Envpilot").option("--api-url <url>", "API URL (default: http://localhost:3000)").option("--no-browser", "Do not automatically open the browser").action(async (options) => {
929
+ var loginCommand = new Command("login").description("Authenticate with Envpilot").option("--api-url <url>", "API URL (default: https://www.envpilot.dev)").option("--no-browser", "Do not automatically open the browser").action(async (options) => {
866
930
  try {
867
931
  if (options.apiUrl) {
868
932
  setApiUrl(options.apiUrl);
@@ -3105,16 +3169,18 @@ async function handleSet(key, value) {
3105
3169
  process.exit(1);
3106
3170
  }
3107
3171
  switch (key) {
3108
- case "apiUrl":
3172
+ case "apiUrl": {
3173
+ const normalized = normalizeApiUrl(value);
3109
3174
  try {
3110
- new URL(value);
3175
+ new URL(normalized);
3111
3176
  } catch {
3112
3177
  error("Invalid URL format");
3113
3178
  process.exit(1);
3114
3179
  }
3115
3180
  setApiUrl(value);
3116
- success(`Set apiUrl to ${value}`);
3181
+ success(`Set apiUrl to ${normalized}`);
3117
3182
  break;
3183
+ }
3118
3184
  default:
3119
3185
  error(`Cannot set key: ${key}`);
3120
3186
  console.log();
package/dist/index.js CHANGED
@@ -4,18 +4,18 @@ import {
4
4
  getTopLevelCommandCatalog,
5
5
  initSentry,
6
6
  openTUI
7
- } from "./chunk-RUYGAIH5.js";
7
+ } from "./chunk-TPYA3DO2.js";
8
8
 
9
9
  // src/lib/program.ts
10
10
  import { Command } from "commander";
11
11
 
12
12
  // src/lib/cli-version.ts
13
- var CLI_VERSION = true ? "1.6.0" : "0.0.0";
13
+ var CLI_VERSION = true ? "1.6.1" : "0.0.0";
14
14
 
15
15
  // src/lib/version-check.ts
16
16
  import chalk from "chalk";
17
17
  import Conf from "conf";
18
- var CLI_VERSION2 = true ? "1.6.0" : "0.0.0";
18
+ var CLI_VERSION2 = true ? "1.6.1" : "0.0.0";
19
19
  var CHECK_INTERVAL = 60 * 60 * 1e3;
20
20
  var _cache = null;
21
21
  function getCache() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envpilot/cli",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Envpilot CLI — sync and manage environment variables from the terminal",
5
5
  "type": "module",
6
6
  "bin": {