@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.0" : "0.0.0",
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-UT26PMDR.js"),
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/api.ts
744
- function registrableDomain(hostname2) {
745
- const parts = hostname2.toLowerCase().split(".").filter(Boolean);
746
- if (parts.length <= 2) return parts.join(".");
747
- return parts.slice(-2).join(".");
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
- var MAX_MANUAL_REDIRECTS = 5;
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
- * Perform a fetch that follows 3xx redirects manually, re-attaching the
792
- * Authorization header when the redirect stays inside the same registrable
793
- * domain (eTLD+1). This defends against the apex→www redirect case where
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
- * Attempt a one-shot access-token refresh using the stored refresh token.
844
- * Persists the rotated tokens and updates this client's in-memory token on
845
- * success. Returns false (without throwing) when there is no refresh token,
846
- * a refresh is already in flight, or the refresh call fails — the caller
847
- * then treats the session as dead.
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
- * Make a GET request
902
- */
903
- async get(path, params) {
904
- return this.withAuthRetry(async () => {
905
- const url = new URL(path, this.baseUrl);
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
- * Make a DELETE request
962
- */
963
- async delete(path) {
964
- return this.withAuthRetry(async () => {
965
- const url = new URL(path, this.baseUrl);
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
- const body2 = await response.text();
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 = body2.replace(/\s+/g, " ").slice(0, 160);
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
- const data = JSON.parse(body);
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 API methods
1233
+ // High-level methods — DIRECT to Convex
1058
1234
  // ============================================
1059
1235
  /**
1060
- * Get current user info
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
- return this.get("/api/cli/auth", { action: "me" });
1064
- }
1065
- /**
1066
- * Get tier info for the active organization
1067
- */
1068
- async getTierInfo(organizationId) {
1069
- return this.get("/api/cli/tier", { organizationId });
1070
- }
1071
- /**
1072
- * Get usage info for the active organization
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 response = await this.get(
1082
- "/api/cli/organizations"
1083
- );
1084
- return response.data || [];
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 response = await this.get(
1091
- "/api/cli/projects",
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
- return this.get(`/api/cli/projects/${projectId}`);
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
- * List variables in a project (with decrypted values).
1104
- *
1105
- * Returns both the variable list and any keys that failed vault decryption.
1106
- * Decryption failures are skipped server-side — they will NOT appear in
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 listVariables(projectId, environment, organizationId) {
1111
- const params = { projectId };
1112
- if (environment) {
1113
- params.environment = environment;
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
- if (organizationId) {
1116
- params.organizationId = organizationId;
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
- variables: response.data || [],
1121
- decryptionFailures: response.meta?.decryptionFailures ?? []
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
- * Check the variable fingerprint for a project/environment.
1126
- *
1127
- * Returns a short hash of variable metadata (id + version + updatedAt)
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 checkFingerprint(projectId, environment, organizationId) {
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.get(
1137
- "/api/cli/variables/fingerprint",
1138
- params
1139
- );
1140
- return response.fingerprint;
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
- return this.post("/api/cli/variables/bulk", data);
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 owners/PMs/team leads
1174
- * create variables directly and get a 403 from the server here).
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.post(
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("Generating authentication code...");
1452
+ const spinner = createSpinner("Requesting device code...");
1239
1453
  spinner.start();
1240
- let initResponse;
1454
+ let device;
1241
1455
  try {
1242
- initResponse = await api.initiateAuth(deviceName);
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(` ${initResponse.code}`));
1465
+ console.log(chalk3.cyan.bold(` ${device.user_code}`));
1252
1466
  console.log();
1253
- console.log(`Open this URL to authenticate:`);
1254
- console.log(chalk3.dim(initResponse.url));
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
- await open(initResponse.url);
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
- for (let attempts = 0; attempts < MAX_POLL_ATTEMPTS; attempts++) {
1265
- await sleep(POLL_INTERVAL_MS);
1266
- let pollResponse;
1267
- try {
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
- if (!pollResponse.accessToken || !pollResponse.refreshToken) {
1282
- throw new Error(
1283
- "Authentication succeeded but the server did not return session tokens. This is likely a server-side issue. Please try again or contact support."
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: accountId,
1289
- user: pollResponse.user ? {
1290
- id: pollResponse.user.id,
1291
- email: pollResponse.user.email,
1292
- name: pollResponse.user.name
1293
- } : { id: accountId, email: `${accountId}@cli.local` },
1294
- accessToken: pollResponse.accessToken,
1295
- refreshToken: pollResponse.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(pollResponse.user?.email)}`);
1301
- return { email: pollResponse.user?.email || "" };
1511
+ success(`Logged in as ${chalk3.bold(email || userId)}`);
1512
+ return { email };
1302
1513
  }
1303
- if (pollResponse.status === "expired" || pollResponse.status === "not_found") {
1304
- throw new Error("Authentication code expired. Please try again.");
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 = z.object({
1362
- id: z.string(),
1363
- email: z.string().email(),
1364
- name: z.string().optional()
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 = z.object({
1367
- id: z.string(),
1597
+ var accountSchema = z2.object({
1598
+ id: z2.string(),
1368
1599
  user: userSchema,
1369
- accessToken: z.string(),
1370
- refreshToken: z.string().optional(),
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: z.string().optional(),
1612
+ role: z2.string().optional(),
1373
1613
  // Per-account active org/project selection.
1374
- activeOrganizationId: z.string().optional(),
1375
- activeProjectId: z.string().optional()
1614
+ activeOrganizationId: z2.string().optional(),
1615
+ activeProjectId: z2.string().optional()
1376
1616
  });
1377
- var organizationSchema = z.object({
1378
- _id: z.string(),
1379
- name: z.string(),
1380
- slug: z.string(),
1381
- tier: z.enum(["free", "pro"]),
1382
- role: z.string().optional(),
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: z.string().nullable().optional()
1624
+ unifiedRole: z2.string().nullable().optional()
1385
1625
  });
1386
- var projectSchema = z.object({
1387
- _id: z.string(),
1388
- name: z.string(),
1389
- slug: z.string(),
1390
- organizationId: z.string(),
1391
- description: z.string().optional(),
1392
- icon: z.string().optional(),
1393
- color: z.string().optional(),
1394
- userRole: z.string().nullable().optional(),
1395
- projectRole: z.string().nullable().optional(),
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: z.string().nullable().optional(),
1398
- assigned: z.boolean().optional(),
1399
- environmentScope: z.array(z.string()).nullable().optional()
1637
+ unifiedRole: z2.string().nullable().optional(),
1638
+ assigned: z2.boolean().optional(),
1639
+ environmentScope: z2.array(z2.string()).nullable().optional()
1400
1640
  });
1401
- var variableTagSchema = z.object({
1402
- _id: z.string(),
1403
- name: z.string(),
1404
- color: z.string()
1641
+ var variableTagSchema = z2.object({
1642
+ _id: z2.string(),
1643
+ name: z2.string(),
1644
+ color: z2.string()
1405
1645
  });
1406
- var variableSchema = z.object({
1407
- _id: z.string(),
1408
- key: z.string(),
1409
- value: z.string(),
1410
- environment: z.enum(["development", "staging", "production"]),
1411
- projectId: z.string(),
1412
- description: z.string().optional(),
1413
- isSensitive: z.boolean().optional(),
1414
- version: z.number().optional(),
1415
- updatedAt: z.number().optional(),
1416
- createdAt: z.number().optional(),
1417
- tags: z.array(variableTagSchema).optional(),
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: z.enum(["read", "write"]).optional()
1660
+ access: z2.enum(["read", "write"]).optional()
1421
1661
  });
1422
- var variablesMetaSchema = z.object({
1423
- total: z.number().optional(),
1424
- page: z.number().optional(),
1425
- limit: z.number().optional(),
1426
- decryptionFailures: z.array(z.string()).optional(),
1427
- unifiedRole: z.string().nullable().optional(),
1428
- assigned: z.boolean().optional(),
1429
- grantOnly: z.boolean().optional(),
1430
- environmentScope: z.array(z.string()).nullable().optional(),
1431
- hasWriteAccess: z.boolean().optional(),
1432
- scopeRestricted: z.boolean().optional()
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 = z.enum([
1674
+ var environmentSchema = z2.enum([
1435
1675
  "development",
1436
1676
  "staging",
1437
1677
  "production"
1438
1678
  ]);
1439
- var cliConfigSchema = z.object({
1679
+ var cliConfigSchema = z2.object({
1440
1680
  // Global CLI setting — shared across all accounts.
1441
- apiUrl: z.string().url(),
1681
+ apiUrl: z2.string().url(),
1442
1682
  // Multi-account store. Keyed by account id (== user id).
1443
- accounts: z.record(z.string(), accountSchema).optional(),
1683
+ accounts: z2.record(z2.string(), accountSchema).optional(),
1444
1684
  // The currently active account id.
1445
- activeAccountId: z.string().optional(),
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: z.string().optional(),
1451
- refreshToken: z.string().optional(),
1452
- activeProjectId: z.string().optional(),
1453
- activeOrganizationId: z.string().optional(),
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: z.string().optional()
1699
+ role: z2.string().optional()
1460
1700
  });
1461
- var projectConfigSchema = z.object({
1462
- projectId: z.string(),
1463
- organizationId: z.string(),
1701
+ var projectConfigSchema = z2.object({
1702
+ projectId: z2.string(),
1703
+ organizationId: z2.string(),
1464
1704
  environment: environmentSchema.default("development")
1465
1705
  });
1466
- var projectEntrySchema = z.object({
1467
- projectId: z.string(),
1468
- organizationId: z.string(),
1469
- projectName: z.string().default(""),
1470
- organizationName: z.string().default(""),
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: z.enum(["auto", "always", "never"]).optional()
1716
+ fileProtection: z2.enum(["auto", "always", "never"]).optional()
1477
1717
  });
1478
- var projectConfigV2Schema = z.object({
1479
- version: z.literal(1),
1480
- activeProjectId: z.string(),
1481
- projects: z.array(projectEntrySchema).min(1)
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 = z.enum([
1723
+ var variableRequestStatusSchema = z2.enum([
1484
1724
  "pending",
1485
1725
  "approved",
1486
1726
  "rejected",
1487
1727
  "canceled"
1488
1728
  ]);
1489
- var variableRequestUserSchema = z.object({
1490
- _id: z.string(),
1491
- email: z.string().optional(),
1492
- name: z.string().optional()
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 = z.object({
1495
- _id: z.string(),
1496
- key: z.string(),
1497
- description: z.string().optional(),
1498
- environments: z.array(z.string()),
1499
- projectId: z.string(),
1500
- organizationId: z.string().optional(),
1501
- isSensitive: z.boolean().optional(),
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: z.string().optional(),
1504
- createdAt: z.number(),
1505
- updatedAt: z.number().optional(),
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get(
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.get("/api/cli/variables", {
2623
- projectId: project.projectId,
2624
- environment: project.environment,
2625
- ...project.organizationId && {
2626
- organizationId: project.organizationId
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.data || [];
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 z2 } from "zod";
2756
- var envKeySchema = z2.string().min(1, "Key cannot be empty").max(256, "Key cannot exceed 256 characters").regex(
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 = z2.string().max(65536, "Value cannot exceed 64KB");
2761
- var environmentSchema2 = z2.enum([
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 = z2.string().min(1, "Slug cannot be empty").max(128, "Slug cannot exceed 128 characters").regex(
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 = z2.string().url("Must be a valid URL");
2771
- var tokenSchema = z2.string().min(1, "Token cannot be empty").regex(/^env_[A-Za-z0-9]{48}$/, "Invalid token format");
2772
- var filePathSchema = z2.string().min(1, "File path cannot be empty");
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 params = {
3129
+ const response = await api.listVariables(
2898
3130
  projectId,
2899
- environment
2900
- };
2901
- if (organizationId) {
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.data || [];
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
- const response = await api.post("/api/cli/variables/bulk", {
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get("/api/cli/projects", { organizationId });
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get("/api/cli/projects", { organizationId: orgId });
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get("/api/cli/organizations");
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
- const response = await api.get(
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 params = { projectId };
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.data || [];
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
- try {
3838
- const api2 = new APIClient({ accessToken: account.accessToken });
3839
- await api2.post("/api/cli/auth?action=revoke", {});
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
- const api = createAPIClient();
3850
- try {
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.get("/api/cli/variables", {
4433
+ const response = await api.listVariables(
4224
4434
  projectId,
4225
4435
  environment,
4226
- ...organizationId && { organizationId }
4227
- });
4436
+ organizationId
4437
+ );
4228
4438
  meta = response.meta;
4229
- return response.data || [];
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
- const response = await api.get("/api/cli/organizations");
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("Token verified against the CLI auth endpoint."));
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 z3 } from "zod";
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 = z3.string().min(1, "Key is required").max(100, "Key must be 100 characters or less").regex(
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 = z3.string().min(1, "Value is required");
4964
- var requestDescriptionSchema = z3.string().max(500, "Description must be 500 characters or less");
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.get("/api/cli/variables", {
5290
- projectId: entry.projectId,
5291
- ...entry.organizationId && { organizationId: entry.organizationId }
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,