convex 1.47.0 → 1.48.0-alpha.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.
Files changed (177) hide show
  1. package/dist/browser.bundle.js +1 -1
  2. package/dist/browser.bundle.js.map +1 -1
  3. package/dist/cjs/bundler/context.js +7 -5
  4. package/dist/cjs/bundler/context.js.map +2 -2
  5. package/dist/cjs/bundler/fs.js +47 -6
  6. package/dist/cjs/bundler/fs.js.map +2 -2
  7. package/dist/cjs/cli/account.js +237 -0
  8. package/dist/cjs/cli/account.js.map +7 -0
  9. package/dist/cjs/cli/configure.js +15 -2
  10. package/dist/cjs/cli/configure.js.map +2 -2
  11. package/dist/cjs/cli/lib/deployKeyWarning.js +43 -0
  12. package/dist/cjs/cli/lib/deployKeyWarning.js.map +7 -0
  13. package/dist/cjs/cli/lib/deployment.js +2 -0
  14. package/dist/cjs/cli/lib/deployment.js.map +2 -2
  15. package/dist/cjs/cli/lib/deploymentSelection.js +27 -18
  16. package/dist/cjs/cli/lib/deploymentSelection.js.map +2 -2
  17. package/dist/cjs/cli/lib/envvars.js +32 -0
  18. package/dist/cjs/cli/lib/envvars.js.map +2 -2
  19. package/dist/cjs/cli/lib/fsUtils.js +1 -1
  20. package/dist/cjs/cli/lib/fsUtils.js.map +2 -2
  21. package/dist/cjs/cli/lib/generateDocs.js +1 -1
  22. package/dist/cjs/cli/lib/generateDocs.js.map +1 -1
  23. package/dist/cjs/cli/lib/login.js +28 -4
  24. package/dist/cjs/cli/lib/login.js.map +2 -2
  25. package/dist/cjs/cli/lib/mcp/requestContext.js +5 -1
  26. package/dist/cjs/cli/lib/mcp/requestContext.js.map +2 -2
  27. package/dist/cjs/cli/lib/profileManagement.js +196 -0
  28. package/dist/cjs/cli/lib/profileManagement.js.map +7 -0
  29. package/dist/cjs/cli/lib/profiles.js +333 -0
  30. package/dist/cjs/cli/lib/profiles.js.map +7 -0
  31. package/dist/cjs/cli/lib/utils/utils.js +18 -9
  32. package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
  33. package/dist/cjs/cli/login.js +213 -114
  34. package/dist/cjs/cli/login.js.map +3 -3
  35. package/dist/cjs/cli/logout.js +113 -12
  36. package/dist/cjs/cli/logout.js.map +3 -3
  37. package/dist/cjs/cli/mcp.js +43 -10
  38. package/dist/cjs/cli/mcp.js.map +3 -3
  39. package/dist/cjs/cli/program.js +2 -1
  40. package/dist/cjs/cli/program.js.map +2 -2
  41. package/dist/cjs/index.js +1 -1
  42. package/dist/cjs/index.js.map +1 -1
  43. package/dist/cjs-types/bundler/context.d.ts +5 -0
  44. package/dist/cjs-types/bundler/context.d.ts.map +1 -1
  45. package/dist/cjs-types/bundler/fs.d.ts +7 -2
  46. package/dist/cjs-types/bundler/fs.d.ts.map +1 -1
  47. package/dist/cjs-types/cli/account.d.ts +6 -0
  48. package/dist/cjs-types/cli/account.d.ts.map +1 -0
  49. package/dist/cjs-types/cli/configure.d.ts.map +1 -1
  50. package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts +3 -0
  51. package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
  52. package/dist/cjs-types/cli/lib/deployment.d.ts +2 -0
  53. package/dist/cjs-types/cli/lib/deployment.d.ts.map +1 -1
  54. package/dist/cjs-types/cli/lib/deploymentSelection.d.ts +2 -1
  55. package/dist/cjs-types/cli/lib/deploymentSelection.d.ts.map +1 -1
  56. package/dist/cjs-types/cli/lib/envvars.d.ts +6 -0
  57. package/dist/cjs-types/cli/lib/envvars.d.ts.map +1 -1
  58. package/dist/cjs-types/cli/lib/fsUtils.d.ts +1 -0
  59. package/dist/cjs-types/cli/lib/fsUtils.d.ts.map +1 -1
  60. package/dist/cjs-types/cli/lib/login.d.ts +3 -1
  61. package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
  62. package/dist/cjs-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
  63. package/dist/cjs-types/cli/lib/mcp/tools/insights.d.ts +2 -2
  64. package/dist/cjs-types/cli/lib/profileManagement.d.ts +10 -0
  65. package/dist/cjs-types/cli/lib/profileManagement.d.ts.map +1 -0
  66. package/dist/cjs-types/cli/lib/profiles.d.ts +59 -0
  67. package/dist/cjs-types/cli/lib/profiles.d.ts.map +1 -0
  68. package/dist/cjs-types/cli/lib/utils/utils.d.ts +5 -0
  69. package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
  70. package/dist/cjs-types/cli/login.d.ts +21 -0
  71. package/dist/cjs-types/cli/login.d.ts.map +1 -1
  72. package/dist/cjs-types/cli/logout.d.ts +8 -1
  73. package/dist/cjs-types/cli/logout.d.ts.map +1 -1
  74. package/dist/cjs-types/cli/mcp.d.ts.map +1 -1
  75. package/dist/cjs-types/cli/program.d.ts.map +1 -1
  76. package/dist/cjs-types/index.d.ts +1 -1
  77. package/dist/cjs-types/index.d.ts.map +1 -1
  78. package/dist/cli.bundle.cjs +2118 -1096
  79. package/dist/cli.bundle.cjs.map +4 -4
  80. package/dist/esm/bundler/context.js +7 -5
  81. package/dist/esm/bundler/context.js.map +2 -2
  82. package/dist/esm/bundler/fs.js +47 -6
  83. package/dist/esm/bundler/fs.js.map +2 -2
  84. package/dist/esm/cli/account.js +221 -0
  85. package/dist/esm/cli/account.js.map +7 -0
  86. package/dist/esm/cli/configure.js +15 -2
  87. package/dist/esm/cli/configure.js.map +2 -2
  88. package/dist/esm/cli/lib/deployKeyWarning.js +27 -0
  89. package/dist/esm/cli/lib/deployKeyWarning.js.map +7 -0
  90. package/dist/esm/cli/lib/deployment.js +2 -2
  91. package/dist/esm/cli/lib/deployment.js.map +2 -2
  92. package/dist/esm/cli/lib/deploymentSelection.js +35 -20
  93. package/dist/esm/cli/lib/deploymentSelection.js.map +2 -2
  94. package/dist/esm/cli/lib/envvars.js +35 -1
  95. package/dist/esm/cli/lib/envvars.js.map +2 -2
  96. package/dist/esm/cli/lib/fsUtils.js +1 -1
  97. package/dist/esm/cli/lib/fsUtils.js.map +2 -2
  98. package/dist/esm/cli/lib/generateDocs.js +1 -1
  99. package/dist/esm/cli/lib/generateDocs.js.map +1 -1
  100. package/dist/esm/cli/lib/login.js +36 -4
  101. package/dist/esm/cli/lib/login.js.map +2 -2
  102. package/dist/esm/cli/lib/mcp/requestContext.js +5 -1
  103. package/dist/esm/cli/lib/mcp/requestContext.js.map +2 -2
  104. package/dist/esm/cli/lib/profileManagement.js +179 -0
  105. package/dist/esm/cli/lib/profileManagement.js.map +7 -0
  106. package/dist/esm/cli/lib/profiles.js +288 -0
  107. package/dist/esm/cli/lib/profiles.js.map +7 -0
  108. package/dist/esm/cli/lib/utils/utils.js +16 -9
  109. package/dist/esm/cli/lib/utils/utils.js.map +2 -2
  110. package/dist/esm/cli/login.js +225 -112
  111. package/dist/esm/cli/login.js.map +3 -3
  112. package/dist/esm/cli/logout.js +110 -13
  113. package/dist/esm/cli/logout.js.map +2 -2
  114. package/dist/esm/cli/mcp.js +34 -10
  115. package/dist/esm/cli/mcp.js.map +2 -2
  116. package/dist/esm/cli/program.js +2 -1
  117. package/dist/esm/cli/program.js.map +2 -2
  118. package/dist/esm/index.js +1 -1
  119. package/dist/esm/index.js.map +1 -1
  120. package/dist/esm-types/bundler/context.d.ts +5 -0
  121. package/dist/esm-types/bundler/context.d.ts.map +1 -1
  122. package/dist/esm-types/bundler/fs.d.ts +7 -2
  123. package/dist/esm-types/bundler/fs.d.ts.map +1 -1
  124. package/dist/esm-types/cli/account.d.ts +6 -0
  125. package/dist/esm-types/cli/account.d.ts.map +1 -0
  126. package/dist/esm-types/cli/configure.d.ts.map +1 -1
  127. package/dist/esm-types/cli/lib/deployKeyWarning.d.ts +3 -0
  128. package/dist/esm-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
  129. package/dist/esm-types/cli/lib/deployment.d.ts +2 -0
  130. package/dist/esm-types/cli/lib/deployment.d.ts.map +1 -1
  131. package/dist/esm-types/cli/lib/deploymentSelection.d.ts +2 -1
  132. package/dist/esm-types/cli/lib/deploymentSelection.d.ts.map +1 -1
  133. package/dist/esm-types/cli/lib/envvars.d.ts +6 -0
  134. package/dist/esm-types/cli/lib/envvars.d.ts.map +1 -1
  135. package/dist/esm-types/cli/lib/fsUtils.d.ts +1 -0
  136. package/dist/esm-types/cli/lib/fsUtils.d.ts.map +1 -1
  137. package/dist/esm-types/cli/lib/login.d.ts +3 -1
  138. package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
  139. package/dist/esm-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
  140. package/dist/esm-types/cli/lib/mcp/tools/insights.d.ts +2 -2
  141. package/dist/esm-types/cli/lib/profileManagement.d.ts +10 -0
  142. package/dist/esm-types/cli/lib/profileManagement.d.ts.map +1 -0
  143. package/dist/esm-types/cli/lib/profiles.d.ts +59 -0
  144. package/dist/esm-types/cli/lib/profiles.d.ts.map +1 -0
  145. package/dist/esm-types/cli/lib/utils/utils.d.ts +5 -0
  146. package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
  147. package/dist/esm-types/cli/login.d.ts +21 -0
  148. package/dist/esm-types/cli/login.d.ts.map +1 -1
  149. package/dist/esm-types/cli/logout.d.ts +8 -1
  150. package/dist/esm-types/cli/logout.d.ts.map +1 -1
  151. package/dist/esm-types/cli/mcp.d.ts.map +1 -1
  152. package/dist/esm-types/cli/program.d.ts.map +1 -1
  153. package/dist/esm-types/index.d.ts +1 -1
  154. package/dist/esm-types/index.d.ts.map +1 -1
  155. package/dist/react.bundle.js +1 -1
  156. package/dist/react.bundle.js.map +1 -1
  157. package/package.json +1 -1
  158. package/src/bundler/context.ts +9 -5
  159. package/src/bundler/fs.ts +54 -5
  160. package/src/cli/account.ts +320 -0
  161. package/src/cli/configure.ts +18 -2
  162. package/src/cli/lib/deployKeyWarning.ts +43 -0
  163. package/src/cli/lib/deployment.ts +2 -2
  164. package/src/cli/lib/deploymentSelection.ts +37 -18
  165. package/src/cli/lib/envvars.ts +43 -1
  166. package/src/cli/lib/fsUtils.ts +5 -2
  167. package/src/cli/lib/generateDocs.ts +1 -1
  168. package/src/cli/lib/login.ts +47 -3
  169. package/src/cli/lib/mcp/requestContext.ts +7 -1
  170. package/src/cli/lib/profileManagement.ts +262 -0
  171. package/src/cli/lib/profiles.ts +398 -0
  172. package/src/cli/lib/utils/utils.ts +19 -15
  173. package/src/cli/login.ts +293 -168
  174. package/src/cli/logout.ts +142 -17
  175. package/src/cli/mcp.ts +48 -11
  176. package/src/cli/program.ts +2 -0
  177. package/src/index.ts +1 -1
@@ -31,18 +31,26 @@ import {
31
31
  targetProjectForLocalSelector,
32
32
  } from "./localDeployment/projectMismatch.js";
33
33
  import { chalkStderr } from "chalk";
34
- import { getBuildEnvironment } from "./envvars.js";
34
+ import {
35
+ getBuildEnvironment,
36
+ loadProjectEnvironmentVariables,
37
+ } from "./envvars.js";
35
38
  import { readGlobalConfig } from "./utils/globalConfig.js";
39
+ import {
40
+ getProfileSelection,
41
+ ProfileSelection,
42
+ readProfileAuth,
43
+ } from "./profiles.js";
36
44
  import {
37
45
  CONVEX_DEPLOYMENT_ENV_VAR_NAME,
38
46
  CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME,
39
47
  CONVEX_DEPLOY_KEY_ENV_VAR_NAME,
40
48
  CONVEX_SELF_HOSTED_ADMIN_KEY_VAR_NAME,
41
49
  CONVEX_SELF_HOSTED_URL_VAR_NAME,
42
- ENV_VAR_FILE_PATH,
43
50
  bigBrainAPI,
44
51
  processDeployKeyValue,
45
52
  readDeployKeyFromEnv,
53
+ readDeployKeyEnvVar,
46
54
  typedPlatformClient,
47
55
  } from "./utils/utils.js";
48
56
  import * as dotenv from "dotenv";
@@ -83,10 +91,11 @@ export async function initializeBigBrainAuth(
83
91
  if (initialArgs.url !== undefined && initialArgs.adminKey !== undefined) {
84
92
  // Do not check any env vars if `url` and `adminKey` are specified via CLI
85
93
  ctx._updateBigBrainAuth(
86
- getBigBrainAuth(ctx, {
94
+ await getBigBrainAuth(ctx, {
87
95
  previewDeployKey: null,
88
96
  projectKey: null,
89
97
  deploymentKey: null,
98
+ skipProfile: true,
90
99
  }),
91
100
  );
92
101
  return;
@@ -106,30 +115,31 @@ export async function initializeBigBrainAuth(
106
115
  const rawDeployKey = readDeployKeyFromEnv((name) => config[name]);
107
116
  const deployKey = await processDeployKeyValue(ctx, rawDeployKey);
108
117
  if (deployKey !== undefined) {
109
- const bigBrainAuth = getBigBrainAuth(ctx, {
118
+ const bigBrainAuth = await getBigBrainAuth(ctx, {
110
119
  previewDeployKey: isPreviewDeployKey(deployKey) ? deployKey : null,
111
120
  projectKey: isProjectKey(deployKey) ? deployKey : null,
112
121
  deploymentKey: isDeploymentKey(deployKey) ? deployKey : null,
122
+ envFile: initialArgs.envFile,
113
123
  });
114
124
  ctx._updateBigBrainAuth(bigBrainAuth);
115
125
  return;
116
126
  }
117
127
  // No deploy key was found in the env file, so fall back on using the global config
118
128
  ctx._updateBigBrainAuth(
119
- getBigBrainAuth(ctx, {
129
+ await getBigBrainAuth(ctx, {
120
130
  previewDeployKey: null,
121
131
  projectKey: null,
122
132
  deploymentKey: null,
133
+ envFile: initialArgs.envFile,
123
134
  }),
124
135
  );
125
136
  return;
126
137
  }
127
- dotenv.config({ path: ENV_VAR_FILE_PATH });
128
- dotenv.config();
138
+ loadProjectEnvironmentVariables();
129
139
  const rawDeployKey = readDeployKeyFromEnv((name) => process.env[name]);
130
140
  const deployKey = await processDeployKeyValue(ctx, rawDeployKey);
131
141
  if (deployKey !== undefined) {
132
- const bigBrainAuth = getBigBrainAuth(ctx, {
142
+ const bigBrainAuth = await getBigBrainAuth(ctx, {
133
143
  previewDeployKey: isPreviewDeployKey(deployKey) ? deployKey : null,
134
144
  projectKey: isProjectKey(deployKey) ? deployKey : null,
135
145
  deploymentKey: isDeploymentKey(deployKey) ? deployKey : null,
@@ -138,7 +148,7 @@ export async function initializeBigBrainAuth(
138
148
  return;
139
149
  }
140
150
  ctx._updateBigBrainAuth(
141
- getBigBrainAuth(ctx, {
151
+ await getBigBrainAuth(ctx, {
142
152
  previewDeployKey: null,
143
153
  projectKey: null,
144
154
  deploymentKey: null,
@@ -150,6 +160,7 @@ export async function initializeBigBrainAuth(
150
160
  export async function updateBigBrainAuthAfterLogin(
151
161
  ctx: Context,
152
162
  accessToken: string,
163
+ profile?: ProfileSelection,
153
164
  ) {
154
165
  const existingAuth = ctx.bigBrainAuth();
155
166
  if (existingAuth !== null && existingAuth.kind === "projectKey") {
@@ -162,6 +173,7 @@ export async function updateBigBrainAuthAfterLogin(
162
173
  accessToken: accessToken,
163
174
  kind: "accessToken",
164
175
  header: `Bearer ${accessToken}`,
176
+ ...(profile ? { profile } : {}),
165
177
  });
166
178
  }
167
179
 
@@ -169,19 +181,22 @@ export async function clearBigBrainAuth(ctx: Context) {
169
181
  ctx._updateBigBrainAuth(null);
170
182
  }
171
183
 
172
- function getBigBrainAuth(
184
+ async function getBigBrainAuth(
173
185
  ctx: Context,
174
186
  opts: {
175
187
  previewDeployKey: string | null;
176
188
  projectKey: string | null;
177
189
  deploymentKey: string | null;
190
+ envFile?: string;
191
+ skipProfile?: boolean;
178
192
  },
179
- ): BigBrainAuth | null {
193
+ ): Promise<BigBrainAuth | null> {
180
194
  if (process.env.CONVEX_OVERRIDE_ACCESS_TOKEN) {
181
195
  return {
182
196
  accessToken: process.env.CONVEX_OVERRIDE_ACCESS_TOKEN,
183
197
  kind: "accessToken",
184
198
  header: `Bearer ${process.env.CONVEX_OVERRIDE_ACCESS_TOKEN}`,
199
+ profile: null,
185
200
  };
186
201
  }
187
202
  if (opts.projectKey !== null) {
@@ -200,12 +215,19 @@ function getBigBrainAuth(
200
215
  deploymentKey: opts.deploymentKey,
201
216
  };
202
217
  }
218
+ const profile = opts.skipProfile
219
+ ? null
220
+ : await getProfileSelection(ctx, { envFile: opts.envFile });
221
+ if (profile !== null) {
222
+ return await readProfileAuth(ctx, profile);
223
+ }
203
224
  const globalConfig = readGlobalConfig(ctx);
204
225
  if (globalConfig) {
205
226
  return {
206
227
  kind: "accessToken",
207
228
  header: `Bearer ${globalConfig.accessToken}`,
208
229
  accessToken: globalConfig.accessToken,
230
+ profile: null,
209
231
  };
210
232
  }
211
233
  if (opts.previewDeployKey !== null) {
@@ -272,9 +294,9 @@ export async function ensureAuthCanCreateDeployment(
272
294
  )} and try again.`,
273
295
  });
274
296
  }
275
- const envVar = process.env[CONVEX_DEPLOY_KEY_ENV_VAR_NAME]
276
- ? CONVEX_DEPLOY_KEY_ENV_VAR_NAME
277
- : CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME;
297
+ const envVar =
298
+ readDeployKeyEnvVar((name) => process.env[name])?.name ??
299
+ CONVEX_DEPLOY_KEY_ENV_VAR_NAME;
278
300
  return await ctx.crash({
279
301
  exitCode: 1,
280
302
  errorType: "fatal",
@@ -569,10 +591,7 @@ async function resolveBaseDeploymentSelection(
569
591
  }
570
592
  return result.metadata;
571
593
  }
572
- // start with .env.local (but doesn't override existing)
573
- dotenv.config({ path: ENV_VAR_FILE_PATH });
574
- // for variables not already set, use .env values
575
- dotenv.config();
594
+ loadProjectEnvironmentVariables();
576
595
  const result = await getDeploymentSelectionFromEnv(
577
596
  ctx,
578
597
  selectionWithinProject,
@@ -6,7 +6,49 @@ import * as dotenv from "dotenv";
6
6
 
7
7
  import { Context } from "../../bundler/context.js";
8
8
  import { logWarning } from "../../bundler/log.js";
9
- import { loadPackageJson } from "./utils/utils.js";
9
+ import {
10
+ CONVEX_DEPLOY_KEY_ENV_VAR_NAME,
11
+ readDeployKeyEnvVar,
12
+ ENV_VAR_FILE_PATH,
13
+ loadPackageJson,
14
+ } from "./utils/utils.js";
15
+
16
+ export const CONVEX_ACCOUNT_ENV_VAR = "CONVEX_ACCOUNT";
17
+
18
+ const loadedEnvironment = new Map<string, { value: string; source: string }>();
19
+
20
+ export function loadProjectEnvironmentVariables() {
21
+ // Only remove values supplied by our previous load; later shell overrides win.
22
+ for (const [name, { value }] of loadedEnvironment) {
23
+ if (process.env[name] === value) delete process.env[name];
24
+ }
25
+ loadedEnvironment.clear();
26
+ const environment: Record<string, string> = {};
27
+ for (const [name, value] of Object.entries(process.env)) {
28
+ if (value !== undefined) environment[name] = value;
29
+ }
30
+ for (const file of [ENV_VAR_FILE_PATH, ".env"]) {
31
+ dotenv.config({ path: file, processEnv: environment });
32
+ for (const [name, value] of Object.entries(environment)) {
33
+ // Account selection reads the current project's files directly.
34
+ if (name !== CONVEX_ACCOUNT_ENV_VAR && process.env[name] === undefined) {
35
+ process.env[name] = value;
36
+ loadedEnvironment.set(name, { value, source: file });
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ export function getDeployKeySource(key: string) {
43
+ const envVarName =
44
+ readDeployKeyEnvVar((name) => process.env[name])?.name ??
45
+ CONVEX_DEPLOY_KEY_ENV_VAR_NAME;
46
+ const loaded = loadedEnvironment.get(envVarName);
47
+ return {
48
+ envVarName,
49
+ source: loaded?.value === key ? loaded.source : "shell environment",
50
+ };
51
+ }
10
52
 
11
53
  const _FRAMEWORKS = [
12
54
  "create-react-app",
@@ -6,12 +6,15 @@ import { NodeFs } from "../../bundler/fs.js";
6
6
  export function recursivelyDelete(
7
7
  ctx: Context,
8
8
  deletePath: string,
9
- opts?: { force?: boolean; dryRun?: boolean },
9
+ opts?: { force?: boolean; dryRun?: boolean; followSymlinks?: boolean },
10
10
  ) {
11
11
  const dryRun = !!opts?.dryRun;
12
12
  let st;
13
13
  try {
14
- st = ctx.fs.stat(deletePath);
14
+ st =
15
+ opts?.followSymlinks === false
16
+ ? ctx.fs.lstat(deletePath)
17
+ : ctx.fs.stat(deletePath);
15
18
  } catch (err: any) {
16
19
  if (err.code === "ENOENT" && opts?.force) {
17
20
  return;
@@ -299,7 +299,7 @@ function renderCopyableCommands(text: string): string {
299
299
  .map((line) =>
300
300
  /^- /.test(line)
301
301
  ? line.replace(
302
- /`(npx convex [^`]*)`/g,
302
+ /`(npx convex(?:@[^\s`]+)? [^`]*)`/g,
303
303
  (_, command) =>
304
304
  `<CodeWithCopyButton text={${JSON.stringify(command)}} />`,
305
305
  )
@@ -32,6 +32,17 @@ import {
32
32
  modifyGlobalConfig,
33
33
  } from "./utils/globalConfig.js";
34
34
  import { updateBigBrainAuthAfterLogin } from "./deploymentSelection.js";
35
+ import {
36
+ getProfileSelection,
37
+ ProfileSelection,
38
+ profileConfigPath,
39
+ writeProfileCredentials,
40
+ readProfileCredentials,
41
+ } from "./profiles.js";
42
+ import {
43
+ profileLoginMetadata,
44
+ revokeReplacedProfileToken,
45
+ } from "./profileManagement.js";
35
46
 
36
47
  // Per https://github.com/panva/node-openid-client/tree/main/docs#customizing
37
48
  custom.setHttpOptionsDefaults({
@@ -291,6 +302,7 @@ export async function performLogin(
291
302
  anonymousId,
292
303
  vercel,
293
304
  vercelOverride,
305
+ profile: selectedProfile,
294
306
  }: {
295
307
  overrideAuthUrl?: string | undefined;
296
308
  overrideAuthClient?: string | undefined;
@@ -307,8 +319,16 @@ export async function performLogin(
307
319
  anonymousId?: string | undefined;
308
320
  vercel?: boolean | undefined;
309
321
  vercelOverride?: string | undefined;
322
+ profile?: ProfileSelection | null;
310
323
  } = {},
311
324
  ) {
325
+ const auth = ctx.bigBrainAuth();
326
+ const profile =
327
+ selectedProfile !== undefined
328
+ ? selectedProfile
329
+ : auth?.kind === "accessToken" && auth.profile !== undefined
330
+ ? auth.profile
331
+ : await getProfileSelection(ctx);
312
332
  loginFlow = loginFlow || "auto";
313
333
  // Get access token from big-brain
314
334
  // Default the device name to the hostname, but allow the user to change this if the terminal is interactive.
@@ -400,6 +420,12 @@ export async function performLogin(
400
420
  }
401
421
 
402
422
  // Exchange the WorkOS access token for a Convex personal access token.
423
+ const previous =
424
+ profile === null ? null : await readProfileCredentials(ctx, profile, true);
425
+ const metadata =
426
+ profile === null
427
+ ? {}
428
+ : await profileLoginMetadata(ctx, profile, accessToken, previous);
403
429
  ctx._updateBigBrainAuth({
404
430
  accessToken: accessToken,
405
431
  kind: "accessToken",
@@ -417,8 +443,19 @@ export async function performLogin(
417
443
  const newAccessToken = response.data!.accessToken;
418
444
  const globalConfig = { accessToken: newAccessToken };
419
445
  try {
420
- await modifyGlobalConfig(ctx, globalConfig);
421
- const path = globalConfigPath();
446
+ if (profile !== null) {
447
+ await writeProfileCredentials(
448
+ ctx,
449
+ profile.name,
450
+ newAccessToken,
451
+ metadata,
452
+ previous,
453
+ );
454
+ } else {
455
+ await modifyGlobalConfig(ctx, globalConfig);
456
+ }
457
+ const path =
458
+ profile !== null ? profileConfigPath(profile.name) : globalConfigPath();
422
459
  logFinishedStep(`Saved credentials to ${formatPathForPrinting(path)}`);
423
460
  } catch (err: unknown) {
424
461
  return await ctx.crash({
@@ -429,8 +466,15 @@ export async function performLogin(
429
466
  });
430
467
  }
431
468
 
469
+ if (profile !== null) {
470
+ await revokeReplacedProfileToken(
471
+ ctx,
472
+ previous?.accessToken,
473
+ newAccessToken,
474
+ );
475
+ }
432
476
  logVerbose(`performLogin: updating big brain auth after login`);
433
- await updateBigBrainAuthAfterLogin(ctx, newAccessToken);
477
+ await updateBigBrainAuthAfterLogin(ctx, newAccessToken, profile ?? undefined);
434
478
 
435
479
  logVerbose(`performLogin: checking opt ins, acceptOptIns: ${acceptOptIns}`);
436
480
  // Do opt in to TOS and Privacy Policy stuff
@@ -149,7 +149,13 @@ export function encodeDeploymentSelector(
149
149
  projectDir,
150
150
  deployment,
151
151
  };
152
- return `${deployment.kind}:${btoa(JSON.stringify(payload))}`;
152
+ // ASCII JSON escapes support Unicode paths with the existing Latin-1 decoder.
153
+ const serialized = JSON.stringify(payload).replace(
154
+ /[\u0080-\uffff]/g,
155
+ (character) =>
156
+ `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
157
+ );
158
+ return `${deployment.kind}:${btoa(serialized)}`;
153
159
  }
154
160
 
155
161
  const payloadSchema = z.object({
@@ -0,0 +1,262 @@
1
+ import { ProjectSelection } from "./deploymentSelection.js";
2
+ import { Context } from "../../bundler/context.js";
3
+ import { logMessage, logWarning } from "../../bundler/log.js";
4
+ import { provisionHost } from "./config.js";
5
+ import { version } from "../version.js";
6
+ import {
7
+ isAnonymousDeployment,
8
+ getDeploymentTypeFromConfiguredDeployment,
9
+ stripDeploymentTypePrefix,
10
+ } from "./deployment.js";
11
+ import { CONVEX_DEPLOYMENT_ENV_VAR_NAME } from "./utils/utils.js";
12
+ import { promptYesNo } from "./utils/prompts.js";
13
+ import { loadProjectEnvironmentVariables } from "./envvars.js";
14
+ import {
15
+ ProfileCredentials,
16
+ ProfileSelection,
17
+ readProfileCredentials,
18
+ readProfileSettings,
19
+ readProfileToken,
20
+ savedProfileNames,
21
+ writeProfileDirectory,
22
+ } from "./profiles.js";
23
+
24
+ export async function profileApi(
25
+ token: string,
26
+ pathname: string,
27
+ body?: unknown,
28
+ ) {
29
+ return await fetch(`${provisionHost}${pathname}`, {
30
+ method: body === undefined ? "GET" : "POST",
31
+ headers: {
32
+ Authorization: `Bearer ${token}`,
33
+ "Convex-Client": `npm-cli-${version}`,
34
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
35
+ },
36
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
37
+ signal: AbortSignal.timeout(10_000),
38
+ });
39
+ }
40
+
41
+ export async function deploymentAccess(
42
+ token: string,
43
+ deploymentName: string,
44
+ ): Promise<"yes" | "no" | "unknown"> {
45
+ try {
46
+ const response = await profileApi(
47
+ token,
48
+ `/v1/deployments/${encodeURIComponent(deploymentName)}`,
49
+ );
50
+ if (response.ok) return "yes";
51
+ return [401, 403, 404].includes(response.status) ? "no" : "unknown";
52
+ } catch {
53
+ return "unknown";
54
+ }
55
+ }
56
+
57
+ export async function projectAccess(
58
+ token: string,
59
+ target: ProjectSelection,
60
+ ): Promise<"yes" | "no" | "unknown"> {
61
+ if (target.kind === "deploymentName")
62
+ return deploymentAccess(token, target.deploymentName);
63
+ if (target.kind !== "teamAndProjectSlugs") return "unknown";
64
+ try {
65
+ const response = await profileApi(
66
+ token,
67
+ `/v1/teams/${encodeURIComponent(target.teamSlug)}/projects/${encodeURIComponent(target.projectSlug)}`,
68
+ );
69
+ if (response.ok) return "yes";
70
+ return [401, 403, 404].includes(response.status) ? "no" : "unknown";
71
+ } catch {
72
+ return "unknown";
73
+ }
74
+ }
75
+
76
+ function configuredCloudDeployment(): string | null {
77
+ const configured = process.env[CONVEX_DEPLOYMENT_ENV_VAR_NAME]?.trim();
78
+ if (
79
+ !configured ||
80
+ getDeploymentTypeFromConfiguredDeployment(configured) === "local"
81
+ )
82
+ return null;
83
+ const name = stripDeploymentTypePrefix(configured);
84
+ return isAnonymousDeployment(name) ? null : name;
85
+ }
86
+
87
+ export async function bindProfileToDirectory(
88
+ ctx: Context,
89
+ name: string,
90
+ force = false,
91
+ target?: ProjectSelection,
92
+ ): Promise<boolean> {
93
+ const credentials = await readProfileCredentials(ctx, {
94
+ name,
95
+ source: "account selection",
96
+ });
97
+ loadProjectEnvironmentVariables();
98
+ const deployment = configuredCloudDeployment();
99
+ const selectedTarget =
100
+ target ??
101
+ (deployment === null
102
+ ? null
103
+ : {
104
+ kind: "deploymentName" as const,
105
+ deploymentName: deployment,
106
+ deploymentType: null,
107
+ });
108
+ if (!force && selectedTarget !== null) {
109
+ const label =
110
+ selectedTarget.kind === "teamAndProjectSlugs"
111
+ ? `project "${selectedTarget.teamSlug}/${selectedTarget.projectSlug}"`
112
+ : `deployment "${selectedTarget.kind === "deploymentName" ? selectedTarget.deploymentName : "configured"}"`;
113
+ const access = await projectAccess(
114
+ credentials!.accessToken,
115
+ selectedTarget,
116
+ );
117
+ if (access !== "yes") {
118
+ logWarning(
119
+ access === "no"
120
+ ? `Account "${name}" cannot access ${label}.`
121
+ : `Could not verify whether account "${name}" can access ${label}.`,
122
+ );
123
+ if (
124
+ !process.stdin.isTTY ||
125
+ !(await promptYesNo(ctx, {
126
+ message: `Use account "${name}" here anyway?`,
127
+ default: false,
128
+ }))
129
+ ) {
130
+ logMessage(
131
+ `Directory selection unchanged. To select this account anyway, run \`npx convex account select ${name} --force\`.`,
132
+ );
133
+ return false;
134
+ }
135
+ }
136
+ }
137
+ await writeProfileDirectory(ctx, name);
138
+ return true;
139
+ }
140
+
141
+ export async function profileLoginMetadata(
142
+ ctx: Context,
143
+ profile: ProfileSelection,
144
+ loginToken: string,
145
+ previous: ProfileCredentials | null,
146
+ ): Promise<Omit<ProfileCredentials, "accessToken">> {
147
+ let identity: { memberId: string; email: string } | null = null;
148
+ try {
149
+ const response = await profileApi(loginToken, "/api/dashboard/profile");
150
+ if (response.ok) {
151
+ const data = await response.json();
152
+ if (
153
+ (typeof data.id === "number" || typeof data.id === "string") &&
154
+ typeof data.email === "string"
155
+ ) {
156
+ identity = { memberId: String(data.id), email: data.email };
157
+ }
158
+ }
159
+ } catch {
160
+ // Older authentication tokens can be valid without access to the dashboard profile API.
161
+ }
162
+ const sameAccount =
163
+ previous !== null && (previous.memberId === undefined || identity === null)
164
+ ? await tokensBelongToSameAccount(previous.accessToken, loginToken)
165
+ : null;
166
+ if (
167
+ previous !== null &&
168
+ (previous.memberId === undefined || identity === null
169
+ ? sameAccount !== true
170
+ : identity.memberId !== previous.memberId)
171
+ ) {
172
+ return await ctx.crash({
173
+ exitCode: 1,
174
+ errorType: "fatal",
175
+ printedMessage:
176
+ sameAccount === null &&
177
+ (previous.memberId === undefined || identity === null)
178
+ ? `Could not verify the identity of saved account "${profile.name}". To replace this saved login, run \`npx convex logout --account ${profile.name}\` followed by \`npx convex login --account ${profile.name}\`. Choose the intended account when logging in.`
179
+ : `The login does not match saved account "${profile.name}". Log in with the matching account, or choose another name with \`npx convex login --account <name>\`.`,
180
+ });
181
+ }
182
+ return {
183
+ ...(identity ??
184
+ (previous?.memberId === undefined
185
+ ? {}
186
+ : { memberId: previous.memberId, email: previous.email })),
187
+ };
188
+ }
189
+
190
+ async function tokensBelongToSameAccount(
191
+ older: string,
192
+ newer: string,
193
+ ): Promise<boolean | null> {
194
+ try {
195
+ const responses = await Promise.all(
196
+ [older, newer].map((token) =>
197
+ profileApi(token, "/v1/list_personal_access_tokens?limit=100"),
198
+ ),
199
+ );
200
+ if (responses.some((response) => !response.ok)) return null;
201
+ const lists = await Promise.all(
202
+ responses.map((response) => response.json()),
203
+ );
204
+ if (
205
+ lists.some(
206
+ (list) => !Array.isArray(list.items) || list.items.length === 0,
207
+ )
208
+ )
209
+ return null;
210
+ const ids = new Set(
211
+ lists[0].items.map((token: { id: unknown }) => String(token.id)),
212
+ );
213
+ if (
214
+ lists[1].items.some((token: { id: unknown }) => ids.has(String(token.id)))
215
+ )
216
+ return true;
217
+ // Disjoint partial pages cannot establish that the accounts differ.
218
+ return lists.some((list) => list.pagination?.hasMore !== false)
219
+ ? null
220
+ : false;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ export async function revokeReplacedProfileToken(
227
+ ctx: Context,
228
+ previous: string | undefined,
229
+ accessToken: string,
230
+ ) {
231
+ if (previous === undefined || previous === accessToken) return;
232
+ const settings = await readProfileSettings(ctx);
233
+ if (
234
+ settings.revokeReplacedTokens === false ||
235
+ settings.accessToken === previous
236
+ )
237
+ return;
238
+ for (const name of savedProfileNames(ctx)) {
239
+ const token = readProfileToken(ctx, name);
240
+ if (token === null) {
241
+ logWarning(
242
+ `The new login was saved, but account "${name}" could not be read. Skipping revocation of the replaced token.`,
243
+ );
244
+ return;
245
+ }
246
+ if (token === previous) return;
247
+ }
248
+ try {
249
+ // The API verifies token ownership using the newly authenticated account.
250
+ const response = await profileApi(
251
+ accessToken,
252
+ "/v1/delete_personal_access_token",
253
+ { id: previous },
254
+ );
255
+ if (response.ok) return;
256
+ } catch {
257
+ // The new credentials are already saved, so a failed revocation can be retried in the dashboard.
258
+ }
259
+ logWarning(
260
+ "The new login was saved, but the replaced token could not be revoked. You can remove it in the dashboard's account settings.",
261
+ );
262
+ }