Skip to content

Commit e7b4e41

Browse files
thomasballingerConvex, Inc.
authored andcommitted
WorkOS Provisioning CLI (#41203)
Add (currently) Convex cloud-only version of an WorkOS integration that allows every development deployment to have its own WorkOS environment which is configured automatically during a `convex dev` session. Upon running `npm run dev` in a project - `WORKOS_CLIENT_ID` is missing in the deployment? Trigger point! - Once “AuthKit dev deployment auto-provisioning” is enabled, this will be the default when a WORKOS_CLIENT_ID is missing. - Until then, ask the user if they'd like to set up the integration - Try to provision a team if necessary (this can be done through the UI but a dashboard flow would be nicer) - Try to provision an environment - Does the deployment have `AUTHKIT_CLIENT_ID` and `AUTHKIT_ENVIRONMENT_ID` and `AUTHKIT_ENVIRONMENT_API_KEY` already set? If so done, just call the WorkOS environment API endpoints. - check for an existing environment + API key for this deployment stored on the Convex cloud - Set relevant Convex deployment environment variables - Set relevant local environment variables (mostly `WORKOS_CLIENT_ID`) - Set relevant WorkOS environment settings (redirect URI, CORS, etc.) GitOrigin-RevId: 31c2b0a3b30ff160ae16b882391c7ce60471a7ac
1 parent 0c5c382 commit e7b4e41

File tree

20 files changed

+818
-52
lines changed

20 files changed

+818
-52
lines changed

npm-packages/convex/src/bundler/context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export type ErrorType =
2828
// The error was some transient issue (e.g. a network
2929
// error). This will then cause a retry after an exponential backoff.
3030
| "transient"
31+
// This error was caught, handled, and now all that needs to happen
32+
// is for the proces to restart. No error is logged or reported.
33+
| "already handled"
3134
// This error is truly permanent. Exit `npx convex dev` because the
3235
// developer will need to take a manual commandline action.
3336
| "fatal";

npm-packages/convex/src/cli/env.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { actionDescription } from "./lib/command.js";
1010
import { ensureHasConvexDependency } from "./lib/utils/utils.js";
1111
import {
12-
envGetInDeployment,
12+
envGetInDeploymentAction,
1313
envListInDeployment,
1414
envRemoveInDeployment,
1515
envSetInDeployment,
@@ -83,7 +83,7 @@ const envGet = new Command("get")
8383
const options = cmd.optsWithGlobals();
8484
const { ctx, deployment } = await selectEnvDeployment(options);
8585
await ensureHasConvexDependency(ctx, "env get");
86-
await envGetInDeployment(ctx, deployment, envVarName);
86+
await envGetInDeploymentAction(ctx, deployment, envVarName);
8787
});
8888

8989
const envRemove = new Command("remove")

npm-packages/convex/src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { disableLocalDeployments } from "./disableLocalDev.js";
3131
import { mcp } from "./mcp.js";
3232
import dns from "node:dns";
3333
import net from "node:net";
34+
import { integration } from "./integration.js";
3435

3536
const MINIMUM_MAJOR_VERSION = 16;
3637
const MINIMUM_MINOR_VERSION = 15;
@@ -115,6 +116,7 @@ async function main() {
115116
.addCommand(update)
116117
.addCommand(logout)
117118
.addCommand(networkTest, { hidden: true })
119+
.addCommand(integration, { hidden: true })
118120
.addCommand(functionSpec)
119121
.addCommand(disableLocalDeployments)
120122
.addCommand(mcp)
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Debugging commands for the WorkOS integration; these are unstable, undocumented, and will change or disappear as the WorkOS integration evolves.
3+
**/
4+
import { Command } from "@commander-js/extra-typings";
5+
import { Context, oneoffContext } from "../bundler/context.js";
6+
import chalk from "chalk";
7+
import {
8+
DeploymentSelectionOptions,
9+
deploymentSelectionWithinProjectFromOptions,
10+
getTeamAndProjectSlugForDeployment,
11+
loadSelectedDeploymentCredentials,
12+
} from "./lib/api.js";
13+
import { actionDescription } from "./lib/command.js";
14+
import { ensureHasConvexDependency } from "./lib/utils/utils.js";
15+
import { getDeploymentSelection } from "./lib/deploymentSelection.js";
16+
import { ensureWorkosEnvironmentProvisioned } from "./lib/workos/workos.js";
17+
import {
18+
getCandidateEmailsForWorkIntegration,
19+
getDeploymentCanProvisionWorkOSEnvironments,
20+
} from "./lib/workos/platformApi.js";
21+
import { logMessage } from "../bundler/log.js";
22+
23+
async function selectEnvDeployment(
24+
options: DeploymentSelectionOptions,
25+
): Promise<{
26+
ctx: Context;
27+
deployment: {
28+
deploymentUrl: string;
29+
deploymentName: string;
30+
adminKey: string;
31+
deploymentNotice: string;
32+
};
33+
}> {
34+
const ctx = await oneoffContext(options);
35+
const deploymentSelection = await getDeploymentSelection(ctx, options);
36+
const selectionWithinProject =
37+
deploymentSelectionWithinProjectFromOptions(options);
38+
const {
39+
adminKey,
40+
url: deploymentUrl,
41+
deploymentFields,
42+
} = await loadSelectedDeploymentCredentials(
43+
ctx,
44+
deploymentSelection,
45+
selectionWithinProject,
46+
);
47+
const deploymentNotice =
48+
deploymentFields !== null
49+
? ` (on ${chalk.bold(deploymentFields.deploymentType)} deployment ${chalk.bold(deploymentFields.deploymentName)})`
50+
: "";
51+
return {
52+
ctx,
53+
deployment: {
54+
deploymentName: deploymentFields!.deploymentName,
55+
deploymentUrl,
56+
adminKey,
57+
deploymentNotice,
58+
},
59+
};
60+
}
61+
62+
const workosTeamStatus = new Command("status")
63+
.summary("Status of associated WorkOS team")
64+
.action(async (_options, cmd) => {
65+
const options = cmd.optsWithGlobals();
66+
const { ctx, deployment } = await selectEnvDeployment(options);
67+
68+
const { hasAssociatedWorkosTeam } =
69+
await getDeploymentCanProvisionWorkOSEnvironments(
70+
ctx,
71+
deployment.deploymentName,
72+
);
73+
74+
const info = await getTeamAndProjectSlugForDeployment(ctx, {
75+
deploymentName: deployment.deploymentName,
76+
});
77+
78+
const { availableEmails } = await getCandidateEmailsForWorkIntegration(ctx);
79+
80+
if (!hasAssociatedWorkosTeam) {
81+
logMessage(
82+
`Convex team ${info?.teamSlug} does not have an associated WorkOS team.`,
83+
);
84+
logMessage(
85+
`Verified emails that mighe be able to add one: ${availableEmails.join(" ")}`,
86+
);
87+
return;
88+
}
89+
90+
logMessage(`Convex team ${info?.teamSlug} has an associated WorkOS team.`);
91+
});
92+
93+
const workosProvisionEnvironment = new Command("provision-environment")
94+
.summary("Provision a WorkOS environment")
95+
.description(
96+
"Create or get the WorkOS environment and API key for this deployment",
97+
)
98+
.configureHelp({ showGlobalOptions: true })
99+
.allowExcessArguments(false)
100+
.addDeploymentSelectionOptions(
101+
actionDescription("Provision WorkOS environment for"),
102+
)
103+
.action(async (_options, cmd) => {
104+
const options = cmd.optsWithGlobals();
105+
const { ctx, deployment } = await selectEnvDeployment(options);
106+
await ensureHasConvexDependency(
107+
ctx,
108+
"integration workos provision-environment",
109+
);
110+
111+
try {
112+
await ensureWorkosEnvironmentProvisioned(
113+
ctx,
114+
deployment.deploymentName,
115+
deployment,
116+
);
117+
} catch (error) {
118+
await ctx.crash({
119+
exitCode: 1,
120+
errorType: "fatal",
121+
errForSentry: error,
122+
printedMessage: `Failed to provision WorkOS environment: ${String(error)}`,
123+
});
124+
}
125+
});
126+
const workos = new Command("workos")
127+
.summary("WorkOS integration commands")
128+
.description("Manage WorkOS team provisioning and environment setup")
129+
.addCommand(workosProvisionEnvironment)
130+
.addCommand(workosTeamStatus);
131+
132+
export const integration = new Command("integration")
133+
.summary("Integration commands")
134+
.description("Commands for managing third-party integrations")
135+
.addCommand(workos);

npm-packages/convex/src/cli/lib/api.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export async function checkAccessToSelectedProject(
249249
}
250250
}
251251

252-
async function getTeamAndProjectSlugForDeployment(
252+
export async function getTeamAndProjectSlugForDeployment(
253253
ctx: Context,
254254
selector: { deploymentName: string },
255255
): Promise<{ teamSlug: string; projectSlug: string } | null> {
@@ -810,3 +810,14 @@ export async function fetchTeamAndProjectForKey(
810810

811811
return data;
812812
}
813+
814+
export async function getTeamsForUser(ctx: Context) {
815+
const teams = await bigBrainAPI<{ id: number; name: string; slug: string }[]>(
816+
{
817+
ctx,
818+
method: "GET",
819+
url: "teams",
820+
},
821+
);
822+
return teams;
823+
}

npm-packages/convex/src/cli/lib/components.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ export async function runComponentsPush(
445445
options.verbose,
446446
);
447447

448-
changeSpinner("Diffing local code and deployment state");
448+
changeSpinner("Diffing local code and deployment state...");
449449
const { diffString } = diffConfig(
450450
remoteConfigWithModuleHashes,
451451
localConfig,

npm-packages/convex/src/cli/lib/config.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
printLocalDeploymentOnError,
4242
} from "./localDeployment/errors.js";
4343
import { debugIsolateBundlesSerially } from "../../bundler/debugBundle.js";
44+
import { ensureWorkosEnvironmentProvisioned } from "./workos/workos.js";
4445
export { productionProvisionHost, provisionHost } from "./utils/utils.js";
4546

4647
const brotli = promisify(zlib.brotliCompress);
@@ -841,6 +842,11 @@ export async function pushConfig(
841842
error,
842843
"Error: Unable to push deployment config to " + options.url,
843844
options.deploymentName,
845+
{
846+
adminKey: options.adminKey,
847+
deploymentUrl: options.url,
848+
deploymentNotice: "",
849+
},
844850
);
845851
}
846852
}
@@ -1087,13 +1093,37 @@ export async function handlePushConfigError(
10871093
error: unknown,
10881094
defaultMessage: string,
10891095
deploymentName: string | null,
1090-
) {
1096+
deployment?: {
1097+
deploymentUrl: string;
1098+
adminKey: string;
1099+
deploymentNotice: string;
1100+
},
1101+
): Promise<never> {
10911102
const data: ErrorData | undefined =
10921103
error instanceof ThrowingFetchError ? error.serverErrorData : undefined;
10931104
if (data?.code === "AuthConfigMissingEnvironmentVariable") {
10941105
const errorMessage = data.message || "(no error message given)";
10951106
const [, variableName] =
10961107
errorMessage.match(/Environment variable (\S+)/i) ?? [];
1108+
1109+
// WORKOS_CLIENT_ID is a special environment variable because cloud Convex
1110+
// deployments may be able to supply it by provisioning a fresh WorkOS
1111+
// environment on demand.
1112+
if (variableName === "WORKOS_CLIENT_ID" && deploymentName && deployment) {
1113+
const result = await ensureWorkosEnvironmentProvisioned(
1114+
ctx,
1115+
deploymentName,
1116+
deployment,
1117+
);
1118+
if (result === "ready") {
1119+
return await ctx.crash({
1120+
exitCode: 1,
1121+
errorType: "already handled",
1122+
printedMessage: null,
1123+
});
1124+
}
1125+
}
1126+
10971127
const envVarMessage =
10981128
`Environment variable ${chalk.bold(
10991129
variableName,

npm-packages/convex/src/cli/lib/deploy2.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export async function startPush(
9292
error,
9393
"Error: Unable to start push to " + options.url,
9494
options.deploymentName,
95+
{
96+
adminKey: request.adminKey,
97+
deploymentUrl: options.url,
98+
deploymentNotice: "",
99+
},
95100
);
96101
}
97102
}

npm-packages/convex/src/cli/lib/deployment.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,24 @@ export async function writeDeploymentEnvVar(
5959
);
6060
const deploymentEnvVarValue =
6161
deploymentType + ":" + deployment.deploymentName;
62+
// The `existingValue` that reaches this function is at least sometimes is missing its prefix.
63+
// Until this is cleaned up consider either of these values not a change.
64+
// Otherwise we spam the init instructions (Welcome to Convex etc.) on every run of `npx convex dev`.
65+
const changedDeploymentEnvVar =
66+
existingValue !== deployment.deploymentName &&
67+
existingValue !== deploymentEnvVarValue;
6268

6369
if (changedFile !== null) {
6470
ctx.fs.writeUtf8File(ENV_VAR_FILE_PATH, changedFile);
6571
// Only do this if we're not reinitializing an existing setup
6672
return {
6773
wroteToGitIgnore: await gitIgnoreEnvVarFile(ctx),
68-
changedDeploymentEnvVar: existingValue !== deploymentEnvVarValue,
74+
changedDeploymentEnvVar,
6975
};
7076
}
7177
return {
7278
wroteToGitIgnore: false,
73-
changedDeploymentEnvVar: existingValue !== deploymentEnvVarValue,
79+
changedDeploymentEnvVar,
7480
};
7581
}
7682

npm-packages/convex/src/cli/lib/dev.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,18 @@ export async function watchAndPush(
189189
break;
190190
}
191191
// Retry after an exponential backoff if we hit a transient error.
192-
if (e.errorType === "transient") {
192+
if (e.errorType === "transient" || e.errorType === "already handled") {
193193
const delay = nextBackoff(numFailures);
194194
numFailures += 1;
195-
logWarning(
196-
chalk.yellow(
197-
`Failed due to network error, retrying in ${formatDuration(
198-
delay,
199-
)}...`,
200-
),
201-
);
195+
if (e.errorType === "transient") {
196+
logWarning(
197+
chalk.yellow(
198+
`Failed due to network error, retrying in ${formatDuration(
199+
delay,
200+
)}...`,
201+
),
202+
);
203+
}
202204
await new Promise((resolve) => setTimeout(resolve, delay));
203205
continue;
204206
}

0 commit comments

Comments
 (0)