convex
Version:
Client for the Convex Cloud
290 lines (289 loc) • 11.2 kB
JavaScript
;
import { Command } from "@commander-js/extra-typings";
import { oneoffContext } from "../bundler/context.js";
import { chalkStderr } from "chalk";
import {
deploymentSelectionWithinProjectFromOptions,
fetchTeamAndProject,
getTeamAndProjectSlugForDeployment,
loadSelectedDeploymentCredentials
} from "./lib/api.js";
import { actionDescription } from "./lib/command.js";
import { ensureHasConvexDependency } from "./lib/utils/utils.js";
import { getDeploymentSelection } from "./lib/deploymentSelection.js";
import {
ensureWorkosEnvironmentProvisioned,
provisionWorkosTeamInteractive
} from "./lib/workos/workos.js";
import {
disconnectWorkOSTeam,
getCandidateEmailsForWorkIntegration,
getDeploymentCanProvisionWorkOSEnvironments,
getInvitationEligibleEmails,
getWorkosEnvironmentHealth,
getWorkosTeamHealth,
inviteToWorkosTeam
} from "./lib/workos/platformApi.js";
import { logFinishedStep, logMessage } from "../bundler/log.js";
import { promptOptions, promptYesNo } from "./lib/utils/prompts.js";
async function selectEnvDeployment(options) {
const ctx = await oneoffContext(options);
const deploymentSelection = await getDeploymentSelection(ctx, options);
const selectionWithinProject = deploymentSelectionWithinProjectFromOptions(options);
const {
adminKey,
url: deploymentUrl,
deploymentFields
} = await loadSelectedDeploymentCredentials(
ctx,
deploymentSelection,
selectionWithinProject
);
const deploymentNotice = deploymentFields !== null ? ` (on ${chalkStderr.bold(deploymentFields.deploymentType)} deployment ${chalkStderr.bold(deploymentFields.deploymentName)})` : "";
return {
ctx,
deployment: {
deploymentName: deploymentFields.deploymentName,
deploymentUrl,
adminKey,
deploymentNotice
}
};
}
const workosTeamStatus = new Command("status").summary("Status of associated WorkOS team and environment").addDeploymentSelectionOptions(actionDescription("Check WorkOS status for")).action(async (_options, cmd) => {
const options = cmd.optsWithGlobals();
const { ctx, deployment } = await selectEnvDeployment(options);
const info = await fetchTeamAndProject(ctx, deployment.deploymentName);
const teamHealth = await getWorkosTeamHealth(ctx, info.teamId);
if (!teamHealth) {
logMessage(`WorkOS team: Not provisioned`);
const { availableEmails } = await getCandidateEmailsForWorkIntegration(ctx);
if (availableEmails.length > 0) {
logMessage(
` Verified emails that can provision: ${availableEmails.join(", ")}`
);
}
} else if (teamHealth.teamStatus === "inactive") {
logMessage(
`WorkOS team: ${teamHealth.name} (no credit card added on workos.com, so production auth environments cannot be created)`
);
} else {
logMessage(`WorkOS team: ${teamHealth.name}`);
}
const envHealth = await getWorkosEnvironmentHealth(
ctx,
deployment.deploymentName
);
if (!envHealth) {
logMessage(`WorkOS environment: Not provisioned`);
} else {
logMessage(`WorkOS environment: ${envHealth.name}`);
const workosUrl = `https://dashboard.workos.com/${envHealth.id}/authentication`;
logMessage(`${workosUrl}`);
}
});
const workosProvisionEnvironment = new Command("provision-environment").summary("Provision a WorkOS environment").description(
"Create or get the WorkOS environment and API key for this deployment"
).configureHelp({ showGlobalOptions: true }).allowExcessArguments(false).addDeploymentSelectionOptions(
actionDescription("Provision WorkOS environment for")
).option(
"--name <name>",
"Custom name for the WorkOS environment (if not provided, uses deployment name)"
).action(async (_options, cmd) => {
const options = cmd.optsWithGlobals();
const { ctx, deployment } = await selectEnvDeployment(options);
await ensureHasConvexDependency(
ctx,
"integration workos provision-environment"
);
const environmentName = options.name;
try {
await ensureWorkosEnvironmentProvisioned(
ctx,
deployment.deploymentName,
deployment,
{
offerToAssociateWorkOSTeam: true,
autoProvisionIfWorkOSTeamAssociated: true,
autoConfigureAuthkitConfig: true,
...environmentName !== void 0 && { environmentName }
}
);
} catch (error) {
await ctx.crash({
exitCode: 1,
errorType: "fatal",
errForSentry: error,
printedMessage: `Failed to provision WorkOS environment: ${String(error)}`
});
}
});
const workosProvisionTeam = new Command("provision-team").summary("Provision a WorkOS team for this Convex team").description(
"Create a WorkOS team and associate it with this Convex team. This enables automatic provisioning of WorkOS environments for deployments on this team."
).configureHelp({ showGlobalOptions: true }).allowExcessArguments(false).addDeploymentSelectionOptions(actionDescription("Provision WorkOS team for")).action(async (_options, cmd) => {
const options = cmd.optsWithGlobals();
const { ctx, deployment } = await selectEnvDeployment(options);
const { hasAssociatedWorkosTeam, teamId } = await getDeploymentCanProvisionWorkOSEnvironments(
ctx,
deployment.deploymentName
);
if (hasAssociatedWorkosTeam) {
logMessage(
chalkStderr.yellow(
"This Convex team already has an associated WorkOS team."
)
);
logMessage(
chalkStderr.dim(
"Use 'npx convex integration workos status' to view details."
)
);
return;
}
const result = await provisionWorkosTeamInteractive(
ctx,
deployment.deploymentName,
teamId
);
if (!result.success) {
logMessage(chalkStderr.gray("Cancelled."));
return;
}
logMessage(
chalkStderr.green(
`
\u2713 Successfully created WorkOS team "${result.workosTeamName}" (${result.workosTeamId})`
)
);
logMessage(
chalkStderr.dim(
"You can now provision WorkOS environments for deployments on this team."
)
);
});
const workosDisconnectTeam = new Command("disconnect-team").summary("Disconnect WorkOS team from Convex team").description(
"Remove the associated WorkOS team from this Convex team. This is a destructive action that will prevent new WorkOS environments from being provisioned. Existing environments will continue to work with their current API keys."
).configureHelp({ showGlobalOptions: true }).allowExcessArguments(false).addDeploymentSelectionOptions(
actionDescription("Disconnect WorkOS team for")
).action(async (_options, cmd) => {
const options = cmd.optsWithGlobals();
const { ctx, deployment } = await selectEnvDeployment(options);
const { hasAssociatedWorkosTeam, teamId } = await getDeploymentCanProvisionWorkOSEnvironments(
ctx,
deployment.deploymentName
);
if (!hasAssociatedWorkosTeam) {
logMessage(
chalkStderr.yellow(
"This Convex team does not have an associated WorkOS team."
)
);
return;
}
const info = await getTeamAndProjectSlugForDeployment(ctx, {
deploymentName: deployment.deploymentName
});
logMessage(
chalkStderr.yellow(
`Warning: This will disconnect the WorkOS team from Convex team "${info?.teamSlug}".`
)
);
logMessage(
"AuthKit environments provisioned for Convex deployments on this team will no longer use this WorkOS team to provision environments."
);
logMessage(
chalkStderr.dim(
"Existing WorkOS environments will continue to work with their current API keys."
)
);
const confirmed = await promptYesNo(ctx, {
message: "Are you sure you want to disconnect this WorkOS team?",
default: false
});
if (!confirmed) {
logMessage(chalkStderr.gray("Cancelled."));
return;
}
const result = await disconnectWorkOSTeam(ctx, teamId);
if (!result.success) {
if (result.error === "not_associated") {
logMessage(
chalkStderr.yellow(
"This Convex team does not have an associated WorkOS team."
)
);
return;
}
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Failed to disconnect WorkOS team: ${result.message}`
});
}
logFinishedStep(
`Successfully disconnected WorkOS team "${result.workosTeamName}" (${result.workosTeamId})`
);
});
const workosInvite = new Command("invite").summary("Invite yourself to the WorkOS team").description(
"Send an invitation to join the WorkOS team associated with your Convex team"
).configureHelp({ showGlobalOptions: true }).allowExcessArguments(false).addDeploymentSelectionOptions(
actionDescription("Invite yourself to WorkOS team for")
).action(async (_options, cmd) => {
const options = cmd.optsWithGlobals();
const { ctx, deployment } = await selectEnvDeployment(options);
const info = await fetchTeamAndProject(ctx, deployment.deploymentName);
const { eligibleEmails, adminEmail } = await getInvitationEligibleEmails(
ctx,
info.teamId
);
const allInvitableEmails = [...eligibleEmails];
if (adminEmail && !allInvitableEmails.includes(adminEmail)) {
allInvitableEmails.push(adminEmail);
}
if (allInvitableEmails.length === 0) {
logMessage(
"You don't have any verified emails available for invitation."
);
logMessage(
"This could be because all your verified emails are already admin of other WorkOS teams."
);
return;
}
const emailToInvite = await promptOptions(ctx, {
message: "Which email would you like to invite to the WorkOS team?",
choices: allInvitableEmails.map((email) => ({
name: email + (email === adminEmail ? " (admin email)" : ""),
value: email
})),
default: allInvitableEmails[0]
});
const confirmed = await promptYesNo(ctx, {
message: `Send invitation to ${emailToInvite}?`,
default: true
});
if (!confirmed) {
logMessage("Invitation cancelled.");
return;
}
logMessage(`Sending invitation to ${emailToInvite}...`);
const result = await inviteToWorkosTeam(ctx, info.teamId, emailToInvite);
if (result.result === "success") {
logMessage(
`\u2713 Successfully sent invitation to ${result.email} with role ${result.roleSlug}`
);
logMessage(
"Check your email for the invitation link to join the WorkOS team."
);
} else if (result.result === "teamNotProvisioned") {
logMessage(
`\u2717 ${result.message}. Run 'npx convex integration workos provision-environment' first.`
);
} else if (result.result === "alreadyInWorkspace") {
logMessage(
`\u2717 ${result.message}. This usually means the email is already used in another WorkOS workspace.`
);
}
});
const workos = new Command("workos").summary("WorkOS integration commands").description("Manage WorkOS team provisioning and environment setup").addCommand(workosProvisionEnvironment).addCommand(workosTeamStatus).addCommand(workosProvisionTeam).addCommand(workosDisconnectTeam).addCommand(workosInvite);
export const integration = new Command("integration").summary("Integration commands").description("Commands for managing third-party integrations").addCommand(workos);
//# sourceMappingURL=integration.js.map