convex 1.48.0 → 1.49.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 (248) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/browser.bundle.js +1 -1
  3. package/dist/browser.bundle.js.map +1 -1
  4. package/dist/cjs/bundler/context.js +7 -5
  5. package/dist/cjs/bundler/context.js.map +2 -2
  6. package/dist/cjs/bundler/fs.js +47 -6
  7. package/dist/cjs/bundler/fs.js.map +2 -2
  8. package/dist/cjs/cli/account.js +237 -0
  9. package/dist/cjs/cli/account.js.map +7 -0
  10. package/dist/cjs/cli/configure.js +15 -2
  11. package/dist/cjs/cli/configure.js.map +2 -2
  12. package/dist/cjs/cli/lib/accountManagement.js +196 -0
  13. package/dist/cjs/cli/lib/accountManagement.js.map +7 -0
  14. package/dist/cjs/cli/lib/accounts.js +334 -0
  15. package/dist/cjs/cli/lib/accounts.js.map +7 -0
  16. package/dist/cjs/cli/lib/deployKeyWarning.js +43 -0
  17. package/dist/cjs/cli/lib/deployKeyWarning.js.map +7 -0
  18. package/dist/cjs/cli/lib/deployment.js +2 -0
  19. package/dist/cjs/cli/lib/deployment.js.map +2 -2
  20. package/dist/cjs/cli/lib/deploymentSelection.js +27 -18
  21. package/dist/cjs/cli/lib/deploymentSelection.js.map +2 -2
  22. package/dist/cjs/cli/lib/envvars.js +32 -0
  23. package/dist/cjs/cli/lib/envvars.js.map +2 -2
  24. package/dist/cjs/cli/lib/fsUtils.js +1 -1
  25. package/dist/cjs/cli/lib/fsUtils.js.map +2 -2
  26. package/dist/cjs/cli/lib/generateDocs.js +1 -1
  27. package/dist/cjs/cli/lib/generateDocs.js.map +1 -1
  28. package/dist/cjs/cli/lib/login.js +42 -4
  29. package/dist/cjs/cli/lib/login.js.map +3 -3
  30. package/dist/cjs/cli/lib/mcp/requestContext.js +5 -1
  31. package/dist/cjs/cli/lib/mcp/requestContext.js.map +2 -2
  32. package/dist/cjs/cli/lib/plugins/claudeCode.js +188 -0
  33. package/dist/cjs/cli/lib/plugins/claudeCode.js.map +7 -0
  34. package/dist/cjs/cli/lib/plugins/codex.js +167 -0
  35. package/dist/cjs/cli/lib/plugins/codex.js.map +7 -0
  36. package/dist/cjs/cli/lib/plugins/harness.js +50 -0
  37. package/dist/cjs/cli/lib/plugins/harness.js.map +7 -0
  38. package/dist/cjs/cli/lib/plugins/marketplaces.js +76 -0
  39. package/dist/cjs/cli/lib/plugins/marketplaces.js.map +7 -0
  40. package/dist/cjs/cli/lib/plugins/skills.js +98 -0
  41. package/dist/cjs/cli/lib/plugins/skills.js.map +7 -0
  42. package/dist/cjs/cli/lib/plugins/utils.js +125 -0
  43. package/dist/cjs/cli/lib/plugins/utils.js.map +7 -0
  44. package/dist/cjs/cli/lib/utils/utils.js +18 -9
  45. package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
  46. package/dist/cjs/cli/login.js +213 -114
  47. package/dist/cjs/cli/login.js.map +3 -3
  48. package/dist/cjs/cli/logout.js +113 -12
  49. package/dist/cjs/cli/logout.js.map +3 -3
  50. package/dist/cjs/cli/mcp.js +43 -10
  51. package/dist/cjs/cli/mcp.js.map +3 -3
  52. package/dist/cjs/cli/plugins.js +114 -0
  53. package/dist/cjs/cli/plugins.js.map +7 -0
  54. package/dist/cjs/cli/program.js +3 -1
  55. package/dist/cjs/cli/program.js.map +2 -2
  56. package/dist/cjs/index.js +1 -1
  57. package/dist/cjs/index.js.map +1 -1
  58. package/dist/cjs/server/impl/actions_impl.js.map +2 -2
  59. package/dist/cjs-types/bundler/context.d.ts +5 -0
  60. package/dist/cjs-types/bundler/context.d.ts.map +1 -1
  61. package/dist/cjs-types/bundler/fs.d.ts +7 -2
  62. package/dist/cjs-types/bundler/fs.d.ts.map +1 -1
  63. package/dist/cjs-types/cli/account.d.ts +6 -0
  64. package/dist/cjs-types/cli/account.d.ts.map +1 -0
  65. package/dist/cjs-types/cli/configure.d.ts.map +1 -1
  66. package/dist/cjs-types/cli/lib/accountManagement.d.ts +10 -0
  67. package/dist/cjs-types/cli/lib/accountManagement.d.ts.map +1 -0
  68. package/dist/cjs-types/cli/lib/accounts.d.ts +59 -0
  69. package/dist/cjs-types/cli/lib/accounts.d.ts.map +1 -0
  70. package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts +3 -0
  71. package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
  72. package/dist/cjs-types/cli/lib/deployment.d.ts +2 -0
  73. package/dist/cjs-types/cli/lib/deployment.d.ts.map +1 -1
  74. package/dist/cjs-types/cli/lib/deploymentSelection.d.ts +2 -1
  75. package/dist/cjs-types/cli/lib/deploymentSelection.d.ts.map +1 -1
  76. package/dist/cjs-types/cli/lib/envvars.d.ts +6 -0
  77. package/dist/cjs-types/cli/lib/envvars.d.ts.map +1 -1
  78. package/dist/cjs-types/cli/lib/fsUtils.d.ts +1 -0
  79. package/dist/cjs-types/cli/lib/fsUtils.d.ts.map +1 -1
  80. package/dist/cjs-types/cli/lib/login.d.ts +3 -1
  81. package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
  82. package/dist/cjs-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
  83. package/dist/cjs-types/cli/lib/mcp/tools/insights.d.ts +2 -2
  84. package/dist/cjs-types/cli/lib/plugins/claudeCode.d.ts +25 -0
  85. package/dist/cjs-types/cli/lib/plugins/claudeCode.d.ts.map +1 -0
  86. package/dist/cjs-types/cli/lib/plugins/codex.d.ts +25 -0
  87. package/dist/cjs-types/cli/lib/plugins/codex.d.ts.map +1 -0
  88. package/dist/cjs-types/cli/lib/plugins/harness.d.ts +33 -0
  89. package/dist/cjs-types/cli/lib/plugins/harness.d.ts.map +1 -0
  90. package/dist/cjs-types/cli/lib/plugins/marketplaces.d.ts +40 -0
  91. package/dist/cjs-types/cli/lib/plugins/marketplaces.d.ts.map +1 -0
  92. package/dist/cjs-types/cli/lib/plugins/skills.d.ts +18 -0
  93. package/dist/cjs-types/cli/lib/plugins/skills.d.ts.map +1 -0
  94. package/dist/cjs-types/cli/lib/plugins/utils.d.ts +21 -0
  95. package/dist/cjs-types/cli/lib/plugins/utils.d.ts.map +1 -0
  96. package/dist/cjs-types/cli/lib/utils/utils.d.ts +5 -0
  97. package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
  98. package/dist/cjs-types/cli/login.d.ts +21 -0
  99. package/dist/cjs-types/cli/login.d.ts.map +1 -1
  100. package/dist/cjs-types/cli/logout.d.ts +8 -1
  101. package/dist/cjs-types/cli/logout.d.ts.map +1 -1
  102. package/dist/cjs-types/cli/mcp.d.ts.map +1 -1
  103. package/dist/cjs-types/cli/plugins.d.ts +3 -0
  104. package/dist/cjs-types/cli/plugins.d.ts.map +1 -0
  105. package/dist/cjs-types/cli/program.d.ts.map +1 -1
  106. package/dist/cjs-types/index.d.ts +1 -1
  107. package/dist/cjs-types/index.d.ts.map +1 -1
  108. package/dist/cjs-types/server/impl/actions_impl.d.ts +3 -2
  109. package/dist/cjs-types/server/impl/actions_impl.d.ts.map +1 -1
  110. package/dist/cli.bundle.cjs +10059 -7094
  111. package/dist/cli.bundle.cjs.map +4 -4
  112. package/dist/esm/bundler/context.js +7 -5
  113. package/dist/esm/bundler/context.js.map +2 -2
  114. package/dist/esm/bundler/fs.js +47 -6
  115. package/dist/esm/bundler/fs.js.map +2 -2
  116. package/dist/esm/cli/account.js +221 -0
  117. package/dist/esm/cli/account.js.map +7 -0
  118. package/dist/esm/cli/configure.js +15 -2
  119. package/dist/esm/cli/configure.js.map +2 -2
  120. package/dist/esm/cli/lib/accountManagement.js +179 -0
  121. package/dist/esm/cli/lib/accountManagement.js.map +7 -0
  122. package/dist/esm/cli/lib/accounts.js +289 -0
  123. package/dist/esm/cli/lib/accounts.js.map +7 -0
  124. package/dist/esm/cli/lib/deployKeyWarning.js +27 -0
  125. package/dist/esm/cli/lib/deployKeyWarning.js.map +7 -0
  126. package/dist/esm/cli/lib/deployment.js +2 -2
  127. package/dist/esm/cli/lib/deployment.js.map +2 -2
  128. package/dist/esm/cli/lib/deploymentSelection.js +35 -20
  129. package/dist/esm/cli/lib/deploymentSelection.js.map +2 -2
  130. package/dist/esm/cli/lib/envvars.js +35 -1
  131. package/dist/esm/cli/lib/envvars.js.map +2 -2
  132. package/dist/esm/cli/lib/fsUtils.js +1 -1
  133. package/dist/esm/cli/lib/fsUtils.js.map +2 -2
  134. package/dist/esm/cli/lib/generateDocs.js +1 -1
  135. package/dist/esm/cli/lib/generateDocs.js.map +1 -1
  136. package/dist/esm/cli/lib/login.js +52 -4
  137. package/dist/esm/cli/lib/login.js.map +3 -3
  138. package/dist/esm/cli/lib/mcp/requestContext.js +5 -1
  139. package/dist/esm/cli/lib/mcp/requestContext.js.map +2 -2
  140. package/dist/esm/cli/lib/plugins/claudeCode.js +160 -0
  141. package/dist/esm/cli/lib/plugins/claudeCode.js.map +7 -0
  142. package/dist/esm/cli/lib/plugins/codex.js +140 -0
  143. package/dist/esm/cli/lib/plugins/codex.js.map +7 -0
  144. package/dist/esm/cli/lib/plugins/harness.js +17 -0
  145. package/dist/esm/cli/lib/plugins/harness.js.map +7 -0
  146. package/dist/esm/cli/lib/plugins/marketplaces.js +53 -0
  147. package/dist/esm/cli/lib/plugins/marketplaces.js.map +7 -0
  148. package/dist/esm/cli/lib/plugins/skills.js +65 -0
  149. package/dist/esm/cli/lib/plugins/skills.js.map +7 -0
  150. package/dist/esm/cli/lib/plugins/utils.js +86 -0
  151. package/dist/esm/cli/lib/plugins/utils.js.map +7 -0
  152. package/dist/esm/cli/lib/utils/utils.js +16 -9
  153. package/dist/esm/cli/lib/utils/utils.js.map +2 -2
  154. package/dist/esm/cli/login.js +225 -112
  155. package/dist/esm/cli/login.js.map +3 -3
  156. package/dist/esm/cli/logout.js +110 -13
  157. package/dist/esm/cli/logout.js.map +2 -2
  158. package/dist/esm/cli/mcp.js +34 -10
  159. package/dist/esm/cli/mcp.js.map +2 -2
  160. package/dist/esm/cli/plugins.js +99 -0
  161. package/dist/esm/cli/plugins.js.map +7 -0
  162. package/dist/esm/cli/program.js +3 -1
  163. package/dist/esm/cli/program.js.map +2 -2
  164. package/dist/esm/index.js +1 -1
  165. package/dist/esm/index.js.map +1 -1
  166. package/dist/esm/server/impl/actions_impl.js.map +2 -2
  167. package/dist/esm-types/bundler/context.d.ts +5 -0
  168. package/dist/esm-types/bundler/context.d.ts.map +1 -1
  169. package/dist/esm-types/bundler/fs.d.ts +7 -2
  170. package/dist/esm-types/bundler/fs.d.ts.map +1 -1
  171. package/dist/esm-types/cli/account.d.ts +6 -0
  172. package/dist/esm-types/cli/account.d.ts.map +1 -0
  173. package/dist/esm-types/cli/configure.d.ts.map +1 -1
  174. package/dist/esm-types/cli/lib/accountManagement.d.ts +10 -0
  175. package/dist/esm-types/cli/lib/accountManagement.d.ts.map +1 -0
  176. package/dist/esm-types/cli/lib/accounts.d.ts +59 -0
  177. package/dist/esm-types/cli/lib/accounts.d.ts.map +1 -0
  178. package/dist/esm-types/cli/lib/deployKeyWarning.d.ts +3 -0
  179. package/dist/esm-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
  180. package/dist/esm-types/cli/lib/deployment.d.ts +2 -0
  181. package/dist/esm-types/cli/lib/deployment.d.ts.map +1 -1
  182. package/dist/esm-types/cli/lib/deploymentSelection.d.ts +2 -1
  183. package/dist/esm-types/cli/lib/deploymentSelection.d.ts.map +1 -1
  184. package/dist/esm-types/cli/lib/envvars.d.ts +6 -0
  185. package/dist/esm-types/cli/lib/envvars.d.ts.map +1 -1
  186. package/dist/esm-types/cli/lib/fsUtils.d.ts +1 -0
  187. package/dist/esm-types/cli/lib/fsUtils.d.ts.map +1 -1
  188. package/dist/esm-types/cli/lib/login.d.ts +3 -1
  189. package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
  190. package/dist/esm-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
  191. package/dist/esm-types/cli/lib/mcp/tools/insights.d.ts +2 -2
  192. package/dist/esm-types/cli/lib/plugins/claudeCode.d.ts +25 -0
  193. package/dist/esm-types/cli/lib/plugins/claudeCode.d.ts.map +1 -0
  194. package/dist/esm-types/cli/lib/plugins/codex.d.ts +25 -0
  195. package/dist/esm-types/cli/lib/plugins/codex.d.ts.map +1 -0
  196. package/dist/esm-types/cli/lib/plugins/harness.d.ts +33 -0
  197. package/dist/esm-types/cli/lib/plugins/harness.d.ts.map +1 -0
  198. package/dist/esm-types/cli/lib/plugins/marketplaces.d.ts +40 -0
  199. package/dist/esm-types/cli/lib/plugins/marketplaces.d.ts.map +1 -0
  200. package/dist/esm-types/cli/lib/plugins/skills.d.ts +18 -0
  201. package/dist/esm-types/cli/lib/plugins/skills.d.ts.map +1 -0
  202. package/dist/esm-types/cli/lib/plugins/utils.d.ts +21 -0
  203. package/dist/esm-types/cli/lib/plugins/utils.d.ts.map +1 -0
  204. package/dist/esm-types/cli/lib/utils/utils.d.ts +5 -0
  205. package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
  206. package/dist/esm-types/cli/login.d.ts +21 -0
  207. package/dist/esm-types/cli/login.d.ts.map +1 -1
  208. package/dist/esm-types/cli/logout.d.ts +8 -1
  209. package/dist/esm-types/cli/logout.d.ts.map +1 -1
  210. package/dist/esm-types/cli/mcp.d.ts.map +1 -1
  211. package/dist/esm-types/cli/plugins.d.ts +3 -0
  212. package/dist/esm-types/cli/plugins.d.ts.map +1 -0
  213. package/dist/esm-types/cli/program.d.ts.map +1 -1
  214. package/dist/esm-types/index.d.ts +1 -1
  215. package/dist/esm-types/index.d.ts.map +1 -1
  216. package/dist/esm-types/server/impl/actions_impl.d.ts +3 -2
  217. package/dist/esm-types/server/impl/actions_impl.d.ts.map +1 -1
  218. package/dist/react.bundle.js +1 -1
  219. package/dist/react.bundle.js.map +1 -1
  220. package/package.json +2 -2
  221. package/src/bundler/context.ts +9 -5
  222. package/src/bundler/fs.ts +54 -5
  223. package/src/cli/account.ts +320 -0
  224. package/src/cli/configure.ts +18 -2
  225. package/src/cli/lib/accountManagement.ts +262 -0
  226. package/src/cli/lib/accounts.ts +400 -0
  227. package/src/cli/lib/deployKeyWarning.ts +43 -0
  228. package/src/cli/lib/deployment.ts +2 -2
  229. package/src/cli/lib/deploymentSelection.ts +37 -18
  230. package/src/cli/lib/envvars.ts +43 -1
  231. package/src/cli/lib/fsUtils.ts +5 -2
  232. package/src/cli/lib/generateDocs.ts +1 -1
  233. package/src/cli/lib/login.ts +64 -3
  234. package/src/cli/lib/mcp/requestContext.ts +7 -1
  235. package/src/cli/lib/plugins/claudeCode.ts +201 -0
  236. package/src/cli/lib/plugins/codex.ts +177 -0
  237. package/src/cli/lib/plugins/harness.ts +49 -0
  238. package/src/cli/lib/plugins/marketplaces.ts +94 -0
  239. package/src/cli/lib/plugins/skills.ts +101 -0
  240. package/src/cli/lib/plugins/utils.ts +121 -0
  241. package/src/cli/lib/utils/utils.ts +19 -15
  242. package/src/cli/login.ts +293 -168
  243. package/src/cli/logout.ts +142 -17
  244. package/src/cli/mcp.ts +48 -11
  245. package/src/cli/plugins.ts +154 -0
  246. package/src/cli/program.ts +4 -0
  247. package/src/index.ts +1 -1
  248. package/src/server/impl/actions_impl.ts +3 -2
@@ -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
+ AccountCredentials,
16
+ AccountSelection,
17
+ readAccountCredentials,
18
+ readAccountSettings,
19
+ readAccountToken,
20
+ savedAccountNames,
21
+ writeAccountDirectory,
22
+ } from "./accounts.js";
23
+
24
+ export async function accountApi(
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 accountApi(
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 accountApi(
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 bindAccountToDirectory(
88
+ ctx: Context,
89
+ name: string,
90
+ force = false,
91
+ target?: ProjectSelection,
92
+ ): Promise<boolean> {
93
+ const credentials = await readAccountCredentials(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 writeAccountDirectory(ctx, name);
138
+ return true;
139
+ }
140
+
141
+ export async function accountLoginMetadata(
142
+ ctx: Context,
143
+ account: AccountSelection,
144
+ loginToken: string,
145
+ previous: AccountCredentials | null,
146
+ ): Promise<Omit<AccountCredentials, "accessToken">> {
147
+ let identity: { memberId: string; email: string } | null = null;
148
+ try {
149
+ const response = await accountApi(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 "${account.name}". To replace this saved login, run \`npx convex logout --account ${account.name}\` followed by \`npx convex login --account ${account.name}\`. Choose the intended account when logging in.`
179
+ : `The login does not match saved account "${account.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
+ accountApi(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 revokeReplacedAccountToken(
227
+ ctx: Context,
228
+ previous: string | undefined,
229
+ accessToken: string,
230
+ ) {
231
+ if (previous === undefined || previous === accessToken) return;
232
+ const settings = await readAccountSettings(ctx);
233
+ if (
234
+ settings.revokeReplacedTokens === false ||
235
+ settings.accessToken === previous
236
+ )
237
+ return;
238
+ for (const name of savedAccountNames(ctx)) {
239
+ const token = readAccountToken(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 accountApi(
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
+ }
@@ -0,0 +1,400 @@
1
+ import path from "path";
2
+ import { isDeepStrictEqual } from "util";
3
+ import { randomUUID } from "crypto";
4
+ import { TempPath } from "../../bundler/fs.js";
5
+ import * as dotenv from "dotenv";
6
+ import { z } from "zod";
7
+ import { BigBrainAuth, Context } from "../../bundler/context.js";
8
+ import { logFinishedStep, logWarning } from "../../bundler/log.js";
9
+ import { globalConfigPath, readGlobalConfig } from "./utils/globalConfig.js";
10
+ import { ENV_VAR_FILE_PATH, rootDirectory } from "./utils/utils.js";
11
+
12
+ import { CONVEX_ACCOUNT_ENV_VAR } from "./envvars.js";
13
+
14
+ export type AccountSelection = {
15
+ name: string;
16
+ source: string;
17
+ };
18
+
19
+ const settingsSchema = z
20
+ .object({
21
+ defaultAccount: z.string().optional(),
22
+ directories: z.record(z.string()).optional(),
23
+ revokeReplacedTokens: z.boolean().optional(),
24
+ })
25
+ .passthrough();
26
+
27
+ const credentialsSchema = z
28
+ .object({
29
+ accessToken: z.string().min(1),
30
+ memberId: z.string().optional(),
31
+ email: z.string().optional(),
32
+ })
33
+ .passthrough();
34
+
35
+ export type AccountCredentials = z.infer<typeof credentialsSchema>;
36
+
37
+ export async function readAccountSettings(
38
+ ctx: Context,
39
+ ): Promise<z.infer<typeof settingsSchema>> {
40
+ if (!ctx.fs.exists(globalConfigPath())) return {};
41
+ try {
42
+ return settingsSchema.parse(
43
+ JSON.parse(ctx.fs.readUtf8File(globalConfigPath())),
44
+ );
45
+ } catch {
46
+ return await ctx.crash({
47
+ exitCode: 1,
48
+ errorType: "invalid filesystem data",
49
+ printedMessage: `Unable to read account settings from ${globalConfigPath()}. Check defaultAccount, directories, and revokeReplacedTokens.`,
50
+ });
51
+ }
52
+ }
53
+
54
+ function writeAccountSettings(ctx: Context, settings: Record<string, unknown>) {
55
+ if (Object.keys(settings).length === 0) {
56
+ if (ctx.fs.exists(globalConfigPath())) ctx.fs.unlink(globalConfigPath());
57
+ return;
58
+ }
59
+ const temporary = `${globalConfigPath()}.${randomUUID()}.tmp` as TempPath;
60
+ try {
61
+ ctx.fs.writeUtf8File(temporary, JSON.stringify(settings, null, 2), 0o600);
62
+ ctx.fs.swapTmpFile(temporary, globalConfigPath());
63
+ } finally {
64
+ if (ctx.fs.exists(temporary)) ctx.fs.unlink(temporary);
65
+ }
66
+ }
67
+
68
+ export async function updateAccountSettings<T>(
69
+ ctx: Context,
70
+ update: (settings: z.infer<typeof settingsSchema>) => T | Promise<T>,
71
+ afterWrite?: () => void,
72
+ ): Promise<T> {
73
+ const lockPath = `${globalConfigPath()}.lock`;
74
+ ctx.fs.mkdir(rootDirectory(), { allowExisting: true });
75
+ const deadline = Date.now() + 5000;
76
+ for (;;) {
77
+ try {
78
+ ctx.fs.mkdir(lockPath);
79
+ break;
80
+ } catch (err: any) {
81
+ // Only the process that creates the directory owns the update lock.
82
+ // eslint-disable-next-line no-restricted-syntax
83
+ if (err.code !== "EEXIST") throw err;
84
+ if (Date.now() >= deadline) {
85
+ return await ctx.crash({
86
+ exitCode: 1,
87
+ errorType: "fatal",
88
+ printedMessage: `Another command is updating Convex accounts. Retry after it finishes. If it was interrupted, remove ${lockPath}.`,
89
+ });
90
+ }
91
+ await new Promise((resolve) => setTimeout(resolve, 25));
92
+ }
93
+ }
94
+ let released = false;
95
+ const release = () => {
96
+ if (released) return;
97
+ ctx.fs.rmdir(lockPath);
98
+ released = true;
99
+ };
100
+ const cleanup = ctx.registerCleanup(async () => release());
101
+ try {
102
+ const settings = await readAccountSettings(ctx);
103
+ const result = await update(settings);
104
+ writeAccountSettings(ctx, settings);
105
+ // Credential deletion follows persistence while sharing the lock with renewals.
106
+ afterWrite?.();
107
+ return result;
108
+ } finally {
109
+ ctx.removeCleanup(cleanup);
110
+ release();
111
+ }
112
+ }
113
+
114
+ export function accountConfigPath(name: string): string {
115
+ return path.join(
116
+ rootDirectory(),
117
+ "accounts",
118
+ `${encodeURIComponent(name)}.json`,
119
+ );
120
+ }
121
+
122
+ export async function validateAccountName(
123
+ ctx: Context,
124
+ name: string,
125
+ ): Promise<void> {
126
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) {
127
+ await ctx.crash({
128
+ exitCode: 1,
129
+ errorType: "invalid filesystem or env vars",
130
+ printedMessage:
131
+ "Account names must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, and underscores.",
132
+ });
133
+ }
134
+ }
135
+
136
+ export async function getAccountSelection(
137
+ ctx: Context,
138
+ options: { name?: string | undefined; envFile?: string | undefined } = {},
139
+ ): Promise<AccountSelection | null> {
140
+ let name = options.name;
141
+ let source = "--account";
142
+ if (name === undefined && process.env[CONVEX_ACCOUNT_ENV_VAR] !== undefined) {
143
+ name = process.env[CONVEX_ACCOUNT_ENV_VAR];
144
+ source = `${CONVEX_ACCOUNT_ENV_VAR} environment variable`;
145
+ }
146
+ if (name === undefined) {
147
+ const files = options.envFile
148
+ ? [options.envFile]
149
+ : [ENV_VAR_FILE_PATH, ".env"];
150
+ for (const file of files) {
151
+ if (!ctx.fs.exists(file)) continue;
152
+ name = dotenv.parse(ctx.fs.readUtf8File(file))[CONVEX_ACCOUNT_ENV_VAR];
153
+ if (name !== undefined) {
154
+ source = `${CONVEX_ACCOUNT_ENV_VAR} in ${file}`;
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ if (name === undefined) {
160
+ const config = await readAccountSettings(ctx);
161
+ const binding = findAccountDirectory(
162
+ config.directories ?? {},
163
+ process.cwd(),
164
+ );
165
+ name = binding?.name ?? config.defaultAccount;
166
+ source = binding
167
+ ? `directory selection for ${binding.directory}`
168
+ : `default account in ${globalConfigPath()}`;
169
+ }
170
+ if (name === undefined) return null;
171
+ await validateAccountName(ctx, name);
172
+ return { name, source };
173
+ }
174
+
175
+ export async function readAccountAuth(
176
+ ctx: Context,
177
+ account: AccountSelection,
178
+ allowMissing = false,
179
+ ): Promise<Extract<BigBrainAuth, { kind: "accessToken" }> | null> {
180
+ const credentials = await readAccountCredentials(ctx, account, allowMissing);
181
+ return credentials === null
182
+ ? null
183
+ : {
184
+ kind: "accessToken",
185
+ accessToken: credentials.accessToken,
186
+ header: `Bearer ${credentials.accessToken}`,
187
+ account,
188
+ };
189
+ }
190
+
191
+ export async function readAccountCredentials(
192
+ ctx: Context,
193
+ account: AccountSelection,
194
+ allowMissing = false,
195
+ ): Promise<AccountCredentials | null> {
196
+ await validateAccountName(ctx, account.name);
197
+ const configPath = accountConfigPath(account.name);
198
+ if (!ctx.fs.exists(configPath)) {
199
+ if (allowMissing) return null;
200
+ return await ctx.crash({
201
+ exitCode: 1,
202
+ errorType: "invalid filesystem data",
203
+ printedMessage: `Account "${account.name}" selected by ${account.source} is not logged in. Run \`npx convex login --account ${account.name}\`.`,
204
+ });
205
+ }
206
+ try {
207
+ return credentialsSchema.parse(JSON.parse(ctx.fs.readUtf8File(configPath)));
208
+ } catch {
209
+ return await ctx.crash({
210
+ exitCode: 1,
211
+ errorType: "invalid filesystem data",
212
+ printedMessage: `Unable to read credentials for account "${account.name}" from ${configPath}.`,
213
+ });
214
+ }
215
+ }
216
+
217
+ export function readAccountToken(ctx: Context, name: string): string | null {
218
+ try {
219
+ const token = JSON.parse(
220
+ ctx.fs.readUtf8File(accountConfigPath(name)),
221
+ ).accessToken;
222
+ return typeof token === "string" && token.length > 0 ? token : null;
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ async function writeCredentialsFile(
229
+ ctx: Context,
230
+ name: string,
231
+ credentials: AccountCredentials,
232
+ ) {
233
+ const configPath = accountConfigPath(name);
234
+ const directory = path.dirname(configPath);
235
+ ctx.fs.mkdir(directory, { allowExisting: true });
236
+ for (const dir of [rootDirectory(), directory]) {
237
+ if (!ctx.fs.lstat(dir).isDirectory()) {
238
+ return await ctx.crash({
239
+ exitCode: 1,
240
+ errorType: "invalid filesystem data",
241
+ printedMessage: `Cannot save account credentials through a symlink or non-directory: ${dir}.`,
242
+ });
243
+ }
244
+ }
245
+ const temporary = `${configPath}.${randomUUID()}.tmp` as TempPath;
246
+ try {
247
+ ctx.fs.writeUtf8File(
248
+ temporary,
249
+ JSON.stringify(credentials, null, 2),
250
+ 0o600,
251
+ );
252
+ ctx.fs.swapTmpFile(temporary, configPath);
253
+ } finally {
254
+ if (ctx.fs.exists(temporary)) ctx.fs.unlink(temporary);
255
+ }
256
+ }
257
+
258
+ export async function writeAccountCredentials(
259
+ ctx: Context,
260
+ name: string,
261
+ accessToken: string,
262
+ metadata: Omit<AccountCredentials, "accessToken"> = {},
263
+ expected?: AccountCredentials | null,
264
+ onConflict?: () => Promise<void>,
265
+ ): Promise<void> {
266
+ await validateAccountName(ctx, name);
267
+ await updateAccountSettings(ctx, async (settings) => {
268
+ if (expected !== undefined) {
269
+ const current = await readAccountCredentials(
270
+ ctx,
271
+ { name, source: "login" },
272
+ true,
273
+ );
274
+ if (!isDeepStrictEqual(current, expected)) {
275
+ await onConflict?.();
276
+ return await ctx.crash({
277
+ exitCode: 1,
278
+ errorType: "fatal",
279
+ printedMessage: `Account "${name}" changed during login. Retry the login.`,
280
+ });
281
+ }
282
+ }
283
+ await writeCredentialsFile(ctx, name, { ...metadata, accessToken });
284
+ if (settings.defaultAccount === name) settings.accessToken = accessToken;
285
+ });
286
+ }
287
+
288
+ export async function legacyAuthForFirstAccount(
289
+ ctx: Context,
290
+ account: AccountSelection,
291
+ ): Promise<Extract<BigBrainAuth, { kind: "accessToken" }> | null> {
292
+ const settings = await readAccountSettings(ctx);
293
+ const accountsDir = path.dirname(accountConfigPath(account.name));
294
+ if (
295
+ settings.defaultAccount !== undefined ||
296
+ (ctx.fs.exists(accountsDir) &&
297
+ ctx.fs.listDir(accountsDir).some((entry) => entry.name.endsWith(".json")))
298
+ ) {
299
+ return null;
300
+ }
301
+ const config = readGlobalConfig(ctx);
302
+ return config === null
303
+ ? null
304
+ : {
305
+ kind: "accessToken",
306
+ accessToken: config.accessToken,
307
+ header: `Bearer ${config.accessToken}`,
308
+ account,
309
+ };
310
+ }
311
+
312
+ export async function initializeDefaultAccount(
313
+ ctx: Context,
314
+ account: AccountSelection,
315
+ namingLegacyAccount: boolean,
316
+ select = true,
317
+ ) {
318
+ const defaultAccount = await updateAccountSettings(ctx, async (settings) => {
319
+ if (settings.defaultAccount !== undefined) return undefined;
320
+ const credentials = await readAccountCredentials(ctx, account);
321
+ const accessToken = credentials!.accessToken;
322
+ const legacyToken = settings.accessToken;
323
+ if (
324
+ !select &&
325
+ (savedAccountNames(ctx).some((name) => name !== account.name) ||
326
+ (!namingLegacyAccount &&
327
+ typeof legacyToken === "string" &&
328
+ legacyToken.length > 0 &&
329
+ legacyToken !== accessToken))
330
+ )
331
+ return undefined;
332
+ if (
333
+ !namingLegacyAccount &&
334
+ typeof legacyToken === "string" &&
335
+ legacyToken.length > 0 &&
336
+ legacyToken !== accessToken
337
+ ) {
338
+ // An explicitly new login keeps the previous account as the default for
339
+ // directories that haven't selected an account.
340
+ let legacyName = "default";
341
+ for (
342
+ let suffix = 2;
343
+ ctx.fs.exists(accountConfigPath(legacyName));
344
+ suffix++
345
+ ) {
346
+ legacyName = `default-${suffix}`;
347
+ }
348
+ await writeCredentialsFile(ctx, legacyName, { accessToken: legacyToken });
349
+ settings.defaultAccount = legacyName;
350
+ } else {
351
+ settings.defaultAccount = account.name;
352
+ settings.accessToken = accessToken;
353
+ }
354
+
355
+ return settings.defaultAccount;
356
+ });
357
+ if (defaultAccount !== undefined)
358
+ logFinishedStep(`Default account: ${defaultAccount}`);
359
+ }
360
+
361
+ export function findAccountDirectory(
362
+ directories: Record<string, string>,
363
+ cwd: string,
364
+ ): { directory: string; name: string } | null {
365
+ let directory = path.resolve(cwd);
366
+ for (;;) {
367
+ if (Object.hasOwn(directories, directory)) {
368
+ return { directory, name: directories[directory] };
369
+ }
370
+ const parent = path.dirname(directory);
371
+ if (parent === directory) return null;
372
+ directory = parent;
373
+ }
374
+ }
375
+
376
+ export function savedAccountNames(ctx: Context): string[] {
377
+ const directory = path.join(rootDirectory(), "accounts");
378
+ if (!ctx.fs.exists(directory)) return [];
379
+ return ctx.fs
380
+ .listDir(directory)
381
+ .filter((entry) => entry.name.endsWith(".json"))
382
+ .map((entry) => entry.name.slice(0, -5))
383
+ .sort();
384
+ }
385
+
386
+ export async function writeAccountDirectory(ctx: Context, name: string) {
387
+ await validateAccountName(ctx, name);
388
+ await updateAccountSettings(ctx, (settings) => {
389
+ settings.directories = { ...settings.directories, [process.cwd()]: name };
390
+ });
391
+ logFinishedStep(
392
+ `Saved directory selection: ${name} (applies to this directory and its subdirectories).`,
393
+ );
394
+ const selected = await getAccountSelection(ctx);
395
+ if (selected !== null && selected.name !== name) {
396
+ logWarning(
397
+ `Account "${selected.name}" selected by ${selected.source} overrides this directory selection. Remove that override to use account "${name}".`,
398
+ );
399
+ }
400
+ }
@@ -0,0 +1,43 @@
1
+ import { Context } from "../../bundler/context.js";
2
+ import { logWarning } from "../../bundler/log.js";
3
+ import {
4
+ deploymentNameFromAdminKey,
5
+ deploymentTypeFromAdminKey,
6
+ isDeploymentKey,
7
+ isPreviewDeployKey,
8
+ isProjectKey,
9
+ } from "./deployment.js";
10
+ import { getDeployKeySource } from "./envvars.js";
11
+ import { IGNORED_DEPLOY_KEY, readDeployKeyFromEnv } from "./utils/utils.js";
12
+
13
+ const warnedContexts = new WeakSet<Context>();
14
+
15
+ export function warnIfDeployKeyOverridesLogin(
16
+ ctx: Context,
17
+ hasAccount: boolean,
18
+ ) {
19
+ if (
20
+ !hasAccount ||
21
+ warnedContexts.has(ctx) ||
22
+ process.env.CONVEX_OVERRIDE_ACCESS_TOKEN
23
+ )
24
+ return;
25
+ const key = readDeployKeyFromEnv((name) => process.env[name]);
26
+ if (
27
+ key === undefined ||
28
+ key === IGNORED_DEPLOY_KEY ||
29
+ isPreviewDeployKey(key)
30
+ )
31
+ return;
32
+ const { envVarName, source } = getDeployKeySource(key);
33
+ const type = deploymentTypeFromAdminKey(key);
34
+ const target = isDeploymentKey(key)
35
+ ? `${type === "prod" ? "production" : type} deployment "${deploymentNameFromAdminKey(key)}"`
36
+ : isProjectKey(key)
37
+ ? "the project selected by the key"
38
+ : "the deployment selected by the key";
39
+ logWarning(
40
+ `WARNING: ${envVarName} in ${source} overrides your Convex account for deployment commands. They target ${target}. Remove ${envVarName} to use the selected account.`,
41
+ );
42
+ warnedContexts.add(ctx);
43
+ }