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.
- package/CHANGELOG.md +7 -0
- package/dist/browser.bundle.js +1 -1
- package/dist/browser.bundle.js.map +1 -1
- package/dist/cjs/bundler/context.js +7 -5
- package/dist/cjs/bundler/context.js.map +2 -2
- package/dist/cjs/bundler/fs.js +47 -6
- package/dist/cjs/bundler/fs.js.map +2 -2
- package/dist/cjs/cli/account.js +237 -0
- package/dist/cjs/cli/account.js.map +7 -0
- package/dist/cjs/cli/configure.js +15 -2
- package/dist/cjs/cli/configure.js.map +2 -2
- package/dist/cjs/cli/lib/accountManagement.js +196 -0
- package/dist/cjs/cli/lib/accountManagement.js.map +7 -0
- package/dist/cjs/cli/lib/accounts.js +334 -0
- package/dist/cjs/cli/lib/accounts.js.map +7 -0
- package/dist/cjs/cli/lib/deployKeyWarning.js +43 -0
- package/dist/cjs/cli/lib/deployKeyWarning.js.map +7 -0
- package/dist/cjs/cli/lib/deployment.js +2 -0
- package/dist/cjs/cli/lib/deployment.js.map +2 -2
- package/dist/cjs/cli/lib/deploymentSelection.js +27 -18
- package/dist/cjs/cli/lib/deploymentSelection.js.map +2 -2
- package/dist/cjs/cli/lib/envvars.js +32 -0
- package/dist/cjs/cli/lib/envvars.js.map +2 -2
- package/dist/cjs/cli/lib/fsUtils.js +1 -1
- package/dist/cjs/cli/lib/fsUtils.js.map +2 -2
- package/dist/cjs/cli/lib/generateDocs.js +1 -1
- package/dist/cjs/cli/lib/generateDocs.js.map +1 -1
- package/dist/cjs/cli/lib/login.js +42 -4
- package/dist/cjs/cli/lib/login.js.map +3 -3
- package/dist/cjs/cli/lib/mcp/requestContext.js +5 -1
- package/dist/cjs/cli/lib/mcp/requestContext.js.map +2 -2
- package/dist/cjs/cli/lib/plugins/claudeCode.js +188 -0
- package/dist/cjs/cli/lib/plugins/claudeCode.js.map +7 -0
- package/dist/cjs/cli/lib/plugins/codex.js +167 -0
- package/dist/cjs/cli/lib/plugins/codex.js.map +7 -0
- package/dist/cjs/cli/lib/plugins/harness.js +50 -0
- package/dist/cjs/cli/lib/plugins/harness.js.map +7 -0
- package/dist/cjs/cli/lib/plugins/marketplaces.js +76 -0
- package/dist/cjs/cli/lib/plugins/marketplaces.js.map +7 -0
- package/dist/cjs/cli/lib/plugins/skills.js +98 -0
- package/dist/cjs/cli/lib/plugins/skills.js.map +7 -0
- package/dist/cjs/cli/lib/plugins/utils.js +125 -0
- package/dist/cjs/cli/lib/plugins/utils.js.map +7 -0
- package/dist/cjs/cli/lib/utils/utils.js +18 -9
- package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
- package/dist/cjs/cli/login.js +213 -114
- package/dist/cjs/cli/login.js.map +3 -3
- package/dist/cjs/cli/logout.js +113 -12
- package/dist/cjs/cli/logout.js.map +3 -3
- package/dist/cjs/cli/mcp.js +43 -10
- package/dist/cjs/cli/mcp.js.map +3 -3
- package/dist/cjs/cli/plugins.js +114 -0
- package/dist/cjs/cli/plugins.js.map +7 -0
- package/dist/cjs/cli/program.js +3 -1
- package/dist/cjs/cli/program.js.map +2 -2
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/server/impl/actions_impl.js.map +2 -2
- package/dist/cjs-types/bundler/context.d.ts +5 -0
- package/dist/cjs-types/bundler/context.d.ts.map +1 -1
- package/dist/cjs-types/bundler/fs.d.ts +7 -2
- package/dist/cjs-types/bundler/fs.d.ts.map +1 -1
- package/dist/cjs-types/cli/account.d.ts +6 -0
- package/dist/cjs-types/cli/account.d.ts.map +1 -0
- package/dist/cjs-types/cli/configure.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/accountManagement.d.ts +10 -0
- package/dist/cjs-types/cli/lib/accountManagement.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/accounts.d.ts +59 -0
- package/dist/cjs-types/cli/lib/accounts.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts +3 -0
- package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/deployment.d.ts +2 -0
- package/dist/cjs-types/cli/lib/deployment.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/deploymentSelection.d.ts +2 -1
- package/dist/cjs-types/cli/lib/deploymentSelection.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/envvars.d.ts +6 -0
- package/dist/cjs-types/cli/lib/envvars.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/fsUtils.d.ts +1 -0
- package/dist/cjs-types/cli/lib/fsUtils.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/login.d.ts +3 -1
- package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/mcp/tools/insights.d.ts +2 -2
- package/dist/cjs-types/cli/lib/plugins/claudeCode.d.ts +25 -0
- package/dist/cjs-types/cli/lib/plugins/claudeCode.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/plugins/codex.d.ts +25 -0
- package/dist/cjs-types/cli/lib/plugins/codex.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/plugins/harness.d.ts +33 -0
- package/dist/cjs-types/cli/lib/plugins/harness.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/plugins/marketplaces.d.ts +40 -0
- package/dist/cjs-types/cli/lib/plugins/marketplaces.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/plugins/skills.d.ts +18 -0
- package/dist/cjs-types/cli/lib/plugins/skills.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/plugins/utils.d.ts +21 -0
- package/dist/cjs-types/cli/lib/plugins/utils.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/utils/utils.d.ts +5 -0
- package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
- package/dist/cjs-types/cli/login.d.ts +21 -0
- package/dist/cjs-types/cli/login.d.ts.map +1 -1
- package/dist/cjs-types/cli/logout.d.ts +8 -1
- package/dist/cjs-types/cli/logout.d.ts.map +1 -1
- package/dist/cjs-types/cli/mcp.d.ts.map +1 -1
- package/dist/cjs-types/cli/plugins.d.ts +3 -0
- package/dist/cjs-types/cli/plugins.d.ts.map +1 -0
- package/dist/cjs-types/cli/program.d.ts.map +1 -1
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/cjs-types/index.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/actions_impl.d.ts +3 -2
- package/dist/cjs-types/server/impl/actions_impl.d.ts.map +1 -1
- package/dist/cli.bundle.cjs +10059 -7094
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/esm/bundler/context.js +7 -5
- package/dist/esm/bundler/context.js.map +2 -2
- package/dist/esm/bundler/fs.js +47 -6
- package/dist/esm/bundler/fs.js.map +2 -2
- package/dist/esm/cli/account.js +221 -0
- package/dist/esm/cli/account.js.map +7 -0
- package/dist/esm/cli/configure.js +15 -2
- package/dist/esm/cli/configure.js.map +2 -2
- package/dist/esm/cli/lib/accountManagement.js +179 -0
- package/dist/esm/cli/lib/accountManagement.js.map +7 -0
- package/dist/esm/cli/lib/accounts.js +289 -0
- package/dist/esm/cli/lib/accounts.js.map +7 -0
- package/dist/esm/cli/lib/deployKeyWarning.js +27 -0
- package/dist/esm/cli/lib/deployKeyWarning.js.map +7 -0
- package/dist/esm/cli/lib/deployment.js +2 -2
- package/dist/esm/cli/lib/deployment.js.map +2 -2
- package/dist/esm/cli/lib/deploymentSelection.js +35 -20
- package/dist/esm/cli/lib/deploymentSelection.js.map +2 -2
- package/dist/esm/cli/lib/envvars.js +35 -1
- package/dist/esm/cli/lib/envvars.js.map +2 -2
- package/dist/esm/cli/lib/fsUtils.js +1 -1
- package/dist/esm/cli/lib/fsUtils.js.map +2 -2
- package/dist/esm/cli/lib/generateDocs.js +1 -1
- package/dist/esm/cli/lib/generateDocs.js.map +1 -1
- package/dist/esm/cli/lib/login.js +52 -4
- package/dist/esm/cli/lib/login.js.map +3 -3
- package/dist/esm/cli/lib/mcp/requestContext.js +5 -1
- package/dist/esm/cli/lib/mcp/requestContext.js.map +2 -2
- package/dist/esm/cli/lib/plugins/claudeCode.js +160 -0
- package/dist/esm/cli/lib/plugins/claudeCode.js.map +7 -0
- package/dist/esm/cli/lib/plugins/codex.js +140 -0
- package/dist/esm/cli/lib/plugins/codex.js.map +7 -0
- package/dist/esm/cli/lib/plugins/harness.js +17 -0
- package/dist/esm/cli/lib/plugins/harness.js.map +7 -0
- package/dist/esm/cli/lib/plugins/marketplaces.js +53 -0
- package/dist/esm/cli/lib/plugins/marketplaces.js.map +7 -0
- package/dist/esm/cli/lib/plugins/skills.js +65 -0
- package/dist/esm/cli/lib/plugins/skills.js.map +7 -0
- package/dist/esm/cli/lib/plugins/utils.js +86 -0
- package/dist/esm/cli/lib/plugins/utils.js.map +7 -0
- package/dist/esm/cli/lib/utils/utils.js +16 -9
- package/dist/esm/cli/lib/utils/utils.js.map +2 -2
- package/dist/esm/cli/login.js +225 -112
- package/dist/esm/cli/login.js.map +3 -3
- package/dist/esm/cli/logout.js +110 -13
- package/dist/esm/cli/logout.js.map +2 -2
- package/dist/esm/cli/mcp.js +34 -10
- package/dist/esm/cli/mcp.js.map +2 -2
- package/dist/esm/cli/plugins.js +99 -0
- package/dist/esm/cli/plugins.js.map +7 -0
- package/dist/esm/cli/program.js +3 -1
- package/dist/esm/cli/program.js.map +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/server/impl/actions_impl.js.map +2 -2
- package/dist/esm-types/bundler/context.d.ts +5 -0
- package/dist/esm-types/bundler/context.d.ts.map +1 -1
- package/dist/esm-types/bundler/fs.d.ts +7 -2
- package/dist/esm-types/bundler/fs.d.ts.map +1 -1
- package/dist/esm-types/cli/account.d.ts +6 -0
- package/dist/esm-types/cli/account.d.ts.map +1 -0
- package/dist/esm-types/cli/configure.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/accountManagement.d.ts +10 -0
- package/dist/esm-types/cli/lib/accountManagement.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/accounts.d.ts +59 -0
- package/dist/esm-types/cli/lib/accounts.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/deployKeyWarning.d.ts +3 -0
- package/dist/esm-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/deployment.d.ts +2 -0
- package/dist/esm-types/cli/lib/deployment.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/deploymentSelection.d.ts +2 -1
- package/dist/esm-types/cli/lib/deploymentSelection.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/envvars.d.ts +6 -0
- package/dist/esm-types/cli/lib/envvars.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/fsUtils.d.ts +1 -0
- package/dist/esm-types/cli/lib/fsUtils.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/login.d.ts +3 -1
- package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/mcp/tools/insights.d.ts +2 -2
- package/dist/esm-types/cli/lib/plugins/claudeCode.d.ts +25 -0
- package/dist/esm-types/cli/lib/plugins/claudeCode.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/plugins/codex.d.ts +25 -0
- package/dist/esm-types/cli/lib/plugins/codex.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/plugins/harness.d.ts +33 -0
- package/dist/esm-types/cli/lib/plugins/harness.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/plugins/marketplaces.d.ts +40 -0
- package/dist/esm-types/cli/lib/plugins/marketplaces.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/plugins/skills.d.ts +18 -0
- package/dist/esm-types/cli/lib/plugins/skills.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/plugins/utils.d.ts +21 -0
- package/dist/esm-types/cli/lib/plugins/utils.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/utils/utils.d.ts +5 -0
- package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
- package/dist/esm-types/cli/login.d.ts +21 -0
- package/dist/esm-types/cli/login.d.ts.map +1 -1
- package/dist/esm-types/cli/logout.d.ts +8 -1
- package/dist/esm-types/cli/logout.d.ts.map +1 -1
- package/dist/esm-types/cli/mcp.d.ts.map +1 -1
- package/dist/esm-types/cli/plugins.d.ts +3 -0
- package/dist/esm-types/cli/plugins.d.ts.map +1 -0
- package/dist/esm-types/cli/program.d.ts.map +1 -1
- package/dist/esm-types/index.d.ts +1 -1
- package/dist/esm-types/index.d.ts.map +1 -1
- package/dist/esm-types/server/impl/actions_impl.d.ts +3 -2
- package/dist/esm-types/server/impl/actions_impl.d.ts.map +1 -1
- package/dist/react.bundle.js +1 -1
- package/dist/react.bundle.js.map +1 -1
- package/package.json +2 -2
- package/src/bundler/context.ts +9 -5
- package/src/bundler/fs.ts +54 -5
- package/src/cli/account.ts +320 -0
- package/src/cli/configure.ts +18 -2
- package/src/cli/lib/accountManagement.ts +262 -0
- package/src/cli/lib/accounts.ts +400 -0
- package/src/cli/lib/deployKeyWarning.ts +43 -0
- package/src/cli/lib/deployment.ts +2 -2
- package/src/cli/lib/deploymentSelection.ts +37 -18
- package/src/cli/lib/envvars.ts +43 -1
- package/src/cli/lib/fsUtils.ts +5 -2
- package/src/cli/lib/generateDocs.ts +1 -1
- package/src/cli/lib/login.ts +64 -3
- package/src/cli/lib/mcp/requestContext.ts +7 -1
- package/src/cli/lib/plugins/claudeCode.ts +201 -0
- package/src/cli/lib/plugins/codex.ts +177 -0
- package/src/cli/lib/plugins/harness.ts +49 -0
- package/src/cli/lib/plugins/marketplaces.ts +94 -0
- package/src/cli/lib/plugins/skills.ts +101 -0
- package/src/cli/lib/plugins/utils.ts +121 -0
- package/src/cli/lib/utils/utils.ts +19 -15
- package/src/cli/login.ts +293 -168
- package/src/cli/logout.ts +142 -17
- package/src/cli/mcp.ts +48 -11
- package/src/cli/plugins.ts +154 -0
- package/src/cli/program.ts +4 -0
- package/src/index.ts +1 -1
- 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
|
+
}
|