convex
Version:
Client for the Convex Cloud
297 lines (293 loc) • 11 kB
JavaScript
;
import crypto from "crypto";
import {
changeSpinner,
logFinishedStep,
logMessage,
logOutput,
logWarning,
showSpinner,
stopSpinner
} from "../../../bundler/log.js";
import { getTeamAndProjectSlugForDeployment } from "../api.js";
import { callUpdateEnvironmentVariables, envGetInDeployment } from "../env.js";
import { changedEnvVarFile, suggestedEnvVarName } from "../envvars.js";
import { promptOptions, promptYesNo } from "../utils/prompts.js";
import { createCORSOrigin, createRedirectURI } from "./environmentApi.js";
import {
createAssociatedWorkosTeam,
createEnvironmentAndAPIKey,
getCandidateEmailsForWorkIntegration,
getDeploymentCanProvisionWorkOSEnvironments
} from "./platformApi.js";
export async function ensureWorkosEnvironmentProvisioned(ctx, deploymentName, deployment, options) {
if (!options.autoConfigureAuthkitConfig) {
return "choseNotToAssociatedTeam";
}
showSpinner("Checking for associated AuthKit environment...");
const existingEnvVars = await getExistingWorkosEnvVars(ctx, deployment);
if (existingEnvVars.clientId && existingEnvVars.environmentId && existingEnvVars.apiKey) {
logOutput(
"Deployment already has environment variables for a WorkOS environment configured for AuthKit."
);
await updateEnvLocal(
ctx,
existingEnvVars.clientId,
existingEnvVars.apiKey,
existingEnvVars.environmentId
);
await updateWorkosEnvironment(ctx, existingEnvVars.apiKey);
logFinishedStep("WorkOS AutKit environment ready");
return "ready";
}
const response = await getDeploymentCanProvisionWorkOSEnvironments(
ctx,
deploymentName
);
const { hasAssociatedWorkosTeam, teamId } = response;
if (response.disabled) {
return "choseNotToAssociatedTeam";
}
if (!hasAssociatedWorkosTeam) {
if (!options.offerToAssociateWorkOSTeam) {
return "choseNotToAssociatedTeam";
}
const result = await tryToCreateAssociatedWorkosTeam(
ctx,
deploymentName,
teamId
);
if (result === "choseNotToAssociatedTeam") {
return "choseNotToAssociatedTeam";
}
result;
}
const environmentResult = await createEnvironmentAndAPIKey(
ctx,
deploymentName,
options.environmentName
);
if (!environmentResult.success) {
if (environmentResult.error === "team_not_provisioned") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Team unexpectedly has no provisioned WorkOS team: ${environmentResult.message}`
});
}
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: environmentResult.message
});
}
const data = environmentResult.data;
if (data.newlyProvisioned) {
logMessage("New AuthKit environment provisioned");
} else {
logMessage(
"Using credentials from existing AuthKit environment already created for this deployment"
);
}
changeSpinner("Setting WORKOS_* deployment environment variables...");
await setConvexEnvVars(
ctx,
deployment,
data.clientId,
data.environmentId,
data.apiKey
);
showSpinner("Updating .env.local with WorkOS configuration");
await updateEnvLocal(ctx, data.clientId, data.apiKey, data.environmentId);
await updateWorkosEnvironment(ctx, data.apiKey);
logFinishedStep("WorkOS AutKit environment ready");
return "ready";
}
export async function provisionWorkosTeamInteractive(ctx, deploymentName, teamId, options = {}) {
const teamInfo = await getTeamAndProjectSlugForDeployment(ctx, {
deploymentName
});
if (teamInfo === null) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Can't find Convex Cloud team for deployment ${deploymentName}`
});
}
stopSpinner();
const defaultPrefix = `A WorkOS team needs to be created for your Convex team "${teamInfo.teamSlug}" in order to use AuthKit.
You and other members of this team will be able to create WorkOS environments for each Convex dev deployment for projects in this team.
By creating this account you agree to the WorkOS Terms of Service (https://workos.com/legal/terms-of-service) and Privacy Policy (https://workos.com/legal/privacy).
Alternately, choose no and set WORKOS_CLIENT_ID for an existing WorkOS environment.
`;
const defaultMessage = `Create a WorkOS team and enable automatic AuthKit environment provisioning for team "${teamInfo.teamSlug}"?`;
const agree = await promptYesNo(ctx, {
prefix: options.promptPrefix ?? defaultPrefix,
message: options.promptMessage ?? defaultMessage
});
if (!agree) {
return { success: false, reason: "cancelled" };
}
const alreadyTried = /* @__PURE__ */ new Map();
let email;
while (true) {
let choice = "refresh";
while (choice === "refresh") {
const { availableEmails } = await getCandidateEmailsForWorkIntegration(ctx);
choice = await promptOptions(ctx, {
message: availableEmails.length === 1 ? "Create a new WorkOS team with this email address?" : "Create a new WorkOS team with which email address?",
suffix: availableEmails.length === 0 ? "\nVisit https://dashboard.convex.dev/profile to add a verified email to use to provision a WorkOS account" : availableEmails.length === 1 ? "\nCreate a new WorkOS team with this email address?" : "\nTo use another email address visit https://dashboard.convex.dev/profile to add and verify, then choose 'refresh'",
choices: [
...availableEmails.map((email2) => ({
name: `${email2}${alreadyTried.has(email2) ? ` (can't create, a WorkOS team already exists with this email)` : ""}`,
value: email2
})),
{
name: "refresh (add an email at https://dashboard.convex.dev/profile)",
value: "refresh"
},
{
name: "cancel (do not create a WorkOS account)",
value: "cancel"
}
]
});
}
if (choice === "cancel") {
return { success: false, reason: "cancelled" };
}
email = choice;
const teamResult = await createAssociatedWorkosTeam(ctx, teamId, email);
if (teamResult.result === "emailAlreadyUsed") {
logMessage(teamResult.message);
alreadyTried.set(email, teamResult.message);
continue;
}
return {
success: true,
workosTeamId: teamResult.workosTeamId,
workosTeamName: teamResult.workosTeamName
};
}
}
export async function tryToCreateAssociatedWorkosTeam(ctx, deploymentName, teamId) {
const result = await provisionWorkosTeamInteractive(
ctx,
deploymentName,
teamId
);
if (!result.success) {
return "choseNotToAssociatedTeam";
}
logFinishedStep("WorkOS team created successfully");
return "ready";
}
async function updateWorkosEnvironment(ctx, workosApiKey) {
let { frontendDevUrl } = await suggestedEnvVarName(ctx);
frontendDevUrl = frontendDevUrl || "http://localhost:5173";
const redirectUri = `${frontendDevUrl}/callback`;
const corsOrigin = `${frontendDevUrl}`;
await applyConfigToWorkosEnvironment(ctx, {
workosApiKey,
redirectUri,
corsOrigin
});
}
async function applyConfigToWorkosEnvironment(ctx, {
workosApiKey,
redirectUri,
corsOrigin
}) {
changeSpinner("Configuring AuthKit redirect URI...");
const { modified: redirectUriAdded } = await createRedirectURI(
ctx,
workosApiKey,
redirectUri
);
if (redirectUriAdded) {
logMessage(`AuthKit redirect URI added: ${redirectUri}`);
}
changeSpinner("Configuring AuthKit CORS origin...");
const { modified: corsAdded } = await createCORSOrigin(
ctx,
workosApiKey,
corsOrigin
);
if (corsAdded) {
logMessage(`AuthKit CORS origin added: ${corsOrigin}`);
}
}
async function updateEnvLocal(ctx, clientId, apiKey, environmentId) {
const envPath = ".env.local";
const { frontendDevUrl, detectedFramework, publicPrefix } = await suggestedEnvVarName(ctx);
if (!detectedFramework || !["Vite", "Next.js", "TanStackStart"].includes(detectedFramework)) {
logWarning(
"Can't configure .env.local, fill it out according to directions for the corresponding AuthKit SDK. Use `npx convex list` to see relevant environment variables."
);
}
let suggestedChanges = {};
let existingFileContent = ctx.fs.exists(envPath) ? ctx.fs.readUtf8File(envPath) : null;
if (publicPrefix) {
if (detectedFramework === "Vite") {
suggestedChanges[`${publicPrefix}WORKOS_CLIENT_ID`] = {
value: clientId,
commentOnPreviousLine: `# See this environment at ${workosUrl(environmentId, "/authentication")}`
};
} else if (detectedFramework === "Next.js" || detectedFramework === "TanStackStart") {
suggestedChanges[`WORKOS_CLIENT_ID`] = {
value: clientId,
commentOnPreviousLine: `# See this environment at ${workosUrl(environmentId, "/authentication")}`
};
}
if (frontendDevUrl) {
suggestedChanges[detectedFramework === "TanStackStart" ? "WORKOS_REDIRECT_URI" : `${publicPrefix}WORKOS_REDIRECT_URI`] = {
value: `${frontendDevUrl}/callback`
};
}
}
if (detectedFramework === "Next.js" || detectedFramework === "TanStackStart") {
if (!existingFileContent || !existingFileContent.includes("WORKOS_COOKIE_PASSWORD")) {
suggestedChanges["WORKOS_COOKIE_PASSWORD"] = {
value: crypto.randomBytes(32).toString("base64url")
};
}
suggestedChanges["WORKOS_API_KEY"] = { value: apiKey };
}
for (const [
envVarName,
{ value: envVarValue, commentOnPreviousLine, commentAfterValue }
] of Object.entries(suggestedChanges)) {
existingFileContent = changedEnvVarFile({
existingFileContent,
envVarName,
envVarValue,
commentAfterValue: commentAfterValue ?? null,
commentOnPreviousLine: commentOnPreviousLine ?? null
}) || existingFileContent;
}
if (existingFileContent !== null) {
ctx.fs.writeUtf8File(envPath, existingFileContent);
logMessage(
`Updated .env.local with ${Object.keys(suggestedChanges).join(", ")}`
);
}
}
async function getExistingWorkosEnvVars(ctx, deployment) {
const [clientId, environmentId, apiKey] = await Promise.all([
envGetInDeployment(ctx, deployment, "WORKOS_CLIENT_ID"),
envGetInDeployment(ctx, deployment, "WORKOS_ENVIRONMENT_ID"),
envGetInDeployment(ctx, deployment, "WORKOS_ENVIRONMENT_API_KEY")
]);
return { clientId, environmentId, apiKey };
}
async function setConvexEnvVars(ctx, deployment, workosClientId, workosEnvironmentId, workosEnvironmentApiKey) {
await callUpdateEnvironmentVariables(ctx, deployment, [
{ name: "WORKOS_CLIENT_ID", value: workosClientId },
{ name: "WORKOS_ENVIRONMENT_ID", value: workosEnvironmentId },
{ name: "WORKOS_ENVIRONMENT_API_KEY", value: workosEnvironmentApiKey }
]);
}
function workosUrl(environmentId, subpath) {
return `https://dashboard.workos.com/${environmentId}${subpath}`;
}
//# sourceMappingURL=workos.js.map