@envpilot/cli 1.10.0 → 1.12.0
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,10 @@ function initSentry() {
|
|
|
7
7
|
Sentry.init({
|
|
8
8
|
dsn,
|
|
9
9
|
environment: "cli",
|
|
10
|
-
release: true ? "1.
|
|
10
|
+
release: true ? "1.12.0" : "0.0.0",
|
|
11
|
+
// All EnvPilot surfaces report to one Sentry project; the surface tag
|
|
12
|
+
// is how dashboards tell web / cli / extension events apart.
|
|
13
|
+
initialScope: { tags: { surface: "cli" } },
|
|
11
14
|
// Free tier: disable performance monitoring
|
|
12
15
|
tracesSampleRate: 0,
|
|
13
16
|
beforeSend(event) {
|
|
@@ -387,15 +390,6 @@ function setApiUrl(url) {
|
|
|
387
390
|
function getAccessToken() {
|
|
388
391
|
return getActiveAccount()?.accessToken;
|
|
389
392
|
}
|
|
390
|
-
function setAccessToken(token) {
|
|
391
|
-
updateActiveAccount({ accessToken: token });
|
|
392
|
-
}
|
|
393
|
-
function getRefreshToken() {
|
|
394
|
-
return getActiveAccount()?.refreshToken;
|
|
395
|
-
}
|
|
396
|
-
function setRefreshToken(token) {
|
|
397
|
-
updateActiveAccount({ refreshToken: token });
|
|
398
|
-
}
|
|
399
393
|
function getActiveProjectId() {
|
|
400
394
|
return getActiveAccount()?.activeProjectId;
|
|
401
395
|
}
|
|
@@ -469,7 +463,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
469
463
|
async function openTUI() {
|
|
470
464
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
471
465
|
import("ink"),
|
|
472
|
-
import("./app-
|
|
466
|
+
import("./app-3TUQOGJ6.js"),
|
|
473
467
|
import("./press-any-key-64XFP4O2.js")
|
|
474
468
|
]);
|
|
475
469
|
while (true) {
|
|
@@ -740,13 +734,221 @@ import open from "open";
|
|
|
740
734
|
import chalk3 from "chalk";
|
|
741
735
|
import { hostname } from "os";
|
|
742
736
|
|
|
743
|
-
// src/lib/
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
+
}
|
|
748
788
|
}
|
|
749
|
-
|
|
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";
|
|
750
952
|
var APIError = class extends Error {
|
|
751
953
|
constructor(message, statusCode, code) {
|
|
752
954
|
super(message);
|
|
@@ -755,48 +957,139 @@ var APIError = class extends Error {
|
|
|
755
957
|
this.name = "APIError";
|
|
756
958
|
}
|
|
757
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;
|
|
758
1083
|
var APIClient = class {
|
|
759
1084
|
baseUrl;
|
|
760
|
-
accessToken;
|
|
761
|
-
// Re-entrancy guard: true while a token refresh is in flight so a 401 from
|
|
762
|
-
// the refresh endpoint itself can never trigger another refresh (no loops).
|
|
763
|
-
refreshing = false;
|
|
764
1085
|
constructor(options) {
|
|
765
1086
|
this.baseUrl = options?.baseUrl ?? getApiUrl();
|
|
766
|
-
this.accessToken = options?.accessToken ?? getAccessToken();
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* Get headers for API requests
|
|
770
|
-
*/
|
|
771
|
-
getHeaders() {
|
|
772
|
-
const headers = {
|
|
773
|
-
"Content-Type": "application/json"
|
|
774
|
-
};
|
|
775
|
-
if (this.accessToken) {
|
|
776
|
-
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
777
|
-
}
|
|
778
|
-
return headers;
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Detect auth middleware redirects that returned HTML instead of CLI JSON.
|
|
782
|
-
*/
|
|
783
|
-
isAuthRedirect(response, bodyText) {
|
|
784
|
-
const location = response.headers.get("location") || "";
|
|
785
|
-
const finalUrl = response.url || "";
|
|
786
|
-
const contentType = response.headers.get("content-type") || "";
|
|
787
|
-
const preview = (bodyText || "").slice(0, 512).toLowerCase();
|
|
788
|
-
return location.includes("authkit") || finalUrl.includes("authkit") || contentType.includes("text/html") && (preview.includes("authorization_session_id") || preview.includes("client_id=") || preview.includes("<!doctype html"));
|
|
789
1087
|
}
|
|
1088
|
+
// ── Vault HTTP path (decrypted secret values) ──────────────────────────
|
|
790
1089
|
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
* Node's default redirect follower drops the Authorization header on any
|
|
795
|
-
* hostname change and the resulting request comes back as a bogus 401 —
|
|
796
|
-
* which used to wipe the user's credentials.
|
|
797
|
-
*
|
|
798
|
-
* Cross-site redirects (different registrable domain) are followed without
|
|
799
|
-
* 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).
|
|
800
1093
|
*/
|
|
801
1094
|
async fetchWithSafeRedirects(initialUrl, init2) {
|
|
802
1095
|
let currentUrl = initialUrl;
|
|
@@ -812,9 +1105,7 @@ var APIClient = class {
|
|
|
812
1105
|
const prevHost = new URL(currentUrl).hostname;
|
|
813
1106
|
const sameSite = registrableDomain(nextUrl.hostname) === registrableDomain(prevHost);
|
|
814
1107
|
const headers = new Headers(currentInit.headers);
|
|
815
|
-
if (!sameSite)
|
|
816
|
-
headers.delete("Authorization");
|
|
817
|
-
}
|
|
1108
|
+
if (!sameSite) headers.delete("Authorization");
|
|
818
1109
|
let nextMethod = (currentInit.method || "GET").toUpperCase();
|
|
819
1110
|
let nextBody = currentInit.body;
|
|
820
1111
|
if (response.status === 301 || response.status === 302 || response.status === 303) {
|
|
@@ -839,166 +1130,54 @@ var APIClient = class {
|
|
|
839
1130
|
"TOO_MANY_REDIRECTS"
|
|
840
1131
|
);
|
|
841
1132
|
}
|
|
842
|
-
/**
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
async tryRefreshToken() {
|
|
850
|
-
if (this.refreshing) return false;
|
|
851
|
-
const refreshToken = getRefreshToken();
|
|
852
|
-
if (!refreshToken) return false;
|
|
853
|
-
this.refreshing = true;
|
|
854
|
-
try {
|
|
855
|
-
const result = await this.refreshToken(refreshToken);
|
|
856
|
-
setAccessToken(result.accessToken);
|
|
857
|
-
setRefreshToken(result.refreshToken);
|
|
858
|
-
this.accessToken = result.accessToken;
|
|
859
|
-
return true;
|
|
860
|
-
} catch {
|
|
861
|
-
return false;
|
|
862
|
-
} finally {
|
|
863
|
-
this.refreshing = false;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* Run a request thunk with automatic one-shot token refresh on a genuine 401.
|
|
868
|
-
*
|
|
869
|
-
* - An AUTH_REDIRECT (HTML sign-in page) is NOT a rejected token — rethrow
|
|
870
|
-
* without clearing creds or refreshing.
|
|
871
|
-
* - A genuine 401 triggers a single refresh attempt. On success the request
|
|
872
|
-
* is retried once with the rotated token; on failure (or a second 401) the
|
|
873
|
-
* local credentials are cleared so the user is prompted to log in again.
|
|
874
|
-
*
|
|
875
|
-
* The thunk rebuilds its headers on each call, so the retry automatically
|
|
876
|
-
* picks up the refreshed access token.
|
|
877
|
-
*/
|
|
878
|
-
async withAuthRetry(exec) {
|
|
879
|
-
try {
|
|
880
|
-
return await exec();
|
|
881
|
-
} catch (err) {
|
|
882
|
-
if (!(err instanceof APIError) || err.statusCode !== 401 || err.code === "AUTH_REDIRECT" || this.refreshing) {
|
|
883
|
-
throw err;
|
|
884
|
-
}
|
|
885
|
-
const refreshed = await this.tryRefreshToken();
|
|
886
|
-
if (!refreshed) {
|
|
887
|
-
clearAuth();
|
|
888
|
-
throw err;
|
|
889
|
-
}
|
|
890
|
-
try {
|
|
891
|
-
return await exec();
|
|
892
|
-
} catch (retryErr) {
|
|
893
|
-
if (retryErr instanceof APIError && retryErr.statusCode === 401 && retryErr.code !== "AUTH_REDIRECT") {
|
|
894
|
-
clearAuth();
|
|
895
|
-
}
|
|
896
|
-
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);
|
|
897
1140
|
}
|
|
898
1141
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (params) {
|
|
907
|
-
for (const [key, value] of Object.entries(params)) {
|
|
908
|
-
url.searchParams.set(key, value);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
912
|
-
method: "GET",
|
|
913
|
-
headers: this.getHeaders()
|
|
914
|
-
});
|
|
915
|
-
return this.handleResponse(response);
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
/**
|
|
919
|
-
* Make a POST request
|
|
920
|
-
*/
|
|
921
|
-
async post(path, body) {
|
|
922
|
-
return this.withAuthRetry(async () => {
|
|
923
|
-
const url = new URL(path, this.baseUrl);
|
|
924
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
925
|
-
method: "POST",
|
|
926
|
-
headers: this.getHeaders(),
|
|
927
|
-
body: body ? JSON.stringify(body) : void 0
|
|
928
|
-
});
|
|
929
|
-
return this.handleResponse(response);
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Make a PUT request
|
|
934
|
-
*/
|
|
935
|
-
async put(path, body) {
|
|
936
|
-
return this.withAuthRetry(async () => {
|
|
937
|
-
const url = new URL(path, this.baseUrl);
|
|
938
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
939
|
-
method: "PUT",
|
|
940
|
-
headers: this.getHeaders(),
|
|
941
|
-
body: body ? JSON.stringify(body) : void 0
|
|
942
|
-
});
|
|
943
|
-
return this.handleResponse(response);
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Make a PATCH request
|
|
948
|
-
*/
|
|
949
|
-
async patch(path, body) {
|
|
950
|
-
return this.withAuthRetry(async () => {
|
|
951
|
-
const url = new URL(path, this.baseUrl);
|
|
952
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
953
|
-
method: "PATCH",
|
|
954
|
-
headers: this.getHeaders(),
|
|
955
|
-
body: body ? JSON.stringify(body) : void 0
|
|
956
|
-
});
|
|
957
|
-
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
|
|
958
1149
|
});
|
|
1150
|
+
return this.handleResponse(response);
|
|
959
1151
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
967
|
-
method: "DELETE",
|
|
968
|
-
headers: this.getHeaders()
|
|
969
|
-
});
|
|
970
|
-
if (!response.ok) {
|
|
971
|
-
await this.handleError(response);
|
|
972
|
-
}
|
|
973
|
-
});
|
|
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"));
|
|
974
1158
|
}
|
|
975
|
-
/**
|
|
976
|
-
* Handle API response
|
|
977
|
-
*/
|
|
978
1159
|
async handleResponse(response) {
|
|
979
1160
|
if (!response.ok) {
|
|
980
1161
|
await this.handleError(response);
|
|
981
1162
|
}
|
|
982
1163
|
const contentType = response.headers.get("content-type") || "";
|
|
1164
|
+
const body = await response.text();
|
|
983
1165
|
if (!contentType.includes("application/json")) {
|
|
984
|
-
|
|
985
|
-
if (this.isAuthRedirect(response, body2)) {
|
|
1166
|
+
if (this.isAuthRedirect(response, body)) {
|
|
986
1167
|
throw new APIError(
|
|
987
1168
|
"Your CLI session is not authorized for this endpoint. Please run `envpilot login` and try again.",
|
|
988
1169
|
401,
|
|
989
1170
|
"AUTH_REDIRECT"
|
|
990
1171
|
);
|
|
991
1172
|
}
|
|
992
|
-
const preview =
|
|
1173
|
+
const preview = body.replace(/\s+/g, " ").slice(0, 160);
|
|
993
1174
|
throw new APIError(
|
|
994
1175
|
`Expected JSON but got ${contentType || "unknown content type"} from ${response.url}. Response starts with: ${preview}`,
|
|
995
1176
|
response.status || 500
|
|
996
1177
|
);
|
|
997
1178
|
}
|
|
998
|
-
const body = await response.text();
|
|
999
1179
|
try {
|
|
1000
|
-
|
|
1001
|
-
return data;
|
|
1180
|
+
return JSON.parse(body);
|
|
1002
1181
|
} catch {
|
|
1003
1182
|
const preview = body.replace(/\s+/g, " ").slice(0, 160);
|
|
1004
1183
|
throw new APIError(
|
|
@@ -1007,9 +1186,6 @@ var APIClient = class {
|
|
|
1007
1186
|
);
|
|
1008
1187
|
}
|
|
1009
1188
|
}
|
|
1010
|
-
/**
|
|
1011
|
-
* Handle API errors
|
|
1012
|
-
*/
|
|
1013
1189
|
async handleError(response) {
|
|
1014
1190
|
const bodyText = await response.text();
|
|
1015
1191
|
let message = `Request failed with status ${response.status}`;
|
|
@@ -1054,192 +1230,230 @@ var APIClient = class {
|
|
|
1054
1230
|
throw new APIError(message, response.status, code);
|
|
1055
1231
|
}
|
|
1056
1232
|
// ============================================
|
|
1057
|
-
// High-level
|
|
1233
|
+
// High-level methods — DIRECT to Convex
|
|
1058
1234
|
// ============================================
|
|
1059
1235
|
/**
|
|
1060
|
-
*
|
|
1236
|
+
* Current authenticated user. Verifies the session is live server-side (an
|
|
1237
|
+
* authed Convex query) and returns the identity captured at login.
|
|
1061
1238
|
*/
|
|
1062
1239
|
async getCurrentUser() {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
*/
|
|
1074
|
-
async getUsage(organizationId) {
|
|
1075
|
-
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;
|
|
1076
1250
|
}
|
|
1077
|
-
/**
|
|
1078
|
-
* List organizations the user has access to
|
|
1079
|
-
*/
|
|
1251
|
+
/** List organizations the user belongs to (with tier + role). */
|
|
1080
1252
|
async listOrganizations() {
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
+
}));
|
|
1085
1275
|
}
|
|
1086
|
-
/**
|
|
1087
|
-
* List projects in an organization
|
|
1088
|
-
*/
|
|
1276
|
+
/** List projects in an organization, with unified-role + assignment info. */
|
|
1089
1277
|
async listProjects(organizationId) {
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
{ 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
|
+
})
|
|
1093
1313
|
);
|
|
1094
|
-
return response.data || [];
|
|
1095
1314
|
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Get a project by ID
|
|
1098
|
-
*/
|
|
1315
|
+
/** Get a single project (metadata) by id. */
|
|
1099
1316
|
async getProject(projectId) {
|
|
1100
|
-
|
|
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
|
+
};
|
|
1101
1330
|
}
|
|
1102
1331
|
/**
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
* `variables`. Callers should warn the user about `decryptionFailures`
|
|
1108
|
-
* 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.
|
|
1109
1336
|
*/
|
|
1110
|
-
async
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
+
);
|
|
1114
1366
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
+
);
|
|
1117
1378
|
}
|
|
1118
|
-
const response = await this.get("/api/cli/variables", params);
|
|
1119
1379
|
return {
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
+
}
|
|
1122
1398
|
};
|
|
1123
1399
|
}
|
|
1400
|
+
// ============================================
|
|
1401
|
+
// High-level methods — VAULT (over HTTP)
|
|
1402
|
+
// ============================================
|
|
1124
1403
|
/**
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
* WITHOUT decrypting vault secrets. The CLI uses this to decide whether
|
|
1129
|
-
* a cached variable set is still current before doing a full (expensive)
|
|
1130
|
-
* 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).
|
|
1131
1407
|
*/
|
|
1132
|
-
async
|
|
1408
|
+
async listVariables(projectId, environment, organizationId) {
|
|
1133
1409
|
const params = { projectId };
|
|
1134
1410
|
if (environment) params.environment = environment;
|
|
1135
1411
|
if (organizationId) params.organizationId = organizationId;
|
|
1136
|
-
const response = await this.
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Get a variable by ID (with decrypted value)
|
|
1144
|
-
*/
|
|
1145
|
-
async getVariable(variableId) {
|
|
1146
|
-
return this.get(`/api/cli/variables/${variableId}`);
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Create a new variable
|
|
1150
|
-
*/
|
|
1151
|
-
async createVariable(data) {
|
|
1152
|
-
return this.post("/api/cli/variables", data);
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Update a variable
|
|
1156
|
-
*/
|
|
1157
|
-
async updateVariable(variableId, data) {
|
|
1158
|
-
return this.patch(`/api/cli/variables/${variableId}`, data);
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* Delete a variable
|
|
1162
|
-
*/
|
|
1163
|
-
async deleteVariable(variableId) {
|
|
1164
|
-
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
|
+
};
|
|
1165
1418
|
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Bulk create/update variables
|
|
1168
|
-
*/
|
|
1419
|
+
/** Bulk create/update variables (vault path — encrypts values server-side). */
|
|
1169
1420
|
async bulkUpsertVariables(data) {
|
|
1170
|
-
|
|
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;
|
|
1171
1426
|
}
|
|
1172
1427
|
/**
|
|
1173
|
-
* Submit a variable request (developers only
|
|
1174
|
-
*
|
|
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.
|
|
1175
1431
|
*/
|
|
1176
1432
|
async createVariableRequest(data) {
|
|
1177
|
-
const response = await this.
|
|
1178
|
-
"/api/cli/variable-requests",
|
|
1179
|
-
data
|
|
1180
|
-
);
|
|
1433
|
+
const response = await this.vaultRequest("POST", "/api/cli/variable-requests", { body: data });
|
|
1181
1434
|
if (!response.data) {
|
|
1182
1435
|
throw new APIError("No variable request returned by server", 500);
|
|
1183
1436
|
}
|
|
1184
1437
|
return response.data.request;
|
|
1185
1438
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
* List variable requests for a project, optionally filtered by status.
|
|
1188
|
-
*/
|
|
1189
|
-
async listVariableRequests(projectId, status) {
|
|
1190
|
-
const params = { projectId };
|
|
1191
|
-
if (status) params.status = status;
|
|
1192
|
-
const response = await this.get("/api/cli/variable-requests", params);
|
|
1193
|
-
return response.data?.requests ?? [];
|
|
1194
|
-
}
|
|
1195
|
-
// ============================================
|
|
1196
|
-
// Authentication methods
|
|
1197
|
-
// ============================================
|
|
1198
|
-
/**
|
|
1199
|
-
* Initiate CLI authentication flow
|
|
1200
|
-
*/
|
|
1201
|
-
async initiateAuth(deviceName) {
|
|
1202
|
-
return this.post("/api/cli/auth?action=initiate", { deviceName });
|
|
1203
|
-
}
|
|
1204
|
-
/**
|
|
1205
|
-
* Poll for authentication status
|
|
1206
|
-
*/
|
|
1207
|
-
async pollAuth(code) {
|
|
1208
|
-
return this.get("/api/cli/auth", { action: "poll", code });
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Refresh access token
|
|
1212
|
-
*/
|
|
1213
|
-
async refreshToken(refreshToken) {
|
|
1214
|
-
return this.post("/api/cli/auth?action=refresh", { refreshToken });
|
|
1215
|
-
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Revoke access token (logout)
|
|
1218
|
-
*/
|
|
1219
|
-
async revokeToken() {
|
|
1220
|
-
return this.post("/api/cli/auth?action=revoke", {});
|
|
1221
|
-
}
|
|
1222
1439
|
};
|
|
1223
1440
|
function createAPIClient() {
|
|
1224
1441
|
return new APIClient();
|
|
1225
1442
|
}
|
|
1226
1443
|
|
|
1227
1444
|
// src/lib/auth-flow.ts
|
|
1228
|
-
var POLL_INTERVAL_MS = 2e3;
|
|
1229
|
-
var MAX_POLL_ATTEMPTS = 150;
|
|
1230
1445
|
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
1231
1446
|
function sleep(ms) {
|
|
1232
1447
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1233
1448
|
}
|
|
1234
1449
|
async function performLogin(options) {
|
|
1235
|
-
const api = createAPIClient();
|
|
1236
1450
|
const deviceName = `CLI - ${hostname()}`;
|
|
1237
1451
|
info("Starting authentication flow...");
|
|
1238
|
-
const spinner = createSpinner("
|
|
1452
|
+
const spinner = createSpinner("Requesting device code...");
|
|
1239
1453
|
spinner.start();
|
|
1240
|
-
let
|
|
1454
|
+
let device;
|
|
1241
1455
|
try {
|
|
1242
|
-
|
|
1456
|
+
device = await requestDeviceCode();
|
|
1243
1457
|
} catch (error2) {
|
|
1244
1458
|
spinner.stop();
|
|
1245
1459
|
throw error2;
|
|
@@ -1248,60 +1462,77 @@ async function performLogin(options) {
|
|
|
1248
1462
|
console.log();
|
|
1249
1463
|
console.log(chalk3.bold("Your authentication code:"));
|
|
1250
1464
|
console.log();
|
|
1251
|
-
console.log(chalk3.cyan.bold(` ${
|
|
1465
|
+
console.log(chalk3.cyan.bold(` ${device.user_code}`));
|
|
1252
1466
|
console.log();
|
|
1253
|
-
console.log(
|
|
1254
|
-
console.log(chalk3.dim(
|
|
1467
|
+
console.log("Open this URL to authenticate:");
|
|
1468
|
+
console.log(chalk3.dim(device.verification_uri_complete));
|
|
1255
1469
|
console.log();
|
|
1256
1470
|
if (options?.browser !== false) {
|
|
1257
1471
|
info("Opening browser...");
|
|
1258
|
-
|
|
1472
|
+
try {
|
|
1473
|
+
await open(device.verification_uri_complete);
|
|
1474
|
+
} catch {
|
|
1475
|
+
}
|
|
1259
1476
|
}
|
|
1260
1477
|
const pollSpinner = createSpinner("Waiting for authentication...");
|
|
1261
1478
|
pollSpinner.start();
|
|
1479
|
+
let intervalMs = device.interval * 1e3;
|
|
1480
|
+
const deadline = Date.now() + device.expires_in * 1e3;
|
|
1262
1481
|
let consecutiveErrors = 0;
|
|
1263
1482
|
try {
|
|
1264
|
-
|
|
1265
|
-
await sleep(
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
pollResponse = await api.pollAuth(initResponse.code);
|
|
1269
|
-
consecutiveErrors = 0;
|
|
1270
|
-
} catch {
|
|
1271
|
-
consecutiveErrors++;
|
|
1272
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1273
|
-
throw new Error(
|
|
1274
|
-
"Too many consecutive network errors while polling. Please check your connection and try again."
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
continue;
|
|
1278
|
-
}
|
|
1279
|
-
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") {
|
|
1280
1487
|
pollSpinner.stop();
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
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;
|
|
1287
1493
|
const account = {
|
|
1288
|
-
id:
|
|
1289
|
-
user:
|
|
1290
|
-
id:
|
|
1291
|
-
email:
|
|
1292
|
-
name
|
|
1293
|
-
}
|
|
1294
|
-
accessToken:
|
|
1295
|
-
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
|
|
1296
1504
|
};
|
|
1297
1505
|
upsertAccount(account);
|
|
1298
1506
|
setActiveAccount(account.id);
|
|
1507
|
+
if (sessionId) {
|
|
1508
|
+
await recordDeviceSession(deviceName, sessionId);
|
|
1509
|
+
}
|
|
1299
1510
|
console.log();
|
|
1300
|
-
success(`Logged in as ${chalk3.bold(
|
|
1301
|
-
return { email
|
|
1511
|
+
success(`Logged in as ${chalk3.bold(email || userId)}`);
|
|
1512
|
+
return { email };
|
|
1302
1513
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
+
);
|
|
1305
1536
|
}
|
|
1306
1537
|
}
|
|
1307
1538
|
throw new Error("Authentication timed out. Please try again.");
|
|
@@ -1357,152 +1588,161 @@ import { execSync } from "child_process";
|
|
|
1357
1588
|
import { join as join2 } from "path";
|
|
1358
1589
|
|
|
1359
1590
|
// src/types/index.ts
|
|
1360
|
-
import { z } from "zod";
|
|
1361
|
-
var userSchema =
|
|
1362
|
-
id:
|
|
1363
|
-
email:
|
|
1364
|
-
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()
|
|
1365
1596
|
});
|
|
1366
|
-
var accountSchema =
|
|
1367
|
-
id:
|
|
1597
|
+
var accountSchema = z2.object({
|
|
1598
|
+
id: z2.string(),
|
|
1368
1599
|
user: userSchema,
|
|
1369
|
-
accessToken
|
|
1370
|
-
|
|
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(),
|
|
1371
1611
|
// Stored role string (legacy or unified); normalized on read.
|
|
1372
|
-
role:
|
|
1612
|
+
role: z2.string().optional(),
|
|
1373
1613
|
// Per-account active org/project selection.
|
|
1374
|
-
activeOrganizationId:
|
|
1375
|
-
activeProjectId:
|
|
1614
|
+
activeOrganizationId: z2.string().optional(),
|
|
1615
|
+
activeProjectId: z2.string().optional()
|
|
1376
1616
|
});
|
|
1377
|
-
var organizationSchema =
|
|
1378
|
-
_id:
|
|
1379
|
-
name:
|
|
1380
|
-
slug:
|
|
1381
|
-
tier:
|
|
1382
|
-
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(),
|
|
1383
1623
|
// Unified org role (additive; legacy responses omit it and fall back to role).
|
|
1384
|
-
unifiedRole:
|
|
1624
|
+
unifiedRole: z2.string().nullable().optional()
|
|
1385
1625
|
});
|
|
1386
|
-
var projectSchema =
|
|
1387
|
-
_id:
|
|
1388
|
-
name:
|
|
1389
|
-
slug:
|
|
1390
|
-
organizationId:
|
|
1391
|
-
description:
|
|
1392
|
-
icon:
|
|
1393
|
-
color:
|
|
1394
|
-
userRole:
|
|
1395
|
-
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(),
|
|
1396
1636
|
// Unified-role fields (optional so legacy server responses still parse)
|
|
1397
|
-
unifiedRole:
|
|
1398
|
-
assigned:
|
|
1399
|
-
environmentScope:
|
|
1637
|
+
unifiedRole: z2.string().nullable().optional(),
|
|
1638
|
+
assigned: z2.boolean().optional(),
|
|
1639
|
+
environmentScope: z2.array(z2.string()).nullable().optional()
|
|
1400
1640
|
});
|
|
1401
|
-
var variableTagSchema =
|
|
1402
|
-
_id:
|
|
1403
|
-
name:
|
|
1404
|
-
color:
|
|
1641
|
+
var variableTagSchema = z2.object({
|
|
1642
|
+
_id: z2.string(),
|
|
1643
|
+
name: z2.string(),
|
|
1644
|
+
color: z2.string()
|
|
1405
1645
|
});
|
|
1406
|
-
var variableSchema =
|
|
1407
|
-
_id:
|
|
1408
|
-
key:
|
|
1409
|
-
value:
|
|
1410
|
-
environment:
|
|
1411
|
-
projectId:
|
|
1412
|
-
description:
|
|
1413
|
-
isSensitive:
|
|
1414
|
-
version:
|
|
1415
|
-
updatedAt:
|
|
1416
|
-
createdAt:
|
|
1417
|
-
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(),
|
|
1418
1658
|
// Per-variable effective access under the unified model. Optional so older
|
|
1419
1659
|
// server responses (which omit it) still parse.
|
|
1420
|
-
access:
|
|
1660
|
+
access: z2.enum(["read", "write"]).optional()
|
|
1421
1661
|
});
|
|
1422
|
-
var variablesMetaSchema =
|
|
1423
|
-
total:
|
|
1424
|
-
page:
|
|
1425
|
-
limit:
|
|
1426
|
-
decryptionFailures:
|
|
1427
|
-
unifiedRole:
|
|
1428
|
-
assigned:
|
|
1429
|
-
grantOnly:
|
|
1430
|
-
environmentScope:
|
|
1431
|
-
hasWriteAccess:
|
|
1432
|
-
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()
|
|
1433
1673
|
}).passthrough();
|
|
1434
|
-
var environmentSchema =
|
|
1674
|
+
var environmentSchema = z2.enum([
|
|
1435
1675
|
"development",
|
|
1436
1676
|
"staging",
|
|
1437
1677
|
"production"
|
|
1438
1678
|
]);
|
|
1439
|
-
var cliConfigSchema =
|
|
1679
|
+
var cliConfigSchema = z2.object({
|
|
1440
1680
|
// Global CLI setting — shared across all accounts.
|
|
1441
|
-
apiUrl:
|
|
1681
|
+
apiUrl: z2.string().url(),
|
|
1442
1682
|
// Multi-account store. Keyed by account id (== user id).
|
|
1443
|
-
accounts:
|
|
1683
|
+
accounts: z2.record(z2.string(), accountSchema).optional(),
|
|
1444
1684
|
// The currently active account id.
|
|
1445
|
-
activeAccountId:
|
|
1685
|
+
activeAccountId: z2.string().optional(),
|
|
1446
1686
|
// --- Legacy single-account fields ---
|
|
1447
1687
|
// Retained ONLY so pre-multi-account configs still parse and can be migrated
|
|
1448
1688
|
// into `accounts` by migrateLegacyConfig(). After migration these are deleted
|
|
1449
1689
|
// so `accounts` is the single source of truth. Do not read/write directly.
|
|
1450
|
-
accessToken:
|
|
1451
|
-
refreshToken:
|
|
1452
|
-
activeProjectId:
|
|
1453
|
-
activeOrganizationId:
|
|
1690
|
+
accessToken: z2.string().optional(),
|
|
1691
|
+
refreshToken: z2.string().optional(),
|
|
1692
|
+
activeProjectId: z2.string().optional(),
|
|
1693
|
+
activeOrganizationId: z2.string().optional(),
|
|
1454
1694
|
user: userSchema.optional(),
|
|
1455
1695
|
// Stored role string. Kept as a free-form string so both legacy
|
|
1456
1696
|
// ("admin"/"team_lead"/"member") and unified
|
|
1457
1697
|
// ("owner"/"project_manager"/"team_lead"/"developer") values round-trip.
|
|
1458
1698
|
// Normalized on read via getUnifiedRole(); no longer enum-enforced.
|
|
1459
|
-
role:
|
|
1699
|
+
role: z2.string().optional()
|
|
1460
1700
|
});
|
|
1461
|
-
var projectConfigSchema =
|
|
1462
|
-
projectId:
|
|
1463
|
-
organizationId:
|
|
1701
|
+
var projectConfigSchema = z2.object({
|
|
1702
|
+
projectId: z2.string(),
|
|
1703
|
+
organizationId: z2.string(),
|
|
1464
1704
|
environment: environmentSchema.default("development")
|
|
1465
1705
|
});
|
|
1466
|
-
var projectEntrySchema =
|
|
1467
|
-
projectId:
|
|
1468
|
-
organizationId:
|
|
1469
|
-
projectName:
|
|
1470
|
-
organizationName:
|
|
1706
|
+
var projectEntrySchema = z2.object({
|
|
1707
|
+
projectId: z2.string(),
|
|
1708
|
+
organizationId: z2.string(),
|
|
1709
|
+
projectName: z2.string().default(""),
|
|
1710
|
+
organizationName: z2.string().default(""),
|
|
1471
1711
|
environment: environmentSchema.default("development"),
|
|
1472
1712
|
// How the pulled .env file's permissions are managed for this project:
|
|
1473
1713
|
// auto → derive from the caller's resolved access (default)
|
|
1474
1714
|
// always → force read-only (0o400)
|
|
1475
1715
|
// never → force writable (0o600)
|
|
1476
|
-
fileProtection:
|
|
1716
|
+
fileProtection: z2.enum(["auto", "always", "never"]).optional()
|
|
1477
1717
|
});
|
|
1478
|
-
var projectConfigV2Schema =
|
|
1479
|
-
version:
|
|
1480
|
-
activeProjectId:
|
|
1481
|
-
projects:
|
|
1718
|
+
var projectConfigV2Schema = z2.object({
|
|
1719
|
+
version: z2.literal(1),
|
|
1720
|
+
activeProjectId: z2.string(),
|
|
1721
|
+
projects: z2.array(projectEntrySchema).min(1)
|
|
1482
1722
|
});
|
|
1483
|
-
var variableRequestStatusSchema =
|
|
1723
|
+
var variableRequestStatusSchema = z2.enum([
|
|
1484
1724
|
"pending",
|
|
1485
1725
|
"approved",
|
|
1486
1726
|
"rejected",
|
|
1487
1727
|
"canceled"
|
|
1488
1728
|
]);
|
|
1489
|
-
var variableRequestUserSchema =
|
|
1490
|
-
_id:
|
|
1491
|
-
email:
|
|
1492
|
-
name:
|
|
1729
|
+
var variableRequestUserSchema = z2.object({
|
|
1730
|
+
_id: z2.string(),
|
|
1731
|
+
email: z2.string().optional(),
|
|
1732
|
+
name: z2.string().optional()
|
|
1493
1733
|
}).nullable().optional();
|
|
1494
|
-
var variableRequestSchema =
|
|
1495
|
-
_id:
|
|
1496
|
-
key:
|
|
1497
|
-
description:
|
|
1498
|
-
environments:
|
|
1499
|
-
projectId:
|
|
1500
|
-
organizationId:
|
|
1501
|
-
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(),
|
|
1502
1742
|
status: variableRequestStatusSchema,
|
|
1503
|
-
reviewReason:
|
|
1504
|
-
createdAt:
|
|
1505
|
-
updatedAt:
|
|
1743
|
+
reviewReason: z2.string().optional(),
|
|
1744
|
+
createdAt: z2.number(),
|
|
1745
|
+
updatedAt: z2.number().optional(),
|
|
1506
1746
|
requester: variableRequestUserSchema,
|
|
1507
1747
|
reviewer: variableRequestUserSchema
|
|
1508
1748
|
});
|
|
@@ -1911,8 +2151,7 @@ async function addProject(existingConfig, options) {
|
|
|
1911
2151
|
let role = getUnifiedRole();
|
|
1912
2152
|
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1913
2153
|
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1914
|
-
|
|
1915
|
-
return response.data || [];
|
|
2154
|
+
return api.listOrganizations();
|
|
1916
2155
|
});
|
|
1917
2156
|
const freshOrg = orgs.find(
|
|
1918
2157
|
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
@@ -1992,8 +2231,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
1992
2231
|
const organizations = await withSpinner(
|
|
1993
2232
|
"Fetching organizations...",
|
|
1994
2233
|
async () => {
|
|
1995
|
-
|
|
1996
|
-
return response.data || [];
|
|
2234
|
+
return api.listOrganizations();
|
|
1997
2235
|
}
|
|
1998
2236
|
);
|
|
1999
2237
|
if (organizations.length === 0) {
|
|
@@ -2028,11 +2266,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
2028
2266
|
selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2029
2267
|
}
|
|
2030
2268
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
2031
|
-
|
|
2032
|
-
"/api/cli/projects",
|
|
2033
|
-
{ organizationId: selectedOrg._id }
|
|
2034
|
-
);
|
|
2035
|
-
return response.data || [];
|
|
2269
|
+
return api.listProjects(selectedOrg._id);
|
|
2036
2270
|
});
|
|
2037
2271
|
if (projects.length === 0) {
|
|
2038
2272
|
error("No projects found. Please create a project first.");
|
|
@@ -2619,15 +2853,13 @@ async function pullProject(project, outputPath, options) {
|
|
|
2619
2853
|
const variables = await withSpinner(
|
|
2620
2854
|
`Fetching ${chalk5.bold(project.environment)} variables...`,
|
|
2621
2855
|
async () => {
|
|
2622
|
-
const response = await api.
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
}
|
|
2628
|
-
});
|
|
2856
|
+
const response = await api.listVariables(
|
|
2857
|
+
project.projectId,
|
|
2858
|
+
project.environment,
|
|
2859
|
+
project.organizationId
|
|
2860
|
+
);
|
|
2629
2861
|
meta = response.meta;
|
|
2630
|
-
return response.
|
|
2862
|
+
return response.variables;
|
|
2631
2863
|
}
|
|
2632
2864
|
);
|
|
2633
2865
|
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
@@ -2752,24 +2984,24 @@ import chalk6 from "chalk";
|
|
|
2752
2984
|
import inquirer3 from "inquirer";
|
|
2753
2985
|
|
|
2754
2986
|
// src/lib/validators.ts
|
|
2755
|
-
import { z as
|
|
2756
|
-
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(
|
|
2757
2989
|
/^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
2758
2990
|
"Key must start with a letter or underscore, followed by letters, numbers, or underscores"
|
|
2759
2991
|
);
|
|
2760
|
-
var envValueSchema =
|
|
2761
|
-
var environmentSchema2 =
|
|
2992
|
+
var envValueSchema = z3.string().max(65536, "Value cannot exceed 64KB");
|
|
2993
|
+
var environmentSchema2 = z3.enum([
|
|
2762
2994
|
"development",
|
|
2763
2995
|
"staging",
|
|
2764
2996
|
"production"
|
|
2765
2997
|
]);
|
|
2766
|
-
var projectSlugSchema =
|
|
2998
|
+
var projectSlugSchema = z3.string().min(1, "Slug cannot be empty").max(128, "Slug cannot exceed 128 characters").regex(
|
|
2767
2999
|
/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/,
|
|
2768
3000
|
"Slug must be lowercase alphanumeric with hyphens, cannot start or end with hyphen"
|
|
2769
3001
|
);
|
|
2770
|
-
var urlSchema =
|
|
2771
|
-
var
|
|
2772
|
-
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");
|
|
2773
3005
|
function validateEnvVars(vars) {
|
|
2774
3006
|
const valid = {};
|
|
2775
3007
|
const invalid = [];
|
|
@@ -2894,16 +3126,13 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2894
3126
|
const remoteVariables = await withSpinner(
|
|
2895
3127
|
"Fetching current variables...",
|
|
2896
3128
|
async () => {
|
|
2897
|
-
const
|
|
3129
|
+
const response = await api.listVariables(
|
|
2898
3130
|
projectId,
|
|
2899
|
-
environment
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
params.organizationId = organizationId;
|
|
2903
|
-
}
|
|
2904
|
-
const response = await api.get("/api/cli/variables", params);
|
|
3131
|
+
environment,
|
|
3132
|
+
organizationId
|
|
3133
|
+
);
|
|
2905
3134
|
meta = response.meta;
|
|
2906
|
-
return response.
|
|
3135
|
+
return response.variables;
|
|
2907
3136
|
}
|
|
2908
3137
|
);
|
|
2909
3138
|
const scope = meta?.environmentScope;
|
|
@@ -2974,7 +3203,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2974
3203
|
const result = await withSpinner(
|
|
2975
3204
|
`Pushing variables to ${chalk6.bold(environment)}...`,
|
|
2976
3205
|
async () => {
|
|
2977
|
-
|
|
3206
|
+
return api.bulkUpsertVariables({
|
|
2978
3207
|
projectId,
|
|
2979
3208
|
environment,
|
|
2980
3209
|
variables: Object.entries(valid).map(([key, value]) => ({
|
|
@@ -2984,7 +3213,6 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2984
3213
|
mode,
|
|
2985
3214
|
...organizationId && { organizationId }
|
|
2986
3215
|
});
|
|
2987
|
-
return response.data;
|
|
2988
3216
|
}
|
|
2989
3217
|
);
|
|
2990
3218
|
success(
|
|
@@ -3077,8 +3305,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3077
3305
|
const organizations = await withSpinner(
|
|
3078
3306
|
"Fetching organizations...",
|
|
3079
3307
|
async () => {
|
|
3080
|
-
|
|
3081
|
-
return response.data || [];
|
|
3308
|
+
return api.listOrganizations();
|
|
3082
3309
|
}
|
|
3083
3310
|
);
|
|
3084
3311
|
const org = organizations.find(
|
|
@@ -3126,8 +3353,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3126
3353
|
const organizations = await withSpinner(
|
|
3127
3354
|
"Fetching organizations...",
|
|
3128
3355
|
async () => {
|
|
3129
|
-
|
|
3130
|
-
return response.data || [];
|
|
3356
|
+
return api.listOrganizations();
|
|
3131
3357
|
}
|
|
3132
3358
|
);
|
|
3133
3359
|
if (organizations.length === 0) {
|
|
@@ -3157,8 +3383,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3157
3383
|
}
|
|
3158
3384
|
}
|
|
3159
3385
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
3160
|
-
|
|
3161
|
-
return response.data || [];
|
|
3386
|
+
return api.listProjects(organizationId);
|
|
3162
3387
|
});
|
|
3163
3388
|
const project = projects.find(
|
|
3164
3389
|
(p) => p._id === projectIdentifier || p.slug === projectIdentifier
|
|
@@ -3268,8 +3493,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3268
3493
|
const organizations = await withSpinner(
|
|
3269
3494
|
"Fetching organizations...",
|
|
3270
3495
|
async () => {
|
|
3271
|
-
|
|
3272
|
-
return response.data || [];
|
|
3496
|
+
return api.listOrganizations();
|
|
3273
3497
|
}
|
|
3274
3498
|
);
|
|
3275
3499
|
if (organizations.length === 0) {
|
|
@@ -3306,8 +3530,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
3306
3530
|
const projects = await withSpinner(
|
|
3307
3531
|
"Fetching projects...",
|
|
3308
3532
|
async () => {
|
|
3309
|
-
|
|
3310
|
-
return response.data || [];
|
|
3533
|
+
return api.listProjects(orgId);
|
|
3311
3534
|
}
|
|
3312
3535
|
);
|
|
3313
3536
|
if (projects.length === 0) {
|
|
@@ -3430,8 +3653,7 @@ async function listOrganizations(api, options) {
|
|
|
3430
3653
|
const organizations = await withSpinner(
|
|
3431
3654
|
"Fetching organizations...",
|
|
3432
3655
|
async () => {
|
|
3433
|
-
|
|
3434
|
-
return response.data || [];
|
|
3656
|
+
return api.listOrganizations();
|
|
3435
3657
|
}
|
|
3436
3658
|
);
|
|
3437
3659
|
if (organizations.length === 0) {
|
|
@@ -3465,8 +3687,7 @@ async function listProjects(api, projectConfig, options) {
|
|
|
3465
3687
|
const organizations = await withSpinner(
|
|
3466
3688
|
"Fetching organizations...",
|
|
3467
3689
|
async () => {
|
|
3468
|
-
|
|
3469
|
-
return response.data || [];
|
|
3690
|
+
return api.listOrganizations();
|
|
3470
3691
|
}
|
|
3471
3692
|
);
|
|
3472
3693
|
if (organizations.length === 0) {
|
|
@@ -3485,11 +3706,7 @@ async function listProjects(api, projectConfig, options) {
|
|
|
3485
3706
|
}
|
|
3486
3707
|
}
|
|
3487
3708
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
3488
|
-
|
|
3489
|
-
"/api/cli/projects",
|
|
3490
|
-
{ organizationId }
|
|
3491
|
-
);
|
|
3492
|
-
return response.data || [];
|
|
3709
|
+
return api.listProjects(organizationId);
|
|
3493
3710
|
});
|
|
3494
3711
|
if (projects.length === 0) {
|
|
3495
3712
|
info("No projects found.");
|
|
@@ -3533,13 +3750,9 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3533
3750
|
}
|
|
3534
3751
|
let meta;
|
|
3535
3752
|
const variables = await withSpinner("Fetching variables...", async () => {
|
|
3536
|
-
const
|
|
3537
|
-
if (environment) {
|
|
3538
|
-
params.environment = environment;
|
|
3539
|
-
}
|
|
3540
|
-
const response = await api.get("/api/cli/variables", params);
|
|
3753
|
+
const response = await api.listVariables(projectId, environment);
|
|
3541
3754
|
meta = response.meta;
|
|
3542
|
-
return response.
|
|
3755
|
+
return response.variables;
|
|
3543
3756
|
});
|
|
3544
3757
|
const tagFilter = options.tag?.toLowerCase();
|
|
3545
3758
|
const filtered = tagFilter ? variables.filter(
|
|
@@ -3834,10 +4047,9 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
|
|
|
3834
4047
|
if (options.all) {
|
|
3835
4048
|
const accounts = listAccounts();
|
|
3836
4049
|
for (const account of accounts) {
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
await
|
|
3840
|
-
} catch {
|
|
4050
|
+
if (account.sessionId) {
|
|
4051
|
+
setActiveAccount(account.id);
|
|
4052
|
+
await revokeDeviceSession(account.sessionId);
|
|
3841
4053
|
}
|
|
3842
4054
|
removeAccount(account.id);
|
|
3843
4055
|
}
|
|
@@ -3846,10 +4058,8 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
|
|
|
3846
4058
|
return;
|
|
3847
4059
|
}
|
|
3848
4060
|
const activeAccount = getActiveAccount();
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
await api.post("/api/cli/auth?action=revoke", {});
|
|
3852
|
-
} catch {
|
|
4061
|
+
if (activeAccount?.sessionId) {
|
|
4062
|
+
await revokeDeviceSession(activeAccount.sessionId);
|
|
3853
4063
|
}
|
|
3854
4064
|
const { newActiveId } = clearAuth();
|
|
3855
4065
|
success(
|
|
@@ -4220,13 +4430,13 @@ var syncCommand = new Command10("sync").description(
|
|
|
4220
4430
|
`Fetching ${chalk11.bold(environment)} variables...`,
|
|
4221
4431
|
async () => {
|
|
4222
4432
|
const api = createAPIClient();
|
|
4223
|
-
const response = await api.
|
|
4433
|
+
const response = await api.listVariables(
|
|
4224
4434
|
projectId,
|
|
4225
4435
|
environment,
|
|
4226
|
-
|
|
4227
|
-
|
|
4436
|
+
organizationId
|
|
4437
|
+
);
|
|
4228
4438
|
meta = response.meta;
|
|
4229
|
-
return response.
|
|
4439
|
+
return response.variables;
|
|
4230
4440
|
}
|
|
4231
4441
|
);
|
|
4232
4442
|
const outputPath = getEnvPathForEnvironment(environment);
|
|
@@ -4330,8 +4540,7 @@ var usageCommand = new Command11("usage").description("Show plan usage and limit
|
|
|
4330
4540
|
const orgs = await withSpinner(
|
|
4331
4541
|
"Fetching organizations...",
|
|
4332
4542
|
async () => {
|
|
4333
|
-
|
|
4334
|
-
return response.data || [];
|
|
4543
|
+
return api.listOrganizations();
|
|
4335
4544
|
}
|
|
4336
4545
|
);
|
|
4337
4546
|
if (orgs.length === 0) {
|
|
@@ -4451,7 +4660,7 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
4451
4660
|
);
|
|
4452
4661
|
}
|
|
4453
4662
|
blank();
|
|
4454
|
-
console.log(chalk13.dim("
|
|
4663
|
+
console.log(chalk13.dim("Session verified against Convex."));
|
|
4455
4664
|
} catch (err) {
|
|
4456
4665
|
await handleError(err);
|
|
4457
4666
|
}
|
|
@@ -4950,18 +5159,18 @@ import chalk17 from "chalk";
|
|
|
4950
5159
|
import inquirer6 from "inquirer";
|
|
4951
5160
|
|
|
4952
5161
|
// src/lib/variable-requests.ts
|
|
4953
|
-
import { z as
|
|
5162
|
+
import { z as z4 } from "zod";
|
|
4954
5163
|
var ALL_REQUEST_ENVIRONMENTS = [
|
|
4955
5164
|
"development",
|
|
4956
5165
|
"staging",
|
|
4957
5166
|
"production"
|
|
4958
5167
|
];
|
|
4959
|
-
var requestKeySchema =
|
|
5168
|
+
var requestKeySchema = z4.string().min(1, "Key is required").max(100, "Key must be 100 characters or less").regex(
|
|
4960
5169
|
/^[A-Z][A-Z0-9_]*$/,
|
|
4961
5170
|
"Key must be uppercase, start with a letter, and contain only letters, numbers, and underscores"
|
|
4962
5171
|
);
|
|
4963
|
-
var requestValueSchema =
|
|
4964
|
-
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");
|
|
4965
5174
|
function validateRequestKey(key) {
|
|
4966
5175
|
const result = requestKeySchema.safeParse(key);
|
|
4967
5176
|
if (result.success) return { valid: true };
|
|
@@ -5286,10 +5495,11 @@ async function resolveEntryNames(api, entry) {
|
|
|
5286
5495
|
};
|
|
5287
5496
|
}
|
|
5288
5497
|
async function fetchProjectMeta(api, entry) {
|
|
5289
|
-
const response = await api.
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5498
|
+
const response = await api.listVariables(
|
|
5499
|
+
entry.projectId,
|
|
5500
|
+
void 0,
|
|
5501
|
+
entry.organizationId
|
|
5502
|
+
);
|
|
5293
5503
|
return response.meta;
|
|
5294
5504
|
}
|
|
5295
5505
|
async function pickRequestTarget(api) {
|
|
@@ -5826,6 +6036,8 @@ function findCommandDefinition(commandIdOrName) {
|
|
|
5826
6036
|
|
|
5827
6037
|
export {
|
|
5828
6038
|
initSentry,
|
|
6039
|
+
captureError,
|
|
6040
|
+
flushSentry,
|
|
5829
6041
|
getApiUrl,
|
|
5830
6042
|
getUser,
|
|
5831
6043
|
isAuthenticated,
|