@envpilot/cli 1.11.0 → 1.12.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.
|
|
10
|
+
release: true ? "1.12.1" : "0.0.0",
|
|
11
11
|
// All EnvPilot surfaces report to one Sentry project; the surface tag
|
|
12
12
|
// is how dashboards tell web / cli / extension events apart.
|
|
13
13
|
initialScope: { tags: { surface: "cli" } },
|
|
@@ -390,15 +390,6 @@ function setApiUrl(url) {
|
|
|
390
390
|
function getAccessToken() {
|
|
391
391
|
return getActiveAccount()?.accessToken;
|
|
392
392
|
}
|
|
393
|
-
function setAccessToken(token) {
|
|
394
|
-
updateActiveAccount({ accessToken: token });
|
|
395
|
-
}
|
|
396
|
-
function getRefreshToken() {
|
|
397
|
-
return getActiveAccount()?.refreshToken;
|
|
398
|
-
}
|
|
399
|
-
function setRefreshToken(token) {
|
|
400
|
-
updateActiveAccount({ refreshToken: token });
|
|
401
|
-
}
|
|
402
393
|
function getActiveProjectId() {
|
|
403
394
|
return getActiveAccount()?.activeProjectId;
|
|
404
395
|
}
|
|
@@ -472,7 +463,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
472
463
|
async function openTUI() {
|
|
473
464
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
474
465
|
import("ink"),
|
|
475
|
-
import("./app-
|
|
466
|
+
import("./app-KEDPMGXA.js"),
|
|
476
467
|
import("./press-any-key-64XFP4O2.js")
|
|
477
468
|
]);
|
|
478
469
|
while (true) {
|
|
@@ -743,13 +734,221 @@ import open from "open";
|
|
|
743
734
|
import chalk3 from "chalk";
|
|
744
735
|
import { hostname } from "os";
|
|
745
736
|
|
|
746
|
-
// src/lib/
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
737
|
+
// src/lib/workos.ts
|
|
738
|
+
import { z } from "zod";
|
|
739
|
+
|
|
740
|
+
// src/lib/env.ts
|
|
741
|
+
var WORKOS_CLIENT_ID = "" ? "" : process.env.WORKOS_CLIENT_ID ?? "";
|
|
742
|
+
var CONVEX_URL = "" ? "" : process.env.NEXT_PUBLIC_CONVEX_URL ?? "";
|
|
743
|
+
|
|
744
|
+
// src/lib/workos.ts
|
|
745
|
+
var WORKOS_BASE = "https://api.workos.com";
|
|
746
|
+
var DEVICE_AUTHORIZE_URL = `${WORKOS_BASE}/user_management/authorize/device`;
|
|
747
|
+
var AUTHENTICATE_URL = `${WORKOS_BASE}/user_management/authenticate`;
|
|
748
|
+
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
749
|
+
var deviceCodeSchema = z.object({
|
|
750
|
+
device_code: z.string(),
|
|
751
|
+
user_code: z.string(),
|
|
752
|
+
verification_uri: z.string(),
|
|
753
|
+
verification_uri_complete: z.string(),
|
|
754
|
+
expires_in: z.number().default(300),
|
|
755
|
+
interval: z.number().default(5)
|
|
756
|
+
});
|
|
757
|
+
var workosUserSchema = z.object({
|
|
758
|
+
id: z.string(),
|
|
759
|
+
email: z.string(),
|
|
760
|
+
first_name: z.string().nullish(),
|
|
761
|
+
last_name: z.string().nullish()
|
|
762
|
+
});
|
|
763
|
+
var tokenSchema = z.object({
|
|
764
|
+
access_token: z.string(),
|
|
765
|
+
refresh_token: z.string(),
|
|
766
|
+
user: workosUserSchema.optional(),
|
|
767
|
+
organization_id: z.string().nullish()
|
|
768
|
+
});
|
|
769
|
+
var refreshSchema = z.object({
|
|
770
|
+
access_token: z.string(),
|
|
771
|
+
// WorkOS MAY rotate the refresh token — persist whichever it returns.
|
|
772
|
+
refresh_token: z.string()
|
|
773
|
+
});
|
|
774
|
+
var WorkosAuthError = class extends Error {
|
|
775
|
+
constructor(message, code) {
|
|
776
|
+
super(message);
|
|
777
|
+
this.code = code;
|
|
778
|
+
this.name = "WorkosAuthError";
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
function assertConfigured() {
|
|
782
|
+
if (!WORKOS_CLIENT_ID) {
|
|
783
|
+
throw new WorkosAuthError(
|
|
784
|
+
"This CLI build has no WorkOS client id embedded. Rebuild with WORKOS_CLIENT_ID set.",
|
|
785
|
+
"not_configured"
|
|
786
|
+
);
|
|
787
|
+
}
|
|
751
788
|
}
|
|
752
|
-
|
|
789
|
+
async function postForm(url, form) {
|
|
790
|
+
const res = await fetch(url, {
|
|
791
|
+
method: "POST",
|
|
792
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
793
|
+
body: new URLSearchParams(form).toString()
|
|
794
|
+
});
|
|
795
|
+
let body = null;
|
|
796
|
+
try {
|
|
797
|
+
body = await res.json();
|
|
798
|
+
} catch {
|
|
799
|
+
body = null;
|
|
800
|
+
}
|
|
801
|
+
return { status: res.status, body };
|
|
802
|
+
}
|
|
803
|
+
async function requestDeviceCode() {
|
|
804
|
+
assertConfigured();
|
|
805
|
+
let result;
|
|
806
|
+
try {
|
|
807
|
+
result = await postForm(DEVICE_AUTHORIZE_URL, {
|
|
808
|
+
client_id: WORKOS_CLIENT_ID
|
|
809
|
+
});
|
|
810
|
+
} catch (err) {
|
|
811
|
+
throw new WorkosAuthError(
|
|
812
|
+
`Could not reach WorkOS to start authentication: ${err.message}`,
|
|
813
|
+
"network"
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (result.status >= 400) {
|
|
817
|
+
const message = extractErrorMessage(result.body);
|
|
818
|
+
throw new WorkosAuthError(
|
|
819
|
+
`WorkOS rejected the device-code request${message ? `: ${message}` : ""}.`,
|
|
820
|
+
"invalid_response"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const parsed = deviceCodeSchema.safeParse(result.body);
|
|
824
|
+
if (!parsed.success) {
|
|
825
|
+
throw new WorkosAuthError(
|
|
826
|
+
"WorkOS returned an unexpected device-code response.",
|
|
827
|
+
"invalid_response"
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
return parsed.data;
|
|
831
|
+
}
|
|
832
|
+
async function pollForToken(deviceCode) {
|
|
833
|
+
assertConfigured();
|
|
834
|
+
let result;
|
|
835
|
+
try {
|
|
836
|
+
result = await postForm(AUTHENTICATE_URL, {
|
|
837
|
+
client_id: WORKOS_CLIENT_ID,
|
|
838
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
839
|
+
device_code: deviceCode
|
|
840
|
+
});
|
|
841
|
+
} catch {
|
|
842
|
+
return { status: "network" };
|
|
843
|
+
}
|
|
844
|
+
if (result.status === 200) {
|
|
845
|
+
const parsed = tokenSchema.safeParse(result.body);
|
|
846
|
+
if (!parsed.success) {
|
|
847
|
+
throw new WorkosAuthError(
|
|
848
|
+
"WorkOS returned an unexpected token response.",
|
|
849
|
+
"invalid_response"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return { status: "complete", token: parsed.data };
|
|
853
|
+
}
|
|
854
|
+
const errorCode = extractOauthError(result.body);
|
|
855
|
+
switch (errorCode) {
|
|
856
|
+
case "authorization_pending":
|
|
857
|
+
return { status: "pending" };
|
|
858
|
+
case "slow_down":
|
|
859
|
+
return { status: "slow_down" };
|
|
860
|
+
case "access_denied":
|
|
861
|
+
return { status: "denied" };
|
|
862
|
+
case "expired_token":
|
|
863
|
+
return { status: "expired" };
|
|
864
|
+
default:
|
|
865
|
+
return { status: "network" };
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async function refreshAccessToken(refreshToken) {
|
|
869
|
+
assertConfigured();
|
|
870
|
+
let result;
|
|
871
|
+
try {
|
|
872
|
+
result = await postForm(AUTHENTICATE_URL, {
|
|
873
|
+
client_id: WORKOS_CLIENT_ID,
|
|
874
|
+
grant_type: "refresh_token",
|
|
875
|
+
refresh_token: refreshToken
|
|
876
|
+
});
|
|
877
|
+
} catch (err) {
|
|
878
|
+
throw new WorkosAuthError(
|
|
879
|
+
`Could not reach WorkOS to refresh the session: ${err.message}`,
|
|
880
|
+
"network"
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (result.status >= 400) {
|
|
884
|
+
const message = extractOauthError(result.body) ?? extractErrorMessage(result.body);
|
|
885
|
+
const transient = result.status >= 500 || result.status === 429;
|
|
886
|
+
throw new WorkosAuthError(
|
|
887
|
+
`Session refresh failed${message ? `: ${message}` : ""}.`,
|
|
888
|
+
transient ? "network" : "access_denied"
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const parsed = refreshSchema.safeParse(result.body);
|
|
892
|
+
if (!parsed.success) {
|
|
893
|
+
throw new WorkosAuthError(
|
|
894
|
+
"WorkOS returned an unexpected refresh response.",
|
|
895
|
+
"invalid_response"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return parsed.data;
|
|
899
|
+
}
|
|
900
|
+
function extractOauthError(body) {
|
|
901
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
902
|
+
const e = body.error;
|
|
903
|
+
return typeof e === "string" ? e : null;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
function extractErrorMessage(body) {
|
|
908
|
+
if (body && typeof body === "object") {
|
|
909
|
+
const obj = body;
|
|
910
|
+
const msg = obj.error_description ?? obj.message ?? obj.error;
|
|
911
|
+
return typeof msg === "string" ? msg : null;
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/lib/jwt.ts
|
|
917
|
+
function decodeJwtPayload(token) {
|
|
918
|
+
try {
|
|
919
|
+
const parts = token.split(".");
|
|
920
|
+
if (parts.length < 2) return null;
|
|
921
|
+
const payload = parts[1];
|
|
922
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
923
|
+
const json = Buffer.from(base64, "base64").toString("utf-8");
|
|
924
|
+
const parsed = JSON.parse(json);
|
|
925
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
926
|
+
} catch {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function getJwtExp(token) {
|
|
931
|
+
const payload = decodeJwtPayload(token);
|
|
932
|
+
const exp = payload?.exp;
|
|
933
|
+
return typeof exp === "number" ? exp : null;
|
|
934
|
+
}
|
|
935
|
+
function getJwtSessionId(token) {
|
|
936
|
+
const payload = decodeJwtPayload(token);
|
|
937
|
+
const sid = payload?.sid;
|
|
938
|
+
return typeof sid === "string" ? sid : null;
|
|
939
|
+
}
|
|
940
|
+
function isTokenExpiring(token, skewSeconds = 60) {
|
|
941
|
+
const exp = getJwtExp(token);
|
|
942
|
+
if (exp === null) return true;
|
|
943
|
+
const nowSeconds = Date.now() / 1e3;
|
|
944
|
+
return exp - nowSeconds <= skewSeconds;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/lib/api.ts
|
|
948
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
949
|
+
import {
|
|
950
|
+
makeFunctionReference
|
|
951
|
+
} from "convex/server";
|
|
753
952
|
var APIError = class extends Error {
|
|
754
953
|
constructor(message, statusCode, code) {
|
|
755
954
|
super(message);
|
|
@@ -758,48 +957,139 @@ var APIError = class extends Error {
|
|
|
758
957
|
this.name = "APIError";
|
|
759
958
|
}
|
|
760
959
|
};
|
|
960
|
+
var refreshInFlight = null;
|
|
961
|
+
async function ensureFreshAccessToken() {
|
|
962
|
+
const account = getActiveAccount();
|
|
963
|
+
if (!account?.accessToken) {
|
|
964
|
+
throw new APIError(
|
|
965
|
+
"You are not logged in. Run `envpilot login`.",
|
|
966
|
+
401,
|
|
967
|
+
"NOT_AUTHENTICATED"
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (!isTokenExpiring(account.accessToken)) {
|
|
971
|
+
return account.accessToken;
|
|
972
|
+
}
|
|
973
|
+
if (refreshInFlight) return refreshInFlight;
|
|
974
|
+
const refreshToken = account.refreshToken;
|
|
975
|
+
if (!refreshToken) {
|
|
976
|
+
throw new APIError(
|
|
977
|
+
"Your session has expired. Run `envpilot login`.",
|
|
978
|
+
401,
|
|
979
|
+
"SESSION_EXPIRED"
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
refreshInFlight = (async () => {
|
|
983
|
+
try {
|
|
984
|
+
const result = await refreshAccessToken(refreshToken);
|
|
985
|
+
updateActiveAccount({
|
|
986
|
+
accessToken: result.access_token,
|
|
987
|
+
refreshToken: result.refresh_token
|
|
988
|
+
});
|
|
989
|
+
return result.access_token;
|
|
990
|
+
} catch (err) {
|
|
991
|
+
if (err instanceof WorkosAuthError && err.code === "access_denied") {
|
|
992
|
+
clearAuth();
|
|
993
|
+
throw new APIError(
|
|
994
|
+
"Your session has expired. Run `envpilot login`.",
|
|
995
|
+
401,
|
|
996
|
+
"SESSION_EXPIRED"
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
throw err;
|
|
1000
|
+
} finally {
|
|
1001
|
+
refreshInFlight = null;
|
|
1002
|
+
}
|
|
1003
|
+
})();
|
|
1004
|
+
return refreshInFlight;
|
|
1005
|
+
}
|
|
1006
|
+
async function getConvexClient() {
|
|
1007
|
+
if (!CONVEX_URL) {
|
|
1008
|
+
throw new APIError(
|
|
1009
|
+
"This CLI build has no Convex URL embedded. Rebuild with NEXT_PUBLIC_CONVEX_URL set.",
|
|
1010
|
+
0,
|
|
1011
|
+
"NOT_CONFIGURED"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const token = await ensureFreshAccessToken();
|
|
1015
|
+
const client = new ConvexHttpClient(CONVEX_URL);
|
|
1016
|
+
client.setAuth(token);
|
|
1017
|
+
return client;
|
|
1018
|
+
}
|
|
1019
|
+
var fnRef = makeFunctionReference;
|
|
1020
|
+
var refs = {
|
|
1021
|
+
listOrganizations: fnRef(
|
|
1022
|
+
"organizations:listForUser"
|
|
1023
|
+
),
|
|
1024
|
+
getMembership: fnRef(
|
|
1025
|
+
"organizations:getMembership"
|
|
1026
|
+
),
|
|
1027
|
+
listProjectsWithStats: fnRef("projects:listWithStats"),
|
|
1028
|
+
getProject: fnRef(
|
|
1029
|
+
"projects:getById"
|
|
1030
|
+
),
|
|
1031
|
+
getProjectMembership: fnRef("projectMembers:getProjectMembership"),
|
|
1032
|
+
listVariablesWithAccess: fnRef("variables:listWithAccess"),
|
|
1033
|
+
listVariableRequests: fnRef("variableRequests:listForProject"),
|
|
1034
|
+
getResolvedFeatures: fnRef("featureRegistry:getResolvedFeatures"),
|
|
1035
|
+
getResolvedFeaturesBatch: fnRef("featureRegistry:getResolvedFeaturesBatch"),
|
|
1036
|
+
getOrganizationUsage: fnRef("tierLimits:getOrganizationUsage"),
|
|
1037
|
+
isEnforcementEnabled: fnRef(
|
|
1038
|
+
"tierLimits:isEnforcementEnabled"
|
|
1039
|
+
),
|
|
1040
|
+
deviceSessionRecord: fnRef("deviceSessions:record"),
|
|
1041
|
+
deviceSessionRevoke: fnRef(
|
|
1042
|
+
"deviceSessions:revoke"
|
|
1043
|
+
)
|
|
1044
|
+
};
|
|
1045
|
+
async function convexQuery(ref, ...args) {
|
|
1046
|
+
const client = await getConvexClient();
|
|
1047
|
+
return client.query(ref, ...args);
|
|
1048
|
+
}
|
|
1049
|
+
async function convexMutation(ref, ...args) {
|
|
1050
|
+
const client = await getConvexClient();
|
|
1051
|
+
return client.mutation(ref, ...args);
|
|
1052
|
+
}
|
|
1053
|
+
async function recordDeviceSession(deviceName, sessionId) {
|
|
1054
|
+
try {
|
|
1055
|
+
await convexMutation(refs.deviceSessionRecord, {
|
|
1056
|
+
deviceName,
|
|
1057
|
+
clientType: "cli",
|
|
1058
|
+
sessionId
|
|
1059
|
+
});
|
|
1060
|
+
} catch {
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async function revokeDeviceSession(sessionId) {
|
|
1064
|
+
try {
|
|
1065
|
+
await convexMutation(refs.deviceSessionRevoke, { sessionId });
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function numericFeature(resolved, key, fallback = null) {
|
|
1070
|
+
const value = resolved?.features?.[key]?.value;
|
|
1071
|
+
return typeof value === "number" ? value : fallback;
|
|
1072
|
+
}
|
|
1073
|
+
function booleanFeature(resolved, key, fallback = false) {
|
|
1074
|
+
const value = resolved?.features?.[key]?.value;
|
|
1075
|
+
return typeof value === "boolean" ? value : fallback;
|
|
1076
|
+
}
|
|
1077
|
+
function registrableDomain(hostname2) {
|
|
1078
|
+
const parts = hostname2.toLowerCase().split(".").filter(Boolean);
|
|
1079
|
+
if (parts.length <= 2) return parts.join(".");
|
|
1080
|
+
return parts.slice(-2).join(".");
|
|
1081
|
+
}
|
|
1082
|
+
var MAX_MANUAL_REDIRECTS = 5;
|
|
761
1083
|
var APIClient = class {
|
|
762
1084
|
baseUrl;
|
|
763
|
-
accessToken;
|
|
764
|
-
// Re-entrancy guard: true while a token refresh is in flight so a 401 from
|
|
765
|
-
// the refresh endpoint itself can never trigger another refresh (no loops).
|
|
766
|
-
refreshing = false;
|
|
767
1085
|
constructor(options) {
|
|
768
1086
|
this.baseUrl = options?.baseUrl ?? getApiUrl();
|
|
769
|
-
this.accessToken = options?.accessToken ?? getAccessToken();
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Get headers for API requests
|
|
773
|
-
*/
|
|
774
|
-
getHeaders() {
|
|
775
|
-
const headers = {
|
|
776
|
-
"Content-Type": "application/json"
|
|
777
|
-
};
|
|
778
|
-
if (this.accessToken) {
|
|
779
|
-
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
780
|
-
}
|
|
781
|
-
return headers;
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Detect auth middleware redirects that returned HTML instead of CLI JSON.
|
|
785
|
-
*/
|
|
786
|
-
isAuthRedirect(response, bodyText) {
|
|
787
|
-
const location = response.headers.get("location") || "";
|
|
788
|
-
const finalUrl = response.url || "";
|
|
789
|
-
const contentType = response.headers.get("content-type") || "";
|
|
790
|
-
const preview = (bodyText || "").slice(0, 512).toLowerCase();
|
|
791
|
-
return location.includes("authkit") || finalUrl.includes("authkit") || contentType.includes("text/html") && (preview.includes("authorization_session_id") || preview.includes("client_id=") || preview.includes("<!doctype html"));
|
|
792
1087
|
}
|
|
1088
|
+
// ── Vault HTTP path (decrypted secret values) ──────────────────────────
|
|
793
1089
|
/**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* Node's default redirect follower drops the Authorization header on any
|
|
798
|
-
* hostname change and the resulting request comes back as a bogus 401 —
|
|
799
|
-
* which used to wipe the user's credentials.
|
|
800
|
-
*
|
|
801
|
-
* Cross-site redirects (different registrable domain) are followed without
|
|
802
|
-
* the Authorization header, matching browser/fetch security semantics.
|
|
1090
|
+
* Follow 3xx redirects manually, re-attaching Authorization only within the
|
|
1091
|
+
* same registrable domain (defends against the apex→www redirect that would
|
|
1092
|
+
* otherwise drop the header and yield a bogus 401).
|
|
803
1093
|
*/
|
|
804
1094
|
async fetchWithSafeRedirects(initialUrl, init2) {
|
|
805
1095
|
let currentUrl = initialUrl;
|
|
@@ -815,9 +1105,7 @@ var APIClient = class {
|
|
|
815
1105
|
const prevHost = new URL(currentUrl).hostname;
|
|
816
1106
|
const sameSite = registrableDomain(nextUrl.hostname) === registrableDomain(prevHost);
|
|
817
1107
|
const headers = new Headers(currentInit.headers);
|
|
818
|
-
if (!sameSite)
|
|
819
|
-
headers.delete("Authorization");
|
|
820
|
-
}
|
|
1108
|
+
if (!sameSite) headers.delete("Authorization");
|
|
821
1109
|
let nextMethod = (currentInit.method || "GET").toUpperCase();
|
|
822
1110
|
let nextBody = currentInit.body;
|
|
823
1111
|
if (response.status === 301 || response.status === 302 || response.status === 303) {
|
|
@@ -842,166 +1130,54 @@ var APIClient = class {
|
|
|
842
1130
|
"TOO_MANY_REDIRECTS"
|
|
843
1131
|
);
|
|
844
1132
|
}
|
|
845
|
-
/**
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
async tryRefreshToken() {
|
|
853
|
-
if (this.refreshing) return false;
|
|
854
|
-
const refreshToken = getRefreshToken();
|
|
855
|
-
if (!refreshToken) return false;
|
|
856
|
-
this.refreshing = true;
|
|
857
|
-
try {
|
|
858
|
-
const result = await this.refreshToken(refreshToken);
|
|
859
|
-
setAccessToken(result.accessToken);
|
|
860
|
-
setRefreshToken(result.refreshToken);
|
|
861
|
-
this.accessToken = result.accessToken;
|
|
862
|
-
return true;
|
|
863
|
-
} catch {
|
|
864
|
-
return false;
|
|
865
|
-
} finally {
|
|
866
|
-
this.refreshing = false;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Run a request thunk with automatic one-shot token refresh on a genuine 401.
|
|
871
|
-
*
|
|
872
|
-
* - An AUTH_REDIRECT (HTML sign-in page) is NOT a rejected token — rethrow
|
|
873
|
-
* without clearing creds or refreshing.
|
|
874
|
-
* - A genuine 401 triggers a single refresh attempt. On success the request
|
|
875
|
-
* is retried once with the rotated token; on failure (or a second 401) the
|
|
876
|
-
* local credentials are cleared so the user is prompted to log in again.
|
|
877
|
-
*
|
|
878
|
-
* The thunk rebuilds its headers on each call, so the retry automatically
|
|
879
|
-
* picks up the refreshed access token.
|
|
880
|
-
*/
|
|
881
|
-
async withAuthRetry(exec) {
|
|
882
|
-
try {
|
|
883
|
-
return await exec();
|
|
884
|
-
} catch (err) {
|
|
885
|
-
if (!(err instanceof APIError) || err.statusCode !== 401 || err.code === "AUTH_REDIRECT" || this.refreshing) {
|
|
886
|
-
throw err;
|
|
887
|
-
}
|
|
888
|
-
const refreshed = await this.tryRefreshToken();
|
|
889
|
-
if (!refreshed) {
|
|
890
|
-
clearAuth();
|
|
891
|
-
throw err;
|
|
892
|
-
}
|
|
893
|
-
try {
|
|
894
|
-
return await exec();
|
|
895
|
-
} catch (retryErr) {
|
|
896
|
-
if (retryErr instanceof APIError && retryErr.statusCode === 401 && retryErr.code !== "AUTH_REDIRECT") {
|
|
897
|
-
clearAuth();
|
|
898
|
-
}
|
|
899
|
-
throw retryErr;
|
|
1133
|
+
/** Perform an authed vault request carrying a fresh WorkOS JWT bearer. */
|
|
1134
|
+
async vaultRequest(method, path, options) {
|
|
1135
|
+
const token = await ensureFreshAccessToken();
|
|
1136
|
+
const url = new URL(path, this.baseUrl);
|
|
1137
|
+
if (options?.params) {
|
|
1138
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
1139
|
+
url.searchParams.set(key, value);
|
|
900
1140
|
}
|
|
901
1141
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (params) {
|
|
910
|
-
for (const [key, value] of Object.entries(params)) {
|
|
911
|
-
url.searchParams.set(key, value);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
915
|
-
method: "GET",
|
|
916
|
-
headers: this.getHeaders()
|
|
917
|
-
});
|
|
918
|
-
return this.handleResponse(response);
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Make a POST request
|
|
923
|
-
*/
|
|
924
|
-
async post(path, body) {
|
|
925
|
-
return this.withAuthRetry(async () => {
|
|
926
|
-
const url = new URL(path, this.baseUrl);
|
|
927
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
928
|
-
method: "POST",
|
|
929
|
-
headers: this.getHeaders(),
|
|
930
|
-
body: body ? JSON.stringify(body) : void 0
|
|
931
|
-
});
|
|
932
|
-
return this.handleResponse(response);
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Make a PUT request
|
|
937
|
-
*/
|
|
938
|
-
async put(path, body) {
|
|
939
|
-
return this.withAuthRetry(async () => {
|
|
940
|
-
const url = new URL(path, this.baseUrl);
|
|
941
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
942
|
-
method: "PUT",
|
|
943
|
-
headers: this.getHeaders(),
|
|
944
|
-
body: body ? JSON.stringify(body) : void 0
|
|
945
|
-
});
|
|
946
|
-
return this.handleResponse(response);
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* Make a PATCH request
|
|
951
|
-
*/
|
|
952
|
-
async patch(path, body) {
|
|
953
|
-
return this.withAuthRetry(async () => {
|
|
954
|
-
const url = new URL(path, this.baseUrl);
|
|
955
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
956
|
-
method: "PATCH",
|
|
957
|
-
headers: this.getHeaders(),
|
|
958
|
-
body: body ? JSON.stringify(body) : void 0
|
|
959
|
-
});
|
|
960
|
-
return this.handleResponse(response);
|
|
1142
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
1143
|
+
method,
|
|
1144
|
+
headers: {
|
|
1145
|
+
"Content-Type": "application/json",
|
|
1146
|
+
Authorization: `Bearer ${token}`
|
|
1147
|
+
},
|
|
1148
|
+
body: options?.body ? JSON.stringify(options.body) : void 0
|
|
961
1149
|
});
|
|
1150
|
+
return this.handleResponse(response);
|
|
962
1151
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
970
|
-
method: "DELETE",
|
|
971
|
-
headers: this.getHeaders()
|
|
972
|
-
});
|
|
973
|
-
if (!response.ok) {
|
|
974
|
-
await this.handleError(response);
|
|
975
|
-
}
|
|
976
|
-
});
|
|
1152
|
+
isAuthRedirect(response, bodyText) {
|
|
1153
|
+
const location = response.headers.get("location") || "";
|
|
1154
|
+
const finalUrl = response.url || "";
|
|
1155
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1156
|
+
const preview = (bodyText || "").slice(0, 512).toLowerCase();
|
|
1157
|
+
return location.includes("authkit") || finalUrl.includes("authkit") || contentType.includes("text/html") && (preview.includes("authorization_session_id") || preview.includes("client_id=") || preview.includes("<!doctype html"));
|
|
977
1158
|
}
|
|
978
|
-
/**
|
|
979
|
-
* Handle API response
|
|
980
|
-
*/
|
|
981
1159
|
async handleResponse(response) {
|
|
982
1160
|
if (!response.ok) {
|
|
983
1161
|
await this.handleError(response);
|
|
984
1162
|
}
|
|
985
1163
|
const contentType = response.headers.get("content-type") || "";
|
|
1164
|
+
const body = await response.text();
|
|
986
1165
|
if (!contentType.includes("application/json")) {
|
|
987
|
-
|
|
988
|
-
if (this.isAuthRedirect(response, body2)) {
|
|
1166
|
+
if (this.isAuthRedirect(response, body)) {
|
|
989
1167
|
throw new APIError(
|
|
990
1168
|
"Your CLI session is not authorized for this endpoint. Please run `envpilot login` and try again.",
|
|
991
1169
|
401,
|
|
992
1170
|
"AUTH_REDIRECT"
|
|
993
1171
|
);
|
|
994
1172
|
}
|
|
995
|
-
const preview =
|
|
1173
|
+
const preview = body.replace(/\s+/g, " ").slice(0, 160);
|
|
996
1174
|
throw new APIError(
|
|
997
1175
|
`Expected JSON but got ${contentType || "unknown content type"} from ${response.url}. Response starts with: ${preview}`,
|
|
998
1176
|
response.status || 500
|
|
999
1177
|
);
|
|
1000
1178
|
}
|
|
1001
|
-
const body = await response.text();
|
|
1002
1179
|
try {
|
|
1003
|
-
|
|
1004
|
-
return data;
|
|
1180
|
+
return JSON.parse(body);
|
|
1005
1181
|
} catch {
|
|
1006
1182
|
const preview = body.replace(/\s+/g, " ").slice(0, 160);
|
|
1007
1183
|
throw new APIError(
|
|
@@ -1010,9 +1186,6 @@ var APIClient = class {
|
|
|
1010
1186
|
);
|
|
1011
1187
|
}
|
|
1012
1188
|
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Handle API errors
|
|
1015
|
-
*/
|
|
1016
1189
|
async handleError(response) {
|
|
1017
1190
|
const bodyText = await response.text();
|
|
1018
1191
|
let message = `Request failed with status ${response.status}`;
|
|
@@ -1057,192 +1230,230 @@ var APIClient = class {
|
|
|
1057
1230
|
throw new APIError(message, response.status, code);
|
|
1058
1231
|
}
|
|
1059
1232
|
// ============================================
|
|
1060
|
-
// High-level
|
|
1233
|
+
// High-level methods — DIRECT to Convex
|
|
1061
1234
|
// ============================================
|
|
1062
1235
|
/**
|
|
1063
|
-
*
|
|
1236
|
+
* Current authenticated user. Verifies the session is live server-side (an
|
|
1237
|
+
* authed Convex query) and returns the identity captured at login.
|
|
1064
1238
|
*/
|
|
1065
1239
|
async getCurrentUser() {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
*/
|
|
1077
|
-
async getUsage(organizationId) {
|
|
1078
|
-
return this.get("/api/cli/usage", { organizationId });
|
|
1240
|
+
const account = getActiveAccount();
|
|
1241
|
+
if (!account) {
|
|
1242
|
+
throw new APIError(
|
|
1243
|
+
"You are not logged in. Run `envpilot login`.",
|
|
1244
|
+
401,
|
|
1245
|
+
"NOT_AUTHENTICATED"
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
await convexQuery(refs.listOrganizations, {});
|
|
1249
|
+
return account.user;
|
|
1079
1250
|
}
|
|
1080
|
-
/**
|
|
1081
|
-
* List organizations the user has access to
|
|
1082
|
-
*/
|
|
1251
|
+
/** List organizations the user belongs to (with tier + role). */
|
|
1083
1252
|
async listOrganizations() {
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1253
|
+
const orgs = await convexQuery(refs.listOrganizations, {});
|
|
1254
|
+
const orgIds = orgs.map((o) => o._id);
|
|
1255
|
+
let tiers = [];
|
|
1256
|
+
if (orgIds.length > 0) {
|
|
1257
|
+
try {
|
|
1258
|
+
tiers = await convexQuery(refs.getResolvedFeaturesBatch, {
|
|
1259
|
+
organizationIds: orgIds
|
|
1260
|
+
});
|
|
1261
|
+
} catch {
|
|
1262
|
+
tiers = [];
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return orgs.map((org, index) => ({
|
|
1266
|
+
_id: org._id,
|
|
1267
|
+
name: org.name,
|
|
1268
|
+
slug: org.slug,
|
|
1269
|
+
tier: tiers[index]?.tierName ?? "free",
|
|
1270
|
+
role: org.role,
|
|
1271
|
+
unifiedRole: normalizeOrgRole(org.role),
|
|
1272
|
+
description: org.description,
|
|
1273
|
+
logoUrl: org.logoUrl
|
|
1274
|
+
}));
|
|
1088
1275
|
}
|
|
1089
|
-
/**
|
|
1090
|
-
* List projects in an organization
|
|
1091
|
-
*/
|
|
1276
|
+
/** List projects in an organization, with unified-role + assignment info. */
|
|
1092
1277
|
async listProjects(organizationId) {
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
{ organizationId }
|
|
1278
|
+
const [projects, membership] = await Promise.all([
|
|
1279
|
+
convexQuery(refs.listProjectsWithStats, { organizationId }),
|
|
1280
|
+
convexQuery(refs.getMembership, { organizationId })
|
|
1281
|
+
]);
|
|
1282
|
+
const unifiedRole = normalizeOrgRole(membership?.role);
|
|
1283
|
+
const isOwner = unifiedRole === "owner";
|
|
1284
|
+
const isDeveloper = unifiedRole === "developer";
|
|
1285
|
+
const legacyProjectRole = isOwner ? null : isDeveloper ? "developer" : "manager";
|
|
1286
|
+
return Promise.all(
|
|
1287
|
+
projects.map(async (project) => {
|
|
1288
|
+
let assigned = isOwner;
|
|
1289
|
+
let environmentScope = null;
|
|
1290
|
+
if (!isOwner) {
|
|
1291
|
+
const projectMembership = await convexQuery(
|
|
1292
|
+
refs.getProjectMembership,
|
|
1293
|
+
{ projectId: project._id }
|
|
1294
|
+
);
|
|
1295
|
+
assigned = projectMembership !== null;
|
|
1296
|
+
environmentScope = isDeveloper ? projectMembership?.environments ?? null : null;
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
_id: project._id,
|
|
1300
|
+
name: project.name,
|
|
1301
|
+
slug: project.slug,
|
|
1302
|
+
description: project.description,
|
|
1303
|
+
icon: project.icon,
|
|
1304
|
+
color: project.color,
|
|
1305
|
+
organizationId: project.organizationId,
|
|
1306
|
+
userRole: unifiedRole,
|
|
1307
|
+
projectRole: legacyProjectRole,
|
|
1308
|
+
unifiedRole,
|
|
1309
|
+
assigned,
|
|
1310
|
+
environmentScope
|
|
1311
|
+
};
|
|
1312
|
+
})
|
|
1096
1313
|
);
|
|
1097
|
-
return response.data || [];
|
|
1098
1314
|
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Get a project by ID
|
|
1101
|
-
*/
|
|
1315
|
+
/** Get a single project (metadata) by id. */
|
|
1102
1316
|
async getProject(projectId) {
|
|
1103
|
-
|
|
1317
|
+
const project = await convexQuery(refs.getProject, { projectId });
|
|
1318
|
+
if (!project) {
|
|
1319
|
+
throw new APIError("Project not found", 404, "PROJECT_NOT_FOUND");
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
_id: project._id,
|
|
1323
|
+
name: project.name,
|
|
1324
|
+
slug: project.slug,
|
|
1325
|
+
organizationId: project.organizationId,
|
|
1326
|
+
description: project.description,
|
|
1327
|
+
icon: project.icon,
|
|
1328
|
+
color: project.color
|
|
1329
|
+
};
|
|
1104
1330
|
}
|
|
1105
1331
|
/**
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
* `variables`. Callers should warn the user about `decryptionFailures`
|
|
1111
|
-
* so they know those secrets weren't injected.
|
|
1332
|
+
* Compute the variable fingerprint from Convex METADATA (no vault decryption).
|
|
1333
|
+
* Byte-compatible with the fingerprint writeCache stores from a full fetch:
|
|
1334
|
+
* both hash `${_id}:${version}:${updatedAt}` over the accessible variables in
|
|
1335
|
+
* the requested environment.
|
|
1112
1336
|
*/
|
|
1113
|
-
async
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1337
|
+
async checkFingerprint(projectId, environment, _organizationId) {
|
|
1338
|
+
const rows = await convexQuery(refs.listVariablesWithAccess, { projectId });
|
|
1339
|
+
const filtered = rows.filter(
|
|
1340
|
+
(row) => row.hasAccess && (!environment || row.environments.includes(environment))
|
|
1341
|
+
);
|
|
1342
|
+
const asVariables = filtered.map(
|
|
1343
|
+
(row) => ({
|
|
1344
|
+
_id: row._id,
|
|
1345
|
+
version: row.version,
|
|
1346
|
+
updatedAt: row.updatedAt
|
|
1347
|
+
})
|
|
1348
|
+
);
|
|
1349
|
+
return computeFingerprint(asVariables);
|
|
1350
|
+
}
|
|
1351
|
+
/** List variable requests for a project (Convex). */
|
|
1352
|
+
async listVariableRequests(projectId, status) {
|
|
1353
|
+
return convexQuery(refs.listVariableRequests, { projectId, status });
|
|
1354
|
+
}
|
|
1355
|
+
/** Tier + usage snapshot for an organization (Convex). */
|
|
1356
|
+
async getUsage(organizationId) {
|
|
1357
|
+
const membership = await convexQuery(refs.getMembership, {
|
|
1358
|
+
organizationId
|
|
1359
|
+
});
|
|
1360
|
+
if (!membership) {
|
|
1361
|
+
throw new APIError(
|
|
1362
|
+
"You are not a member of this organization",
|
|
1363
|
+
403,
|
|
1364
|
+
"FORBIDDEN"
|
|
1365
|
+
);
|
|
1117
1366
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1367
|
+
const [usageData, enforcementEnabled, resolved] = await Promise.all([
|
|
1368
|
+
convexQuery(refs.getOrganizationUsage, { organizationId }),
|
|
1369
|
+
convexQuery(refs.isEnforcementEnabled, {}),
|
|
1370
|
+
convexQuery(refs.getResolvedFeatures, { organizationId })
|
|
1371
|
+
]);
|
|
1372
|
+
if (!usageData) {
|
|
1373
|
+
throw new APIError(
|
|
1374
|
+
"Organization not found",
|
|
1375
|
+
404,
|
|
1376
|
+
"ORGANIZATION_NOT_FOUND"
|
|
1377
|
+
);
|
|
1120
1378
|
}
|
|
1121
|
-
const response = await this.get("/api/cli/variables", params);
|
|
1122
1379
|
return {
|
|
1123
|
-
|
|
1124
|
-
|
|
1380
|
+
tier: resolved?.tierName ?? usageData.tier,
|
|
1381
|
+
enforcementEnabled,
|
|
1382
|
+
limits: {
|
|
1383
|
+
projects: numericFeature(resolved, "max_projects"),
|
|
1384
|
+
variablesPerProject: numericFeature(
|
|
1385
|
+
resolved,
|
|
1386
|
+
"max_variables_per_project"
|
|
1387
|
+
),
|
|
1388
|
+
teamMembers: numericFeature(resolved, "max_team_members")
|
|
1389
|
+
},
|
|
1390
|
+
usage: usageData.usage,
|
|
1391
|
+
features: {
|
|
1392
|
+
versionHistory: booleanFeature(resolved, "variable_version_history"),
|
|
1393
|
+
bulkImport: booleanFeature(resolved, "bulk_import"),
|
|
1394
|
+
extensionAccess: booleanFeature(resolved, "extension_access", true),
|
|
1395
|
+
granularPermissions: booleanFeature(resolved, "granular_permissions"),
|
|
1396
|
+
auditLogRetentionDays: numericFeature(resolved, "audit_log_retention_days") ?? 7
|
|
1397
|
+
}
|
|
1125
1398
|
};
|
|
1126
1399
|
}
|
|
1400
|
+
// ============================================
|
|
1401
|
+
// High-level methods — VAULT (over HTTP)
|
|
1402
|
+
// ============================================
|
|
1127
1403
|
/**
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
* WITHOUT decrypting vault secrets. The CLI uses this to decide whether
|
|
1132
|
-
* a cached variable set is still current before doing a full (expensive)
|
|
1133
|
-
* fetch. If the fingerprint matches, the cache can be extended for free.
|
|
1404
|
+
* List variables in a project WITH decrypted values (vault path).
|
|
1405
|
+
* Returns the variable list, the response meta (unified role / scope info),
|
|
1406
|
+
* and any keys that failed vault decryption (skipped server-side).
|
|
1134
1407
|
*/
|
|
1135
|
-
async
|
|
1408
|
+
async listVariables(projectId, environment, organizationId) {
|
|
1136
1409
|
const params = { projectId };
|
|
1137
1410
|
if (environment) params.environment = environment;
|
|
1138
1411
|
if (organizationId) params.organizationId = organizationId;
|
|
1139
|
-
const response = await this.
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* Get a variable by ID (with decrypted value)
|
|
1147
|
-
*/
|
|
1148
|
-
async getVariable(variableId) {
|
|
1149
|
-
return this.get(`/api/cli/variables/${variableId}`);
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Create a new variable
|
|
1153
|
-
*/
|
|
1154
|
-
async createVariable(data) {
|
|
1155
|
-
return this.post("/api/cli/variables", data);
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Update a variable
|
|
1159
|
-
*/
|
|
1160
|
-
async updateVariable(variableId, data) {
|
|
1161
|
-
return this.patch(`/api/cli/variables/${variableId}`, data);
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Delete a variable
|
|
1165
|
-
*/
|
|
1166
|
-
async deleteVariable(variableId) {
|
|
1167
|
-
return this.delete(`/api/cli/variables/${variableId}`);
|
|
1412
|
+
const response = await this.vaultRequest("GET", "/api/cli/variables", { params });
|
|
1413
|
+
return {
|
|
1414
|
+
variables: response.data || [],
|
|
1415
|
+
meta: response.meta,
|
|
1416
|
+
decryptionFailures: response.meta?.decryptionFailures ?? []
|
|
1417
|
+
};
|
|
1168
1418
|
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Bulk create/update variables
|
|
1171
|
-
*/
|
|
1419
|
+
/** Bulk create/update variables (vault path — encrypts values server-side). */
|
|
1172
1420
|
async bulkUpsertVariables(data) {
|
|
1173
|
-
|
|
1421
|
+
const response = await this.vaultRequest("POST", "/api/cli/variables/bulk", { body: data });
|
|
1422
|
+
if (!response.data) {
|
|
1423
|
+
throw new APIError("No result returned by server", 500);
|
|
1424
|
+
}
|
|
1425
|
+
return response.data;
|
|
1174
1426
|
}
|
|
1175
1427
|
/**
|
|
1176
|
-
* Submit a variable request (developers only
|
|
1177
|
-
*
|
|
1428
|
+
* Submit a variable request (developers only). Goes over the vault path
|
|
1429
|
+
* because the requested value is encrypted into WorkOS Vault server-side;
|
|
1430
|
+
* owners/PMs/team leads get a 403 here and create variables directly.
|
|
1178
1431
|
*/
|
|
1179
1432
|
async createVariableRequest(data) {
|
|
1180
|
-
const response = await this.
|
|
1181
|
-
"/api/cli/variable-requests",
|
|
1182
|
-
data
|
|
1183
|
-
);
|
|
1433
|
+
const response = await this.vaultRequest("POST", "/api/cli/variable-requests", { body: data });
|
|
1184
1434
|
if (!response.data) {
|
|
1185
1435
|
throw new APIError("No variable request returned by server", 500);
|
|
1186
1436
|
}
|
|
1187
1437
|
return response.data.request;
|
|
1188
1438
|
}
|
|
1189
|
-
/**
|
|
1190
|
-
* List variable requests for a project, optionally filtered by status.
|
|
1191
|
-
*/
|
|
1192
|
-
async listVariableRequests(projectId, status) {
|
|
1193
|
-
const params = { projectId };
|
|
1194
|
-
if (status) params.status = status;
|
|
1195
|
-
const response = await this.get("/api/cli/variable-requests", params);
|
|
1196
|
-
return response.data?.requests ?? [];
|
|
1197
|
-
}
|
|
1198
|
-
// ============================================
|
|
1199
|
-
// Authentication methods
|
|
1200
|
-
// ============================================
|
|
1201
|
-
/**
|
|
1202
|
-
* Initiate CLI authentication flow
|
|
1203
|
-
*/
|
|
1204
|
-
async initiateAuth(deviceName) {
|
|
1205
|
-
return this.post("/api/cli/auth?action=initiate", { deviceName });
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Poll for authentication status
|
|
1209
|
-
*/
|
|
1210
|
-
async pollAuth(code) {
|
|
1211
|
-
return this.get("/api/cli/auth", { action: "poll", code });
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Refresh access token
|
|
1215
|
-
*/
|
|
1216
|
-
async refreshToken(refreshToken) {
|
|
1217
|
-
return this.post("/api/cli/auth?action=refresh", { refreshToken });
|
|
1218
|
-
}
|
|
1219
|
-
/**
|
|
1220
|
-
* Revoke access token (logout)
|
|
1221
|
-
*/
|
|
1222
|
-
async revokeToken() {
|
|
1223
|
-
return this.post("/api/cli/auth?action=revoke", {});
|
|
1224
|
-
}
|
|
1225
1439
|
};
|
|
1226
1440
|
function createAPIClient() {
|
|
1227
1441
|
return new APIClient();
|
|
1228
1442
|
}
|
|
1229
1443
|
|
|
1230
1444
|
// src/lib/auth-flow.ts
|
|
1231
|
-
var POLL_INTERVAL_MS = 2e3;
|
|
1232
|
-
var MAX_POLL_ATTEMPTS = 150;
|
|
1233
1445
|
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
1234
1446
|
function sleep(ms) {
|
|
1235
1447
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1236
1448
|
}
|
|
1237
1449
|
async function performLogin(options) {
|
|
1238
|
-
const api = createAPIClient();
|
|
1239
1450
|
const deviceName = `CLI - ${hostname()}`;
|
|
1240
1451
|
info("Starting authentication flow...");
|
|
1241
|
-
const spinner = createSpinner("
|
|
1452
|
+
const spinner = createSpinner("Requesting device code...");
|
|
1242
1453
|
spinner.start();
|
|
1243
|
-
let
|
|
1454
|
+
let device;
|
|
1244
1455
|
try {
|
|
1245
|
-
|
|
1456
|
+
device = await requestDeviceCode();
|
|
1246
1457
|
} catch (error2) {
|
|
1247
1458
|
spinner.stop();
|
|
1248
1459
|
throw error2;
|
|
@@ -1251,60 +1462,77 @@ async function performLogin(options) {
|
|
|
1251
1462
|
console.log();
|
|
1252
1463
|
console.log(chalk3.bold("Your authentication code:"));
|
|
1253
1464
|
console.log();
|
|
1254
|
-
console.log(chalk3.cyan.bold(` ${
|
|
1465
|
+
console.log(chalk3.cyan.bold(` ${device.user_code}`));
|
|
1255
1466
|
console.log();
|
|
1256
|
-
console.log(
|
|
1257
|
-
console.log(chalk3.dim(
|
|
1467
|
+
console.log("Open this URL to authenticate:");
|
|
1468
|
+
console.log(chalk3.dim(device.verification_uri_complete));
|
|
1258
1469
|
console.log();
|
|
1259
1470
|
if (options?.browser !== false) {
|
|
1260
1471
|
info("Opening browser...");
|
|
1261
|
-
|
|
1472
|
+
try {
|
|
1473
|
+
await open(device.verification_uri_complete);
|
|
1474
|
+
} catch {
|
|
1475
|
+
}
|
|
1262
1476
|
}
|
|
1263
1477
|
const pollSpinner = createSpinner("Waiting for authentication...");
|
|
1264
1478
|
pollSpinner.start();
|
|
1479
|
+
let intervalMs = device.interval * 1e3;
|
|
1480
|
+
const deadline = Date.now() + device.expires_in * 1e3;
|
|
1265
1481
|
let consecutiveErrors = 0;
|
|
1266
1482
|
try {
|
|
1267
|
-
|
|
1268
|
-
await sleep(
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
pollResponse = await api.pollAuth(initResponse.code);
|
|
1272
|
-
consecutiveErrors = 0;
|
|
1273
|
-
} catch {
|
|
1274
|
-
consecutiveErrors++;
|
|
1275
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1276
|
-
throw new Error(
|
|
1277
|
-
"Too many consecutive network errors while polling. Please check your connection and try again."
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
if (pollResponse.status === "authenticated") {
|
|
1483
|
+
while (Date.now() < deadline) {
|
|
1484
|
+
await sleep(intervalMs);
|
|
1485
|
+
const result = await pollForToken(device.device_code);
|
|
1486
|
+
if (result.status === "complete") {
|
|
1283
1487
|
pollSpinner.stop();
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const accountId = pollResponse.user?.id ?? `session-${pollResponse.accessToken.slice(0, 8)}`;
|
|
1488
|
+
const token = result.token;
|
|
1489
|
+
const email = token.user?.email ?? "";
|
|
1490
|
+
const userId = token.user?.id ?? `session-${token.access_token.slice(0, 8)}`;
|
|
1491
|
+
const name = [token.user?.first_name, token.user?.last_name].filter(Boolean).join(" ") || void 0;
|
|
1492
|
+
const sessionId = getJwtSessionId(token.access_token) ?? void 0;
|
|
1290
1493
|
const account = {
|
|
1291
|
-
id:
|
|
1292
|
-
user:
|
|
1293
|
-
id:
|
|
1294
|
-
email:
|
|
1295
|
-
name
|
|
1296
|
-
}
|
|
1297
|
-
accessToken:
|
|
1298
|
-
refreshToken:
|
|
1494
|
+
id: userId,
|
|
1495
|
+
user: {
|
|
1496
|
+
id: userId,
|
|
1497
|
+
email: email || `${userId}@cli.local`,
|
|
1498
|
+
name
|
|
1499
|
+
},
|
|
1500
|
+
accessToken: token.access_token,
|
|
1501
|
+
refreshToken: token.refresh_token,
|
|
1502
|
+
sessionId,
|
|
1503
|
+
deviceName
|
|
1299
1504
|
};
|
|
1300
1505
|
upsertAccount(account);
|
|
1301
1506
|
setActiveAccount(account.id);
|
|
1507
|
+
if (sessionId) {
|
|
1508
|
+
await recordDeviceSession(deviceName, sessionId);
|
|
1509
|
+
}
|
|
1302
1510
|
console.log();
|
|
1303
|
-
success(`Logged in as ${chalk3.bold(
|
|
1304
|
-
return { email
|
|
1511
|
+
success(`Logged in as ${chalk3.bold(email || userId)}`);
|
|
1512
|
+
return { email };
|
|
1305
1513
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1514
|
+
switch (result.status) {
|
|
1515
|
+
case "pending":
|
|
1516
|
+
consecutiveErrors = 0;
|
|
1517
|
+
break;
|
|
1518
|
+
case "slow_down":
|
|
1519
|
+
consecutiveErrors = 0;
|
|
1520
|
+
intervalMs += 5e3;
|
|
1521
|
+
break;
|
|
1522
|
+
case "network":
|
|
1523
|
+
consecutiveErrors++;
|
|
1524
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1525
|
+
throw new Error(
|
|
1526
|
+
"Too many consecutive network errors while polling. Please check your connection and try again."
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
break;
|
|
1530
|
+
case "denied":
|
|
1531
|
+
throw new Error("Authentication was denied. Please try again.");
|
|
1532
|
+
case "expired":
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
"Authentication code expired. Please run `envpilot login` again."
|
|
1535
|
+
);
|
|
1308
1536
|
}
|
|
1309
1537
|
}
|
|
1310
1538
|
throw new Error("Authentication timed out. Please try again.");
|
|
@@ -1360,152 +1588,161 @@ import { execSync } from "child_process";
|
|
|
1360
1588
|
import { join as join2 } from "path";
|
|
1361
1589
|
|
|
1362
1590
|
// src/types/index.ts
|
|
1363
|
-
import { z } from "zod";
|
|
1364
|
-
var userSchema =
|
|
1365
|
-
id:
|
|
1366
|
-
email:
|
|
1367
|
-
name:
|
|
1591
|
+
import { z as z2 } from "zod";
|
|
1592
|
+
var userSchema = z2.object({
|
|
1593
|
+
id: z2.string(),
|
|
1594
|
+
email: z2.string().email(),
|
|
1595
|
+
name: z2.string().optional()
|
|
1368
1596
|
});
|
|
1369
|
-
var accountSchema =
|
|
1370
|
-
id:
|
|
1597
|
+
var accountSchema = z2.object({
|
|
1598
|
+
id: z2.string(),
|
|
1371
1599
|
user: userSchema,
|
|
1372
|
-
accessToken
|
|
1373
|
-
|
|
1600
|
+
// WorkOS AuthKit tokens. accessToken is a short-lived (5 min) JWT refreshed
|
|
1601
|
+
// on demand via the WorkOS refresh token. The JWT itself is never stored
|
|
1602
|
+
// server-side — identity is proven per-request.
|
|
1603
|
+
accessToken: z2.string(),
|
|
1604
|
+
refreshToken: z2.string().optional(),
|
|
1605
|
+
// WorkOS session id (`sid` claim), decoded from the access token at login.
|
|
1606
|
+
// Recorded server-side (deviceSessions.record) so the session appears in the
|
|
1607
|
+
// active-sessions UI and can be remotely revoked.
|
|
1608
|
+
sessionId: z2.string().optional(),
|
|
1609
|
+
// Human-friendly device label shown in the sessions UI (e.g. "CLI - host").
|
|
1610
|
+
deviceName: z2.string().optional(),
|
|
1374
1611
|
// Stored role string (legacy or unified); normalized on read.
|
|
1375
|
-
role:
|
|
1612
|
+
role: z2.string().optional(),
|
|
1376
1613
|
// Per-account active org/project selection.
|
|
1377
|
-
activeOrganizationId:
|
|
1378
|
-
activeProjectId:
|
|
1614
|
+
activeOrganizationId: z2.string().optional(),
|
|
1615
|
+
activeProjectId: z2.string().optional()
|
|
1379
1616
|
});
|
|
1380
|
-
var organizationSchema =
|
|
1381
|
-
_id:
|
|
1382
|
-
name:
|
|
1383
|
-
slug:
|
|
1384
|
-
tier:
|
|
1385
|
-
role:
|
|
1617
|
+
var organizationSchema = z2.object({
|
|
1618
|
+
_id: z2.string(),
|
|
1619
|
+
name: z2.string(),
|
|
1620
|
+
slug: z2.string(),
|
|
1621
|
+
tier: z2.enum(["free", "pro"]),
|
|
1622
|
+
role: z2.string().optional(),
|
|
1386
1623
|
// Unified org role (additive; legacy responses omit it and fall back to role).
|
|
1387
|
-
unifiedRole:
|
|
1624
|
+
unifiedRole: z2.string().nullable().optional()
|
|
1388
1625
|
});
|
|
1389
|
-
var projectSchema =
|
|
1390
|
-
_id:
|
|
1391
|
-
name:
|
|
1392
|
-
slug:
|
|
1393
|
-
organizationId:
|
|
1394
|
-
description:
|
|
1395
|
-
icon:
|
|
1396
|
-
color:
|
|
1397
|
-
userRole:
|
|
1398
|
-
projectRole:
|
|
1626
|
+
var projectSchema = z2.object({
|
|
1627
|
+
_id: z2.string(),
|
|
1628
|
+
name: z2.string(),
|
|
1629
|
+
slug: z2.string(),
|
|
1630
|
+
organizationId: z2.string(),
|
|
1631
|
+
description: z2.string().optional(),
|
|
1632
|
+
icon: z2.string().optional(),
|
|
1633
|
+
color: z2.string().optional(),
|
|
1634
|
+
userRole: z2.string().nullable().optional(),
|
|
1635
|
+
projectRole: z2.string().nullable().optional(),
|
|
1399
1636
|
// Unified-role fields (optional so legacy server responses still parse)
|
|
1400
|
-
unifiedRole:
|
|
1401
|
-
assigned:
|
|
1402
|
-
environmentScope:
|
|
1637
|
+
unifiedRole: z2.string().nullable().optional(),
|
|
1638
|
+
assigned: z2.boolean().optional(),
|
|
1639
|
+
environmentScope: z2.array(z2.string()).nullable().optional()
|
|
1403
1640
|
});
|
|
1404
|
-
var variableTagSchema =
|
|
1405
|
-
_id:
|
|
1406
|
-
name:
|
|
1407
|
-
color:
|
|
1641
|
+
var variableTagSchema = z2.object({
|
|
1642
|
+
_id: z2.string(),
|
|
1643
|
+
name: z2.string(),
|
|
1644
|
+
color: z2.string()
|
|
1408
1645
|
});
|
|
1409
|
-
var variableSchema =
|
|
1410
|
-
_id:
|
|
1411
|
-
key:
|
|
1412
|
-
value:
|
|
1413
|
-
environment:
|
|
1414
|
-
projectId:
|
|
1415
|
-
description:
|
|
1416
|
-
isSensitive:
|
|
1417
|
-
version:
|
|
1418
|
-
updatedAt:
|
|
1419
|
-
createdAt:
|
|
1420
|
-
tags:
|
|
1646
|
+
var variableSchema = z2.object({
|
|
1647
|
+
_id: z2.string(),
|
|
1648
|
+
key: z2.string(),
|
|
1649
|
+
value: z2.string(),
|
|
1650
|
+
environment: z2.enum(["development", "staging", "production"]),
|
|
1651
|
+
projectId: z2.string(),
|
|
1652
|
+
description: z2.string().optional(),
|
|
1653
|
+
isSensitive: z2.boolean().optional(),
|
|
1654
|
+
version: z2.number().optional(),
|
|
1655
|
+
updatedAt: z2.number().optional(),
|
|
1656
|
+
createdAt: z2.number().optional(),
|
|
1657
|
+
tags: z2.array(variableTagSchema).optional(),
|
|
1421
1658
|
// Per-variable effective access under the unified model. Optional so older
|
|
1422
1659
|
// server responses (which omit it) still parse.
|
|
1423
|
-
access:
|
|
1660
|
+
access: z2.enum(["read", "write"]).optional()
|
|
1424
1661
|
});
|
|
1425
|
-
var variablesMetaSchema =
|
|
1426
|
-
total:
|
|
1427
|
-
page:
|
|
1428
|
-
limit:
|
|
1429
|
-
decryptionFailures:
|
|
1430
|
-
unifiedRole:
|
|
1431
|
-
assigned:
|
|
1432
|
-
grantOnly:
|
|
1433
|
-
environmentScope:
|
|
1434
|
-
hasWriteAccess:
|
|
1435
|
-
scopeRestricted:
|
|
1662
|
+
var variablesMetaSchema = z2.object({
|
|
1663
|
+
total: z2.number().optional(),
|
|
1664
|
+
page: z2.number().optional(),
|
|
1665
|
+
limit: z2.number().optional(),
|
|
1666
|
+
decryptionFailures: z2.array(z2.string()).optional(),
|
|
1667
|
+
unifiedRole: z2.string().nullable().optional(),
|
|
1668
|
+
assigned: z2.boolean().optional(),
|
|
1669
|
+
grantOnly: z2.boolean().optional(),
|
|
1670
|
+
environmentScope: z2.array(z2.string()).nullable().optional(),
|
|
1671
|
+
hasWriteAccess: z2.boolean().optional(),
|
|
1672
|
+
scopeRestricted: z2.boolean().optional()
|
|
1436
1673
|
}).passthrough();
|
|
1437
|
-
var environmentSchema =
|
|
1674
|
+
var environmentSchema = z2.enum([
|
|
1438
1675
|
"development",
|
|
1439
1676
|
"staging",
|
|
1440
1677
|
"production"
|
|
1441
1678
|
]);
|
|
1442
|
-
var cliConfigSchema =
|
|
1679
|
+
var cliConfigSchema = z2.object({
|
|
1443
1680
|
// Global CLI setting — shared across all accounts.
|
|
1444
|
-
apiUrl:
|
|
1681
|
+
apiUrl: z2.string().url(),
|
|
1445
1682
|
// Multi-account store. Keyed by account id (== user id).
|
|
1446
|
-
accounts:
|
|
1683
|
+
accounts: z2.record(z2.string(), accountSchema).optional(),
|
|
1447
1684
|
// The currently active account id.
|
|
1448
|
-
activeAccountId:
|
|
1685
|
+
activeAccountId: z2.string().optional(),
|
|
1449
1686
|
// --- Legacy single-account fields ---
|
|
1450
1687
|
// Retained ONLY so pre-multi-account configs still parse and can be migrated
|
|
1451
1688
|
// into `accounts` by migrateLegacyConfig(). After migration these are deleted
|
|
1452
1689
|
// so `accounts` is the single source of truth. Do not read/write directly.
|
|
1453
|
-
accessToken:
|
|
1454
|
-
refreshToken:
|
|
1455
|
-
activeProjectId:
|
|
1456
|
-
activeOrganizationId:
|
|
1690
|
+
accessToken: z2.string().optional(),
|
|
1691
|
+
refreshToken: z2.string().optional(),
|
|
1692
|
+
activeProjectId: z2.string().optional(),
|
|
1693
|
+
activeOrganizationId: z2.string().optional(),
|
|
1457
1694
|
user: userSchema.optional(),
|
|
1458
1695
|
// Stored role string. Kept as a free-form string so both legacy
|
|
1459
1696
|
// ("admin"/"team_lead"/"member") and unified
|
|
1460
1697
|
// ("owner"/"project_manager"/"team_lead"/"developer") values round-trip.
|
|
1461
1698
|
// Normalized on read via getUnifiedRole(); no longer enum-enforced.
|
|
1462
|
-
role:
|
|
1699
|
+
role: z2.string().optional()
|
|
1463
1700
|
});
|
|
1464
|
-
var projectConfigSchema =
|
|
1465
|
-
projectId:
|
|
1466
|
-
organizationId:
|
|
1701
|
+
var projectConfigSchema = z2.object({
|
|
1702
|
+
projectId: z2.string(),
|
|
1703
|
+
organizationId: z2.string(),
|
|
1467
1704
|
environment: environmentSchema.default("development")
|
|
1468
1705
|
});
|
|
1469
|
-
var projectEntrySchema =
|
|
1470
|
-
projectId:
|
|
1471
|
-
organizationId:
|
|
1472
|
-
projectName:
|
|
1473
|
-
organizationName:
|
|
1706
|
+
var projectEntrySchema = z2.object({
|
|
1707
|
+
projectId: z2.string(),
|
|
1708
|
+
organizationId: z2.string(),
|
|
1709
|
+
projectName: z2.string().default(""),
|
|
1710
|
+
organizationName: z2.string().default(""),
|
|
1474
1711
|
environment: environmentSchema.default("development"),
|
|
1475
1712
|
// How the pulled .env file's permissions are managed for this project:
|
|
1476
1713
|
// auto → derive from the caller's resolved access (default)
|
|
1477
1714
|
// always → force read-only (0o400)
|
|
1478
1715
|
// never → force writable (0o600)
|
|
1479
|
-
fileProtection:
|
|
1716
|
+
fileProtection: z2.enum(["auto", "always", "never"]).optional()
|
|
1480
1717
|
});
|
|
1481
|
-
var projectConfigV2Schema =
|
|
1482
|
-
version:
|
|
1483
|
-
activeProjectId:
|
|
1484
|
-
projects:
|
|
1718
|
+
var projectConfigV2Schema = z2.object({
|
|
1719
|
+
version: z2.literal(1),
|
|
1720
|
+
activeProjectId: z2.string(),
|
|
1721
|
+
projects: z2.array(projectEntrySchema).min(1)
|
|
1485
1722
|
});
|
|
1486
|
-
var variableRequestStatusSchema =
|
|
1723
|
+
var variableRequestStatusSchema = z2.enum([
|
|
1487
1724
|
"pending",
|
|
1488
1725
|
"approved",
|
|
1489
1726
|
"rejected",
|
|
1490
1727
|
"canceled"
|
|
1491
1728
|
]);
|
|
1492
|
-
var variableRequestUserSchema =
|
|
1493
|
-
_id:
|
|
1494
|
-
email:
|
|
1495
|
-
name:
|
|
1729
|
+
var variableRequestUserSchema = z2.object({
|
|
1730
|
+
_id: z2.string(),
|
|
1731
|
+
email: z2.string().optional(),
|
|
1732
|
+
name: z2.string().optional()
|
|
1496
1733
|
}).nullable().optional();
|
|
1497
|
-
var variableRequestSchema =
|
|
1498
|
-
_id:
|
|
1499
|
-
key:
|
|
1500
|
-
description:
|
|
1501
|
-
environments:
|
|
1502
|
-
projectId:
|
|
1503
|
-
organizationId:
|
|
1504
|
-
isSensitive:
|
|
1734
|
+
var variableRequestSchema = z2.object({
|
|
1735
|
+
_id: z2.string(),
|
|
1736
|
+
key: z2.string(),
|
|
1737
|
+
description: z2.string().optional(),
|
|
1738
|
+
environments: z2.array(z2.string()),
|
|
1739
|
+
projectId: z2.string(),
|
|
1740
|
+
organizationId: z2.string().optional(),
|
|
1741
|
+
isSensitive: z2.boolean().optional(),
|
|
1505
1742
|
status: variableRequestStatusSchema,
|
|
1506
|
-
reviewReason:
|
|
1507
|
-
createdAt:
|
|
1508
|
-
updatedAt:
|
|
1743
|
+
reviewReason: z2.string().optional(),
|
|
1744
|
+
createdAt: z2.number(),
|
|
1745
|
+
updatedAt: z2.number().optional(),
|
|
1509
1746
|
requester: variableRequestUserSchema,
|
|
1510
1747
|
reviewer: variableRequestUserSchema
|
|
1511
1748
|
});
|
|
@@ -1914,8 +2151,7 @@ async function addProject(existingConfig, options) {
|
|
|
1914
2151
|
let role = getUnifiedRole();
|
|
1915
2152
|
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1916
2153
|
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1917
|
-
|
|
1918
|
-
return response.data || [];
|
|
2154
|
+
return api.listOrganizations();
|
|
1919
2155
|
});
|
|
1920
2156
|
const freshOrg = orgs.find(
|
|
1921
2157
|
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
@@ -1995,8 +2231,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
1995
2231
|
const organizations = await withSpinner(
|
|
1996
2232
|
"Fetching organizations...",
|
|
1997
2233
|
async () => {
|
|
1998
|
-
|
|
1999
|
-
return response.data || [];
|
|
2234
|
+
return api.listOrganizations();
|
|
2000
2235
|
}
|
|
2001
2236
|
);
|
|
2002
2237
|
if (organizations.length === 0) {
|
|
@@ -2031,11 +2266,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
2031
2266
|
selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2032
2267
|
}
|
|
2033
2268
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
2034
|
-
|
|
2035
|
-
"/api/cli/projects",
|
|
2036
|
-
{ organizationId: selectedOrg._id }
|
|
2037
|
-
);
|
|
2038
|
-
return response.data || [];
|
|
2269
|
+
return api.listProjects(selectedOrg._id);
|
|
2039
2270
|
});
|
|
2040
2271
|
if (projects.length === 0) {
|
|
2041
2272
|
error("No projects found. Please create a project first.");
|
|
@@ -2622,15 +2853,13 @@ async function pullProject(project, outputPath, options) {
|
|
|
2622
2853
|
const variables = await withSpinner(
|
|
2623
2854
|
`Fetching ${chalk5.bold(project.environment)} variables...`,
|
|
2624
2855
|
async () => {
|
|
2625
|
-
const response = await api.
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
}
|
|
2631
|
-
});
|
|
2856
|
+
const response = await api.listVariables(
|
|
2857
|
+
project.projectId,
|
|
2858
|
+
project.environment,
|
|
2859
|
+
project.organizationId
|
|
2860
|
+
);
|
|
2632
2861
|
meta = response.meta;
|
|
2633
|
-
return response.
|
|
2862
|
+
return response.variables;
|
|
2634
2863
|
}
|
|
2635
2864
|
);
|
|
2636
2865
|
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
@@ -2755,24 +2984,24 @@ import chalk6 from "chalk";
|
|
|
2755
2984
|
import inquirer3 from "inquirer";
|
|
2756
2985
|
|
|
2757
2986
|
// src/lib/validators.ts
|
|
2758
|
-
import { z as
|
|
2759
|
-
var envKeySchema =
|
|
2987
|
+
import { z as z3 } from "zod";
|
|
2988
|
+
var envKeySchema = z3.string().min(1, "Key cannot be empty").max(256, "Key cannot exceed 256 characters").regex(
|
|
2760
2989
|
/^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
2761
2990
|
"Key must start with a letter or underscore, followed by letters, numbers, or underscores"
|
|
2762
2991
|
);
|
|
2763
|
-
var envValueSchema =
|
|
2764
|
-
var environmentSchema2 =
|
|
2992
|
+
var envValueSchema = z3.string().max(65536, "Value cannot exceed 64KB");
|
|
2993
|
+
var environmentSchema2 = z3.enum([
|
|
2765
2994
|
"development",
|
|
2766
2995
|
"staging",
|
|
2767
2996
|
"production"
|
|
2768
2997
|
]);
|
|
2769
|
-
var projectSlugSchema =
|
|
2998
|
+
var projectSlugSchema = z3.string().min(1, "Slug cannot be empty").max(128, "Slug cannot exceed 128 characters").regex(
|
|
2770
2999
|
/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/,
|
|
2771
3000
|
"Slug must be lowercase alphanumeric with hyphens, cannot start or end with hyphen"
|
|
2772
3001
|
);
|
|
2773
|
-
var urlSchema =
|
|
2774
|
-
var
|
|
2775
|
-
var filePathSchema =
|
|
3002
|
+
var urlSchema = z3.string().url("Must be a valid URL");
|
|
3003
|
+
var tokenSchema2 = z3.string().min(1, "Token cannot be empty").regex(/^env_[A-Za-z0-9]{48}$/, "Invalid token format");
|
|
3004
|
+
var filePathSchema = z3.string().min(1, "File path cannot be empty");
|
|
2776
3005
|
function validateEnvVars(vars) {
|
|
2777
3006
|
const valid = {};
|
|
2778
3007
|
const invalid = [];
|
|
@@ -2897,16 +3126,13 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2897
3126
|
const remoteVariables = await withSpinner(
|
|
2898
3127
|
"Fetching current variables...",
|
|
2899
3128
|
async () => {
|
|
2900
|
-
const
|
|
3129
|
+
const response = await api.listVariables(
|
|
2901
3130
|
projectId,
|
|
2902
|
-
environment
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
params.organizationId = organizationId;
|
|
2906
|
-
}
|
|
2907
|
-
const response = await api.get("/api/cli/variables", params);
|
|
3131
|
+
environment,
|
|
3132
|
+
organizationId
|
|
3133
|
+
);
|
|
2908
3134
|
meta = response.meta;
|
|
2909
|
-
return response.
|
|
3135
|
+
return response.variables;
|
|
2910
3136
|
}
|
|
2911
3137
|
);
|
|
2912
3138
|
const scope = meta?.environmentScope;
|
|
@@ -2977,7 +3203,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2977
3203
|
const result = await withSpinner(
|
|
2978
3204
|
`Pushing variables to ${chalk6.bold(environment)}...`,
|
|
2979
3205
|
async () => {
|
|
2980
|
-
|
|
3206
|
+
return api.bulkUpsertVariables({
|
|
2981
3207
|
projectId,
|
|
2982
3208
|
environment,
|
|
2983
3209
|
variables: Object.entries(valid).map(([key, value]) => ({
|
|
@@ -2987,7 +3213,6 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2987
3213
|
mode,
|
|
2988
3214
|
...organizationId && { organizationId }
|
|
2989
3215
|
});
|
|
2990
|
-
return response.data;
|
|
2991
3216
|
}
|
|
2992
3217
|
);
|
|
2993
3218
|
success(
|
|
@@ -3080,8 +3305,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3080
3305
|
const organizations = await withSpinner(
|
|
3081
3306
|
"Fetching organizations...",
|
|
3082
3307
|
async () => {
|
|
3083
|
-
|
|
3084
|
-
return response.data || [];
|
|
3308
|
+
return api.listOrganizations();
|
|
3085
3309
|
}
|
|
3086
3310
|
);
|
|
3087
3311
|
const org = organizations.find(
|
|
@@ -3129,8 +3353,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3129
3353
|
const organizations = await withSpinner(
|
|
3130
3354
|
"Fetching organizations...",
|
|
3131
3355
|
async () => {
|
|
3132
|
-
|
|
3133
|
-
return response.data || [];
|
|
3356
|
+
return api.listOrganizations();
|
|
3134
3357
|
}
|
|
3135
3358
|
);
|
|
3136
3359
|
if (organizations.length === 0) {
|
|
@@ -3160,8 +3383,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3160
3383
|
}
|
|
3161
3384
|
}
|
|
3162
3385
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
3163
|
-
|
|
3164
|
-
return response.data || [];
|
|
3386
|
+
return api.listProjects(organizationId);
|
|
3165
3387
|
});
|
|
3166
3388
|
const project = projects.find(
|
|
3167
3389
|
(p) => p._id === projectIdentifier || p.slug === projectIdentifier
|
|
@@ -3271,8 +3493,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3271
3493
|
const organizations = await withSpinner(
|
|
3272
3494
|
"Fetching organizations...",
|
|
3273
3495
|
async () => {
|
|
3274
|
-
|
|
3275
|
-
return response.data || [];
|
|
3496
|
+
return api.listOrganizations();
|
|
3276
3497
|
}
|
|
3277
3498
|
);
|
|
3278
3499
|
if (organizations.length === 0) {
|
|
@@ -3309,8 +3530,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3309
3530
|
const projects = await withSpinner(
|
|
3310
3531
|
"Fetching projects...",
|
|
3311
3532
|
async () => {
|
|
3312
|
-
|
|
3313
|
-
return response.data || [];
|
|
3533
|
+
return api.listProjects(orgId);
|
|
3314
3534
|
}
|
|
3315
3535
|
);
|
|
3316
3536
|
if (projects.length === 0) {
|
|
@@ -3433,8 +3653,7 @@ async function listOrganizations(api, options) {
|
|
|
3433
3653
|
const organizations = await withSpinner(
|
|
3434
3654
|
"Fetching organizations...",
|
|
3435
3655
|
async () => {
|
|
3436
|
-
|
|
3437
|
-
return response.data || [];
|
|
3656
|
+
return api.listOrganizations();
|
|
3438
3657
|
}
|
|
3439
3658
|
);
|
|
3440
3659
|
if (organizations.length === 0) {
|
|
@@ -3468,8 +3687,7 @@ async function listProjects(api, projectConfig, options) {
|
|
|
3468
3687
|
const organizations = await withSpinner(
|
|
3469
3688
|
"Fetching organizations...",
|
|
3470
3689
|
async () => {
|
|
3471
|
-
|
|
3472
|
-
return response.data || [];
|
|
3690
|
+
return api.listOrganizations();
|
|
3473
3691
|
}
|
|
3474
3692
|
);
|
|
3475
3693
|
if (organizations.length === 0) {
|
|
@@ -3488,11 +3706,7 @@ async function listProjects(api, projectConfig, options) {
|
|
|
3488
3706
|
}
|
|
3489
3707
|
}
|
|
3490
3708
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
3491
|
-
|
|
3492
|
-
"/api/cli/projects",
|
|
3493
|
-
{ organizationId }
|
|
3494
|
-
);
|
|
3495
|
-
return response.data || [];
|
|
3709
|
+
return api.listProjects(organizationId);
|
|
3496
3710
|
});
|
|
3497
3711
|
if (projects.length === 0) {
|
|
3498
3712
|
info("No projects found.");
|
|
@@ -3536,13 +3750,9 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3536
3750
|
}
|
|
3537
3751
|
let meta;
|
|
3538
3752
|
const variables = await withSpinner("Fetching variables...", async () => {
|
|
3539
|
-
const
|
|
3540
|
-
if (environment) {
|
|
3541
|
-
params.environment = environment;
|
|
3542
|
-
}
|
|
3543
|
-
const response = await api.get("/api/cli/variables", params);
|
|
3753
|
+
const response = await api.listVariables(projectId, environment);
|
|
3544
3754
|
meta = response.meta;
|
|
3545
|
-
return response.
|
|
3755
|
+
return response.variables;
|
|
3546
3756
|
});
|
|
3547
3757
|
const tagFilter = options.tag?.toLowerCase();
|
|
3548
3758
|
const filtered = tagFilter ? variables.filter(
|
|
@@ -3837,10 +4047,9 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
|
|
|
3837
4047
|
if (options.all) {
|
|
3838
4048
|
const accounts = listAccounts();
|
|
3839
4049
|
for (const account of accounts) {
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
await
|
|
3843
|
-
} catch {
|
|
4050
|
+
if (account.sessionId) {
|
|
4051
|
+
setActiveAccount(account.id);
|
|
4052
|
+
await revokeDeviceSession(account.sessionId);
|
|
3844
4053
|
}
|
|
3845
4054
|
removeAccount(account.id);
|
|
3846
4055
|
}
|
|
@@ -3849,10 +4058,8 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
|
|
|
3849
4058
|
return;
|
|
3850
4059
|
}
|
|
3851
4060
|
const activeAccount = getActiveAccount();
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
await api.post("/api/cli/auth?action=revoke", {});
|
|
3855
|
-
} catch {
|
|
4061
|
+
if (activeAccount?.sessionId) {
|
|
4062
|
+
await revokeDeviceSession(activeAccount.sessionId);
|
|
3856
4063
|
}
|
|
3857
4064
|
const { newActiveId } = clearAuth();
|
|
3858
4065
|
success(
|
|
@@ -4223,13 +4430,13 @@ var syncCommand = new Command10("sync").description(
|
|
|
4223
4430
|
`Fetching ${chalk11.bold(environment)} variables...`,
|
|
4224
4431
|
async () => {
|
|
4225
4432
|
const api = createAPIClient();
|
|
4226
|
-
const response = await api.
|
|
4433
|
+
const response = await api.listVariables(
|
|
4227
4434
|
projectId,
|
|
4228
4435
|
environment,
|
|
4229
|
-
|
|
4230
|
-
|
|
4436
|
+
organizationId
|
|
4437
|
+
);
|
|
4231
4438
|
meta = response.meta;
|
|
4232
|
-
return response.
|
|
4439
|
+
return response.variables;
|
|
4233
4440
|
}
|
|
4234
4441
|
);
|
|
4235
4442
|
const outputPath = getEnvPathForEnvironment(environment);
|
|
@@ -4333,8 +4540,7 @@ var usageCommand = new Command11("usage").description("Show plan usage and limit
|
|
|
4333
4540
|
const orgs = await withSpinner(
|
|
4334
4541
|
"Fetching organizations...",
|
|
4335
4542
|
async () => {
|
|
4336
|
-
|
|
4337
|
-
return response.data || [];
|
|
4543
|
+
return api.listOrganizations();
|
|
4338
4544
|
}
|
|
4339
4545
|
);
|
|
4340
4546
|
if (orgs.length === 0) {
|
|
@@ -4454,7 +4660,7 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
4454
4660
|
);
|
|
4455
4661
|
}
|
|
4456
4662
|
blank();
|
|
4457
|
-
console.log(chalk13.dim("
|
|
4663
|
+
console.log(chalk13.dim("Session verified against Convex."));
|
|
4458
4664
|
} catch (err) {
|
|
4459
4665
|
await handleError(err);
|
|
4460
4666
|
}
|
|
@@ -4953,18 +5159,18 @@ import chalk17 from "chalk";
|
|
|
4953
5159
|
import inquirer6 from "inquirer";
|
|
4954
5160
|
|
|
4955
5161
|
// src/lib/variable-requests.ts
|
|
4956
|
-
import { z as
|
|
5162
|
+
import { z as z4 } from "zod";
|
|
4957
5163
|
var ALL_REQUEST_ENVIRONMENTS = [
|
|
4958
5164
|
"development",
|
|
4959
5165
|
"staging",
|
|
4960
5166
|
"production"
|
|
4961
5167
|
];
|
|
4962
|
-
var requestKeySchema =
|
|
5168
|
+
var requestKeySchema = z4.string().min(1, "Key is required").max(100, "Key must be 100 characters or less").regex(
|
|
4963
5169
|
/^[A-Z][A-Z0-9_]*$/,
|
|
4964
5170
|
"Key must be uppercase, start with a letter, and contain only letters, numbers, and underscores"
|
|
4965
5171
|
);
|
|
4966
|
-
var requestValueSchema =
|
|
4967
|
-
var requestDescriptionSchema =
|
|
5172
|
+
var requestValueSchema = z4.string().min(1, "Value is required");
|
|
5173
|
+
var requestDescriptionSchema = z4.string().max(500, "Description must be 500 characters or less");
|
|
4968
5174
|
function validateRequestKey(key) {
|
|
4969
5175
|
const result = requestKeySchema.safeParse(key);
|
|
4970
5176
|
if (result.success) return { valid: true };
|
|
@@ -5289,10 +5495,11 @@ async function resolveEntryNames(api, entry) {
|
|
|
5289
5495
|
};
|
|
5290
5496
|
}
|
|
5291
5497
|
async function fetchProjectMeta(api, entry) {
|
|
5292
|
-
const response = await api.
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5498
|
+
const response = await api.listVariables(
|
|
5499
|
+
entry.projectId,
|
|
5500
|
+
void 0,
|
|
5501
|
+
entry.organizationId
|
|
5502
|
+
);
|
|
5296
5503
|
return response.meta;
|
|
5297
5504
|
}
|
|
5298
5505
|
async function pickRequestTarget(api) {
|