* fix: improve Vercel domain 'forbidden' error message The 'forbidden' error code from Vercel is a generic permission denial, not specifically 'domain owned by another team'. It can occur for various reasons: wrong teamId, token lacking project access, account-level restrictions, or domain ownership by another team. Updated the error message to be more accurate and changed the HTTP status code from 400 to 403 to better reflect the permission error nature. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: add message and invalidToken fields to Vercel error response schema This allows better logging of Vercel API error responses, especially when tokens become invalid. The new fields help with debugging permission issues. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: revert status code to 400 for Vercel forbidden errors Keep the status code at 400 to avoid potential downstream side effects while still using the improved error message and schema fields. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use .nullish() instead of .optional() for message and invalidToken fields .nullish() handles both null and undefined, which is safer for API responses. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
178 lines
4.9 KiB
TypeScript
178 lines
4.9 KiB
TypeScript
import z from "zod";
|
|
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
|
|
import logger from "../../logger";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["Vercel/DomainManager"] });
|
|
const vercelApiForProjectUrl = `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}`;
|
|
const vercelDomainApiResponseSchema = z.object({
|
|
error: z
|
|
.object({
|
|
code: z.string().nullish(),
|
|
domain: z.any().nullish(),
|
|
message: z.string().nullish(),
|
|
invalidToken: z.boolean().nullish(),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
export const createDomain = async (domain: string) => {
|
|
assertVercelEnvVars(process.env);
|
|
log.info(`Creating domain in Vercel: ${domain}`);
|
|
const response = await fetch(`${vercelApiForProjectUrl}/domains?teamId=${process.env.TEAM_ID_VERCEL}`, {
|
|
body: JSON.stringify({ name: domain }),
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
method: "POST",
|
|
});
|
|
|
|
const responseJson = await response.json();
|
|
|
|
const parsedResponse = vercelDomainApiResponseSchema.safeParse(responseJson);
|
|
|
|
if (!parsedResponse.success) {
|
|
// Looks like Vercel changed the response format, so sometimes zod parsing fails
|
|
log.error(
|
|
safeStringify({
|
|
errorMessage: "Failed to parse Vercel domain creation response",
|
|
zodError: parsedResponse.error,
|
|
response: responseJson,
|
|
})
|
|
);
|
|
// Let's consider domain creation failed
|
|
return false;
|
|
}
|
|
|
|
if (!parsedResponse.data.error) {
|
|
return true;
|
|
}
|
|
|
|
return handleDomainCreationError(parsedResponse.data.error);
|
|
};
|
|
|
|
export const deleteDomain = async (domain: string) => {
|
|
log.info(`Deleting domain in Vercel: ${domain}`);
|
|
assertVercelEnvVars(process.env);
|
|
|
|
const response = await fetch(
|
|
`${vercelApiForProjectUrl}/domains/${domain}?teamId=${process.env.TEAM_ID_VERCEL}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
|
|
},
|
|
method: "DELETE",
|
|
}
|
|
);
|
|
|
|
const data = vercelDomainApiResponseSchema.parse(await response.json());
|
|
if (!data.error) {
|
|
return true;
|
|
}
|
|
|
|
return handleDomainDeletionError(data.error);
|
|
};
|
|
|
|
function handleDomainCreationError(error: {
|
|
code?: string | null;
|
|
domain?: string | null;
|
|
message?: string | null;
|
|
invalidToken?: boolean | null;
|
|
}){
|
|
// Vercel returns "forbidden" for various permission issues, not just domain ownership
|
|
if (error.code === "forbidden") {
|
|
const errorMessage =
|
|
"Vercel denied permission to manage this domain. Please verify your Vercel project, team, and domain permissions.";
|
|
log.error(
|
|
safeStringify({
|
|
errorMessage,
|
|
vercelError: error,
|
|
})
|
|
);
|
|
throw new HttpError({
|
|
message: errorMessage,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
if (error.code === "domain_taken") {
|
|
const errorMessage = "Domain is already being used by a different project";
|
|
log.error(
|
|
safeStringify({
|
|
errorMessage,
|
|
vercelError: error,
|
|
})
|
|
);
|
|
throw new HttpError({
|
|
message: errorMessage,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
if (error.code === "domain_already_in_use") {
|
|
// Domain is already configured correctly, this is not an error when it happens during creation as it could be re-attempt to create an existing domain
|
|
return true;
|
|
}
|
|
|
|
const errorMessage = `Failed to create domain on Vercel: ${error.domain}`;
|
|
log.error(safeStringify({ errorMessage, vercelError: error }));
|
|
throw new HttpError({
|
|
message: errorMessage,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
function handleDomainDeletionError(error: {
|
|
code?: string | null;
|
|
domain?: string | null;
|
|
message?: string | null;
|
|
invalidToken?: boolean | null;
|
|
}){
|
|
if (error.code === "not_found") {
|
|
// Domain is already deleted
|
|
return true;
|
|
}
|
|
|
|
// Vercel returns "forbidden" for various permission issues, not just domain ownership
|
|
if (error.code === "forbidden") {
|
|
const errorMessage =
|
|
"Vercel denied permission to manage this domain. Please verify your Vercel project, team, and domain permissions.";
|
|
log.error(
|
|
safeStringify({
|
|
errorMessage,
|
|
vercelError: error,
|
|
})
|
|
);
|
|
throw new HttpError({
|
|
message: errorMessage,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
const errorMessage = `Failed to take action for domain: ${error.domain}`;
|
|
log.error(safeStringify({ errorMessage, vercelError: error }));
|
|
throw new HttpError({
|
|
message: errorMessage,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
function assertVercelEnvVars(env: typeof process.env): asserts env is {
|
|
PROJECT_ID_VERCEL: string;
|
|
TEAM_ID_VERCEL: string;
|
|
AUTH_BEARER_TOKEN_VERCEL: string;
|
|
} & typeof process.env {
|
|
if (!env.PROJECT_ID_VERCEL) {
|
|
throw new Error("Missing env var: PROJECT_ID_VERCEL");
|
|
}
|
|
|
|
// TEAM_ID_VERCEL is optional
|
|
|
|
if (!env.AUTH_BEARER_TOKEN_VERCEL) {
|
|
throw new Error("Missing env var: AUTH_BEARER_TOKEN_VERCEL");
|
|
}
|
|
}
|