@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.
|
@@ -7,7 +7,7 @@ function initSentry() {
|
|
|
7
7
|
Sentry.init({
|
|
8
8
|
dsn,
|
|
9
9
|
environment: "cli",
|
|
10
|
-
release: true ? "1.6.
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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:
|
|
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(
|
|
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 ${
|
|
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-
|
|
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.
|
|
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.
|
|
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() {
|