diff --git a/docs/content/docs/concepts/api.mdx b/docs/content/docs/concepts/api.mdx
index 7c79e434ea..b37f22e9a3 100644
--- a/docs/content/docs/concepts/api.mdx
+++ b/docs/content/docs/concepts/api.mdx
@@ -100,7 +100,7 @@ const response = await auth.api.signInEmail({
When you call an API endpoint on the server, it will throw an error if the request fails. You can catch the error and handle it as you see fit. The error instance is an instance of `APIError`.
```ts title="server.ts"
-import { APIError } from "better-auth/api";
+import { APIError, isAPIError } from "better-auth/api";
try {
await auth.api.signInEmail({
@@ -110,7 +110,7 @@ try {
}
})
} catch (error) {
- if (error instanceof APIError) {
+ if (isAPIError(error)) {
console.log(error.message, error.status)
}
}
diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts
index 57380fe47d..c7ab3e32ff 100644
--- a/packages/better-auth/src/api/index.ts
+++ b/packages/better-auth/src/api/index.ts
@@ -7,8 +7,9 @@ import type {
import type { InternalLogger } from "@better-auth/core/env";
import { logger } from "@better-auth/core/env";
import type { Endpoint, Middleware } from "better-call";
-import { APIError, createRouter } from "better-call";
+import { createRouter } from "better-call";
import type { UnionToIntersection } from "../types/helper";
+import { isAPIError } from "../utils/is-api-error";
import { originCheckMiddleware } from "./middlewares";
import { onRequestRateLimit } from "./rate-limiter";
import {
@@ -315,7 +316,7 @@ export const router = (
return res;
},
onError(e) {
- if (e instanceof APIError && e.status === "FOUND") {
+ if (isAPIError(e) && e.status === "FOUND") {
return;
}
if (options.onAPIError?.throw) {
@@ -352,7 +353,7 @@ export const router = (
}
}
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
if (e.status === "INTERNAL_SERVER_ERROR") {
ctx.logger.error(e.status, e);
}
@@ -375,7 +376,8 @@ export {
createAuthMiddleware,
optionsMiddleware,
} from "@better-auth/core/api";
-export { APIError } from "better-call";
+export { APIError } from "@better-auth/core/error";
export { getIp } from "../utils/get-request-ip";
+export { isAPIError } from "../utils/is-api-error";
export * from "./middlewares";
export * from "./routes";
diff --git a/packages/better-auth/src/api/middlewares/origin-check.ts b/packages/better-auth/src/api/middlewares/origin-check.ts
index 114fa889b0..18eda4784f 100644
--- a/packages/better-auth/src/api/middlewares/origin-check.ts
+++ b/packages/better-auth/src/api/middlewares/origin-check.ts
@@ -1,6 +1,6 @@
import type { GenericEndpointContext } from "@better-auth/core";
import { createAuthMiddleware } from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
/**
* A middleware to validate callbackURL and origin against
@@ -25,7 +25,15 @@ export const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
const newUserCallbackURL = body?.newUserCallbackURL;
const useCookies = headers?.has("cookie");
- const validateURL = (url: string | undefined, label: string) => {
+ const validateURL = (
+ url: string | undefined,
+ label:
+ | "origin"
+ | "callbackURL"
+ | "redirectURL"
+ | "errorCallbackURL"
+ | "newUserCallbackURL",
+ ) => {
if (!url) {
return;
}
@@ -39,7 +47,30 @@ export const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`,
`Current list of trustedOrigins: ${ctx.context.trustedOrigins}`,
);
- throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
+ if (label === "origin") {
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.INVALID_ORIGIN);
+ }
+ if (label === "callbackURL") {
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.INVALID_CALLBACK_URL);
+ }
+ if (label === "redirectURL") {
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.INVALID_REDIRECT_URL);
+ }
+ if (label === "errorCallbackURL") {
+ throw APIError.from(
+ "FORBIDDEN",
+ BASE_ERROR_CODES.INVALID_ERROR_CALLBACK_URL,
+ );
+ }
+ if (label === "newUserCallbackURL") {
+ throw APIError.from(
+ "FORBIDDEN",
+ BASE_ERROR_CODES.INVALID_NEW_USER_CALLBACK_URL,
+ );
+ }
+ throw APIError.fromStatus("FORBIDDEN", {
+ message: `Invalid ${label}`,
+ });
}
};
if (
@@ -48,7 +79,7 @@ export const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
!ctx.context.skipOriginCheck
) {
if (!originHeader || originHeader === "null") {
- throw new APIError("FORBIDDEN", { message: "Missing or null Origin" });
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.MISSING_OR_NULL_ORIGIN);
}
validateURL(originHeader, "origin");
}
@@ -80,7 +111,36 @@ export const originCheck = (
`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`,
`Current list of trustedOrigins: ${ctx.context.trustedOrigins}`,
);
- throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
+ if (label === "origin") {
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.INVALID_ORIGIN);
+ }
+ if (label === "callbackURL") {
+ throw APIError.from(
+ "FORBIDDEN",
+ BASE_ERROR_CODES.INVALID_CALLBACK_URL,
+ );
+ }
+ if (label === "redirectURL") {
+ throw APIError.from(
+ "FORBIDDEN",
+ BASE_ERROR_CODES.INVALID_REDIRECT_URL,
+ );
+ }
+ if (label === "errorCallbackURL") {
+ throw APIError.from(
+ "FORBIDDEN",
+ BASE_ERROR_CODES.INVALID_ERROR_CALLBACK_URL,
+ );
+ }
+ if (label === "newUserCallbackURL") {
+ throw APIError.from(
+ "FORBIDDEN",
+ BASE_ERROR_CODES.INVALID_NEW_USER_CALLBACK_URL,
+ );
+ }
+ throw APIError.fromStatus("FORBIDDEN", {
+ message: `Invalid ${label}`,
+ });
}
};
const callbacks = Array.isArray(callbackURL) ? callbackURL : [callbackURL];
diff --git a/packages/better-auth/src/api/routes/account.test.ts b/packages/better-auth/src/api/routes/account.test.ts
index 61e8c170c6..b93f3f5007 100644
--- a/packages/better-auth/src/api/routes/account.test.ts
+++ b/packages/better-auth/src/api/routes/account.test.ts
@@ -327,7 +327,7 @@ describe("account", async () => {
accountId: unlinkAccountId,
});
expect(unlinkRes.error?.message).toBe(
- BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT,
+ BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT.message,
);
});
});
diff --git a/packages/better-auth/src/api/routes/account.ts b/packages/better-auth/src/api/routes/account.ts
index 93f11f949b..1f7ed9044c 100644
--- a/packages/better-auth/src/api/routes/account.ts
+++ b/packages/better-auth/src/api/routes/account.ts
@@ -1,9 +1,9 @@
import { createAuthEndpoint } from "@better-auth/core/api";
import type { Account } from "@better-auth/core/db";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import type { OAuth2Tokens } from "@better-auth/core/oauth2";
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
-import { APIError } from "better-call";
+
import * as z from "zod";
import {
getAccountCookie,
@@ -224,9 +224,7 @@ export const linkSocialAccount = createAuthEndpoint(
provider: c.body.provider,
},
);
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
}
// Handle ID Token flow if provided
@@ -238,9 +236,10 @@ export const linkSocialAccount = createAuthEndpoint(
provider: c.body.provider,
},
);
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
- });
+ throw APIError.from(
+ "NOT_FOUND",
+ BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
+ );
}
const { token, nonce } = c.body.idToken;
@@ -249,9 +248,7 @@ export const linkSocialAccount = createAuthEndpoint(
c.context.logger.error("Invalid id token", {
provider: c.body.provider,
});
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.INVALID_TOKEN,
- });
+ throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
}
const linkingUserInfo = await provider.getUserInfo({
@@ -264,9 +261,10 @@ export const linkSocialAccount = createAuthEndpoint(
c.context.logger.error("Failed to get user info", {
provider: c.body.provider,
});
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
+ );
}
const linkingUserId = String(linkingUserInfo.user.id);
@@ -275,9 +273,10 @@ export const linkSocialAccount = createAuthEndpoint(
c.context.logger.error("User email not found", {
provider: c.body.provider,
});
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
+ );
}
const existingAccounts = await c.context.internalAdapter.findAccounts(
@@ -304,8 +303,9 @@ export const linkSocialAccount = createAuthEndpoint(
(!isTrustedProvider && !linkingUserInfo.user.emailVerified) ||
c.context.options.account?.accountLinking?.enabled === false
) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.from("UNAUTHORIZED", {
message: "Account not linked - linking not allowed",
+ code: "LINKING_NOT_ALLOWED",
});
}
@@ -313,8 +313,9 @@ export const linkSocialAccount = createAuthEndpoint(
linkingUserInfo.user.email !== session.user.email &&
c.context.options.account?.accountLinking?.allowDifferentEmails !== true
) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.from("UNAUTHORIZED", {
message: "Account not linked - different emails not allowed",
+ code: "LINKING_DIFFERENT_EMAILS_NOT_ALLOWED",
});
}
@@ -328,9 +329,10 @@ export const linkSocialAccount = createAuthEndpoint(
refreshToken: c.body.idToken.refreshToken,
scope: c.body.idToken.scopes?.join(","),
});
- } catch {
- throw new APIError("EXPECTATION_FAILED", {
+ } catch (_e: any) {
+ throw APIError.from("EXPECTATION_FAILED", {
message: "Account not linked - unable to create account",
+ code: "LINKING_FAILED",
});
}
@@ -418,9 +420,10 @@ export const unlinkAccount = createAuthEndpoint(
accounts.length === 1 &&
!ctx.context.options.account?.accountLinking?.allowUnlinkingAll
) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT,
+ );
}
const accountExist = accounts.find((account) =>
accountId
@@ -428,9 +431,7 @@ export const unlinkAccount = createAuthEndpoint(
: account.providerId === providerId,
);
if (!accountExist) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.ACCOUNT_NOT_FOUND,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
}
await ctx.context.internalAdapter.deleteAccount(accountExist.id);
return ctx.json({
@@ -515,8 +516,9 @@ export const getAccessToken = createAuthEndpoint(
throw ctx.error("UNAUTHORIZED");
}
if (!ctx.context.socialProviders.find((p) => p.id === providerId)) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: `Provider ${providerId} is not supported.`,
+ code: "PROVIDER_NOT_SUPPORTED",
});
}
const accountData = await getAccountCookie(ctx);
@@ -538,17 +540,13 @@ export const getAccessToken = createAuthEndpoint(
}
if (!account) {
- throw new APIError("BAD_REQUEST", {
- message: "Account not found",
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
}
const provider = ctx.context.socialProviders.find(
(p) => p.id === providerId,
);
if (!provider) {
- throw new APIError("BAD_REQUEST", {
- message: `Provider ${providerId} not found.`,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
}
try {
@@ -598,10 +596,10 @@ export const getAccessToken = createAuthEndpoint(
idToken: newTokens?.idToken ?? account.idToken ?? undefined,
};
return ctx.json(tokens);
- } catch (error) {
- throw new APIError("BAD_REQUEST", {
+ } catch (_error) {
+ throw APIError.from("BAD_REQUEST", {
message: "Failed to get a valid access token",
- cause: error,
+ code: "FAILED_TO_GET_ACCESS_TOKEN",
});
}
},
@@ -680,21 +678,24 @@ export const refreshToken = createAuthEndpoint(
}
let resolvedUserId = session?.user?.id || userId;
if (!resolvedUserId) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: `Either userId or session is required`,
+ code: "USER_ID_OR_SESSION_REQUIRED",
});
}
const provider = ctx.context.socialProviders.find(
(p) => p.id === providerId,
);
if (!provider) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: `Provider ${providerId} not found.`,
+ code: "PROVIDER_NOT_FOUND",
});
}
if (!provider.refreshAccessToken) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: `Provider ${providerId} does not support token refreshing.`,
+ code: "TOKEN_REFRESH_NOT_SUPPORTED",
});
}
@@ -717,9 +718,7 @@ export const refreshToken = createAuthEndpoint(
}
if (!account) {
- throw new APIError("BAD_REQUEST", {
- message: "Account not found",
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
}
let refreshToken: string | null | undefined = undefined;
@@ -730,8 +729,9 @@ export const refreshToken = createAuthEndpoint(
}
if (!refreshToken) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "Refresh token not found",
+ code: "REFRESH_TOKEN_NOT_FOUND",
});
}
@@ -783,10 +783,10 @@ export const refreshToken = createAuthEndpoint(
providerId: account.providerId,
accountId: account.accountId,
});
- } catch (error) {
- throw new APIError("BAD_REQUEST", {
+ } catch (_error) {
+ throw APIError.from("BAD_REQUEST", {
message: "Failed to refresh access token",
- cause: error,
+ code: "FAILED_TO_REFRESH_ACCESS_TOKEN",
});
}
},
@@ -877,9 +877,7 @@ export const accountInfo = createAuthEndpoint(
}
if (!account || account.userId !== ctx.context.session.user.id) {
- throw new APIError("BAD_REQUEST", {
- message: "Account not found",
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
}
const provider = ctx.context.socialProviders.find(
@@ -887,8 +885,9 @@ export const accountInfo = createAuthEndpoint(
);
if (!provider) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.from("INTERNAL_SERVER_ERROR", {
message: `Provider account provider is ${account.providerId} but it is not configured`,
+ code: "PROVIDER_NOT_CONFIGURED",
});
}
const tokens = await getAccessToken({
@@ -902,8 +901,9 @@ export const accountInfo = createAuthEndpoint(
returnStatus: false,
});
if (!tokens.accessToken) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "Access token not found",
+ code: "ACCESS_TOKEN_NOT_FOUND",
});
}
const info = await provider.getUserInfo({
diff --git a/packages/better-auth/src/api/routes/email-verification.ts b/packages/better-auth/src/api/routes/email-verification.ts
index 39cea4c628..2660e08df9 100644
--- a/packages/better-auth/src/api/routes/email-verification.ts
+++ b/packages/better-auth/src/api/routes/email-verification.ts
@@ -1,6 +1,6 @@
import type { GenericEndpointContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import type { JWTPayload, JWTVerifyResult } from "jose";
import { jwtVerify } from "jose";
import { JWTExpired } from "jose/errors";
@@ -48,9 +48,10 @@ export async function sendVerificationEmailFn(
) {
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
ctx.context.logger.error("Verification email isn't enabled.");
- throw new APIError("BAD_REQUEST", {
- message: "Verification email isn't enabled",
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.VERIFICATION_EMAIL_NOT_ENABLED,
+ );
}
const token = await createEmailVerificationToken(
ctx.context.secret,
@@ -160,9 +161,10 @@ export const sendVerificationEmail = createAuthEndpoint(
async (ctx) => {
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
ctx.context.logger.error("Verification email isn't enabled.");
- throw new APIError("BAD_REQUEST", {
- message: "Verification email isn't enabled",
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.VERIFICATION_EMAIL_NOT_ENABLED,
+ );
}
const { email } = ctx.body;
const session = await getSessionFromCtx(ctx);
@@ -186,15 +188,13 @@ export const sendVerificationEmail = createAuthEndpoint(
});
}
if (session?.user.emailVerified) {
- throw new APIError("BAD_REQUEST", {
- message:
- "You can only send a verification email to an unverified email",
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.EMAIL_ALREADY_VERIFIED,
+ );
}
if (session?.user.email !== email) {
- throw new APIError("BAD_REQUEST", {
- message: "You can only send a verification email to your own email",
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.EMAIL_MISMATCH);
}
await sendVerificationEmailFn(ctx, session.user);
return ctx.json({
@@ -271,16 +271,14 @@ export const verifyEmail = createAuthEndpoint(
},
},
async (ctx) => {
- function redirectOnError(error: string) {
+ function redirectOnError(error: { code: string; message: string }) {
if (ctx.query.callbackURL) {
if (ctx.query.callbackURL.includes("?")) {
- throw ctx.redirect(`${ctx.query.callbackURL}&error=${error}`);
+ throw ctx.redirect(`${ctx.query.callbackURL}&error=${error.code}`);
}
- throw ctx.redirect(`${ctx.query.callbackURL}?error=${error}`);
+ throw ctx.redirect(`${ctx.query.callbackURL}?error=${error.code}`);
}
- throw new APIError("UNAUTHORIZED", {
- message: error,
- });
+ throw APIError.from("UNAUTHORIZED", error);
}
const { token } = ctx.query;
let jwt: JWTVerifyResult;
@@ -294,9 +292,9 @@ export const verifyEmail = createAuthEndpoint(
);
} catch (e) {
if (e instanceof JWTExpired) {
- return redirectOnError("token_expired");
+ return redirectOnError(BASE_ERROR_CODES.TOKEN_EXPIRED);
}
- return redirectOnError("invalid_token");
+ return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
}
const schema = z.object({
email: z.email(),
@@ -308,12 +306,12 @@ export const verifyEmail = createAuthEndpoint(
parsed.email,
);
if (!user) {
- return redirectOnError("user_not_found");
+ return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
}
if (parsed.updateTo) {
let session = await getSessionFromCtx(ctx);
if (session && session.user.email !== parsed.email) {
- return redirectOnError("unauthorized");
+ return redirectOnError(BASE_ERROR_CODES.INVALID_USER);
}
if (parsed.requestType === "change-email-confirmation") {
const newToken = await createEmailVerificationToken(
@@ -356,9 +354,10 @@ export const verifyEmail = createAuthEndpoint(
user.user.id,
);
if (!newSession) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: "Failed to create session",
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
session = {
session: newSession,
@@ -479,9 +478,10 @@ export const verifyEmail = createAuthEndpoint(
user.user.id,
);
if (!session) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: "Failed to create session",
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
await setSessionCookie(ctx, {
session,
diff --git a/packages/better-auth/src/api/routes/reset-password.ts b/packages/better-auth/src/api/routes/reset-password.ts
index 2285bdc52f..b882d46df8 100644
--- a/packages/better-auth/src/api/routes/reset-password.ts
+++ b/packages/better-auth/src/api/routes/reset-password.ts
@@ -1,7 +1,6 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { generateId } from "../../utils";
import { getDate } from "../../utils/date";
@@ -89,8 +88,9 @@ export const requestPasswordReset = createAuthEndpoint(
ctx.context.logger.error(
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPassword function in your auth config!",
);
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "Reset password isn't enabled",
+ code: "RESET_PASSWORD_DISABLED",
});
}
const { email, redirectTo } = ctx.body;
@@ -271,9 +271,7 @@ export const resetPassword = createAuthEndpoint(
async (ctx) => {
const token = ctx.body.token || ctx.query?.token;
if (!token) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_TOKEN,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
}
const { newPassword } = ctx.body;
@@ -281,14 +279,10 @@ export const resetPassword = createAuthEndpoint(
const minLength = ctx.context.password?.config.minPasswordLength;
const maxLength = ctx.context.password?.config.maxPasswordLength;
if (newPassword.length < minLength) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
if (newPassword.length > maxLength) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const id = `reset-password:${token}`;
@@ -296,9 +290,7 @@ export const resetPassword = createAuthEndpoint(
const verification =
await ctx.context.internalAdapter.findVerificationValue(id);
if (!verification || verification.expiresAt < new Date()) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_TOKEN,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
}
const userId = verification.value;
const hashedPassword = await ctx.context.password.hash(newPassword);
diff --git a/packages/better-auth/src/api/routes/session.ts b/packages/better-auth/src/api/routes/session.ts
index 01b23c2f5c..e657e71ec0 100644
--- a/packages/better-auth/src/api/routes/session.ts
+++ b/packages/better-auth/src/api/routes/session.ts
@@ -6,12 +6,12 @@ import {
createAuthEndpoint,
createAuthMiddleware,
} from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { base64Url } from "@better-auth/utils/base64";
import { binary } from "@better-auth/utils/binary";
import { createHMAC } from "@better-auth/utils/hmac";
-import { APIError } from "better-call";
+
import * as z from "zod";
import {
deleteSessionCookie,
@@ -442,9 +442,10 @@ export const getSession = () =>
);
} catch (error) {
ctx.context.logger.error("INTERNAL_SERVER_ERROR", error);
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
+ );
}
},
);
@@ -494,7 +495,10 @@ export const getSessionFromCtx = async <
export const sessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.from("UNAUTHORIZED", {
+ message: "Unauthorized",
+ code: "UNAUTHORIZED",
+ });
}
return {
session,
@@ -509,7 +513,10 @@ export const sessionMiddleware = createAuthMiddleware(async (ctx) => {
export const sensitiveSessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
if (!session?.session) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.from("UNAUTHORIZED", {
+ message: "Unauthorized",
+ code: "UNAUTHORIZED",
+ });
}
return {
session,
@@ -524,7 +531,10 @@ export const requestOnlySessionMiddleware = createAuthMiddleware(
async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session && (ctx.request || ctx.headers)) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.from("UNAUTHORIZED", {
+ message: "Unauthorized",
+ code: "UNAUTHORIZED",
+ });
}
return { session };
},
@@ -540,7 +550,10 @@ export const requestOnlySessionMiddleware = createAuthMiddleware(
export const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.from("UNAUTHORIZED", {
+ message: "Unauthorized",
+ code: "UNAUTHORIZED",
+ });
}
if (ctx.context.sessionConfig.freshAge === 0) {
return {
@@ -554,9 +567,7 @@ export const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
const now = Date.now();
const isFresh = now - lastUpdated < freshAge * 1000;
if (!isFresh) {
- throw new APIError("FORBIDDEN", {
- message: "Session is not fresh",
- });
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.SESSION_NOT_FRESH);
}
return {
session,
@@ -683,7 +694,10 @@ export const revokeSession = createAuthEndpoint(
: "",
error,
);
- throw new APIError("INTERNAL_SERVER_ERROR");
+ throw APIError.from("INTERNAL_SERVER_ERROR", {
+ message: "Internal Server Error",
+ code: "INTERNAL_SERVER_ERROR",
+ });
}
}
return ctx.json({
@@ -738,7 +752,10 @@ export const revokeSessions = createAuthEndpoint(
: "",
error,
);
- throw new APIError("INTERNAL_SERVER_ERROR");
+ throw APIError.from("INTERNAL_SERVER_ERROR", {
+ message: "Internal Server Error",
+ code: "INTERNAL_SERVER_ERROR",
+ });
}
return ctx.json({
status: true,
@@ -782,7 +799,10 @@ export const revokeOtherSessions = createAuthEndpoint(
async (ctx) => {
const session = ctx.context.session;
if (!session.user) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.from("UNAUTHORIZED", {
+ message: "Unauthorized",
+ code: "UNAUTHORIZED",
+ });
}
const sessions = await ctx.context.internalAdapter.listSessions(
session.user.id,
diff --git a/packages/better-auth/src/api/routes/sign-in.test.ts b/packages/better-auth/src/api/routes/sign-in.test.ts
index 4d2a3b55a6..ac6a085431 100644
--- a/packages/better-auth/src/api/routes/sign-in.test.ts
+++ b/packages/better-auth/src/api/routes/sign-in.test.ts
@@ -1,5 +1,4 @@
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { describe, expect, vi } from "vitest";
import { parseSetCookieHeader } from "../../cookies";
import { getTestInstance } from "../../test-utils/test-instance";
@@ -70,9 +69,7 @@ describe("sign-in", async (it) => {
},
}),
).rejects.toThrowError(
- new APIError("FORBIDDEN", {
- message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
- }),
+ APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED),
);
expect(sendVerificationEmail).toHaveBeenCalledTimes(2);
@@ -101,9 +98,7 @@ describe("sign-in", async (it) => {
},
}),
).rejects.toThrowError(
- new APIError("FORBIDDEN", {
- message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
- }),
+ APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED),
);
expect(sendVerificationEmail).toHaveBeenCalledTimes(1);
diff --git a/packages/better-auth/src/api/routes/sign-in.ts b/packages/better-auth/src/api/routes/sign-in.ts
index 17b865f417..e11a17d29f 100644
--- a/packages/better-auth/src/api/routes/sign-in.ts
+++ b/packages/better-auth/src/api/routes/sign-in.ts
@@ -1,8 +1,7 @@
import type { BetterAuthOptions } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
-import { APIError } from "better-call";
import * as z from "zod";
import { setSessionCookie } from "../../cookies";
import { parseUserOutput } from "../../db/schema";
@@ -223,9 +222,7 @@ export const signInSocial = () =>
provider: c.body.provider,
},
);
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
}
if (c.body.idToken) {
@@ -236,9 +233,10 @@ export const signInSocial = () =>
provider: c.body.provider,
},
);
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
- });
+ throw APIError.from(
+ "NOT_FOUND",
+ BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
+ );
}
const { token, nonce } = c.body.idToken;
const valid = await provider.verifyIdToken(token, nonce);
@@ -246,9 +244,7 @@ export const signInSocial = () =>
c.context.logger.error("Invalid id token", {
provider: c.body.provider,
});
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.INVALID_TOKEN,
- });
+ throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
}
const userInfo = await provider.getUserInfo({
idToken: token,
@@ -259,17 +255,19 @@ export const signInSocial = () =>
c.context.logger.error("Failed to get user info", {
provider: c.body.provider,
});
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
+ );
}
if (!userInfo.user.email) {
c.context.logger.error("User email not found", {
provider: c.body.provider,
});
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
+ );
}
const data = await handleOAuthUserInfo(c, {
userInfo: {
@@ -291,8 +289,9 @@ export const signInSocial = () =>
provider.disableSignUp,
});
if (data.error) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.from("UNAUTHORIZED", {
message: data.error,
+ code: "OAUTH_LINK_ERROR",
});
}
await setSessionCookie(c, data.data!);
@@ -437,16 +436,15 @@ export const signInEmail = () =>
ctx.context.logger.error(
"Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://better-auth.com/docs/authentication/email-password` for more!",
);
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
+ code: "EMAIL_PASSWORD_DISABLED",
message: "Email and password is not enabled",
});
}
const { email, password } = ctx.body;
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
const user = await ctx.context.internalAdapter.findUserByEmail(email, {
includeAccounts: true,
@@ -457,9 +455,10 @@ export const signInEmail = () =>
// By hashing passwords for invalid emails, we ensure consistent response times
await ctx.context.password.hash(password);
ctx.context.logger.error("User not found", { email });
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
+ );
}
const credentialAccount = user.accounts.find(
@@ -468,17 +467,19 @@ export const signInEmail = () =>
if (!credentialAccount) {
await ctx.context.password.hash(password);
ctx.context.logger.error("Credential account not found", { email });
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
+ );
}
const currentPassword = credentialAccount?.password;
if (!currentPassword) {
await ctx.context.password.hash(password);
ctx.context.logger.error("Password not found", { email });
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
+ );
}
const validPassword = await ctx.context.password.verify({
hash: currentPassword,
@@ -486,9 +487,10 @@ export const signInEmail = () =>
});
if (!validPassword) {
ctx.context.logger.error("Invalid password");
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
+ );
}
if (
@@ -496,9 +498,7 @@ export const signInEmail = () =>
!user.user.emailVerified
) {
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) {
- throw new APIError("FORBIDDEN", {
- message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
- });
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
}
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
@@ -524,9 +524,7 @@ export const signInEmail = () =>
);
}
- throw new APIError("FORBIDDEN", {
- message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
- });
+ throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
}
const session = await ctx.context.internalAdapter.createSession(
@@ -536,9 +534,10 @@ export const signInEmail = () =>
if (!session) {
ctx.context.logger.error("Failed to create session");
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
await setSessionCookie(
diff --git a/packages/better-auth/src/api/routes/sign-up.ts b/packages/better-auth/src/api/routes/sign-up.ts
index 191c3f445d..e43630a4e6 100644
--- a/packages/better-auth/src/api/routes/sign-up.ts
+++ b/packages/better-auth/src/api/routes/sign-up.ts
@@ -2,13 +2,13 @@ import type { BetterAuthOptions } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { runWithTransaction } from "@better-auth/core/context";
import { isDevelopment } from "@better-auth/core/env";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { setSessionCookie } from "../../cookies";
import { parseUserInput } from "../../db";
import { parseUserOutput } from "../../db/schema";
import type { AdditionalUserFieldsInput, InferUser, User } from "../../types";
+import { isAPIError } from "../../utils/is-api-error";
import { createEmailVerificationToken } from "./email-verification";
const signUpEmailBodySchema = z
@@ -176,8 +176,9 @@ export const signUpEmail = () =>
!ctx.context.options.emailAndPassword?.enabled ||
ctx.context.options.emailAndPassword?.disableSignUp
) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "Email and password sign up is not enabled",
+ code: "EMAIL_PASSWORD_SIGN_UP_DISABLED",
});
}
const body = ctx.body as any as User & {
@@ -199,34 +200,35 @@ export const signUpEmail = () =>
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (password.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
+ );
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (password.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.PASSWORD_TOO_LONG,
+ );
}
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
if (dbUser?.user) {
ctx.context.logger.info(
`Sign-up attempt for existing email: ${email}`,
);
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
+ );
}
/**
* Hash the password
@@ -248,26 +250,29 @@ export const signUpEmail = () =>
emailVerified: false,
});
if (!createdUser) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
} catch (e) {
if (isDevelopment()) {
ctx.context.logger.error("Failed to create user", e);
}
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
throw e;
}
ctx.context.logger?.error("Failed to create user", e);
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
if (!createdUser) {
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
await ctx.context.internalAdapter.linkAccount({
userId: createdUser.id,
@@ -322,9 +327,10 @@ export const signUpEmail = () =>
rememberMe === false,
);
if (!session) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
await setSessionCookie(
ctx,
diff --git a/packages/better-auth/src/api/routes/update-user.ts b/packages/better-auth/src/api/routes/update-user.ts
index e46cddc6dc..371eb0748d 100644
--- a/packages/better-auth/src/api/routes/update-user.ts
+++ b/packages/better-auth/src/api/routes/update-user.ts
@@ -1,7 +1,6 @@
import type { BetterAuthOptions } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { deleteSessionCookie, setSessionCookie } from "../../cookies";
import { generateRandomString } from "../../crypto";
@@ -95,9 +94,10 @@ export const updateUser = () =>
}
if (body.email) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.EMAIL_CAN_NOT_BE_UPDATED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.EMAIL_CAN_NOT_BE_UPDATED,
+ );
}
const { name, image, ...rest } = body;
const session = ctx.context.session;
@@ -111,7 +111,7 @@ export const updateUser = () =>
name === undefined &&
Object.keys(additionalFields).length === 0
) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "No fields to update",
});
}
@@ -252,18 +252,14 @@ export const changePassword = createAuthEndpoint(
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (newPassword.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (newPassword.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const accounts = await ctx.context.internalAdapter.findAccounts(
@@ -273,9 +269,10 @@ export const changePassword = createAuthEndpoint(
(account) => account.providerId === "credential" && account.password,
);
if (!account || !account.password) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
+ );
}
const passwordHash = await ctx.context.password.hash(newPassword);
const verify = await ctx.context.password.verify({
@@ -283,9 +280,7 @@ export const changePassword = createAuthEndpoint(
password: currentPassword,
});
if (!verify) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_PASSWORD,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
}
await ctx.context.internalAdapter.updateAccount(account.id, {
password: passwordHash,
@@ -297,9 +292,10 @@ export const changePassword = createAuthEndpoint(
session.user.id,
);
if (!newSession) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
+ );
}
// set the new session cookie
await setSessionCookie(ctx, {
@@ -343,18 +339,14 @@ export const setPassword = createAuthEndpoint(
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (newPassword.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (newPassword.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const accounts = await ctx.context.internalAdapter.findAccounts(
@@ -375,8 +367,9 @@ export const setPassword = createAuthEndpoint(
status: true,
});
}
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "user already has a password",
+ code: "USER_ALREADY_HAS_PASSWORD",
});
},
);
@@ -480,7 +473,7 @@ export const deleteUser = createAuthEndpoint(
ctx.context.logger.error(
"Delete user is disabled. Enable it in the options",
);
- throw new APIError("NOT_FOUND");
+ throw APIError.fromStatus("NOT_FOUND");
}
const session = ctx.context.session;
@@ -492,18 +485,17 @@ export const deleteUser = createAuthEndpoint(
(account) => account.providerId === "credential" && account.password,
);
if (!account || !account.password) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
+ );
}
const verify = await ctx.context.password.verify({
hash: account.password,
password: ctx.body.password,
});
if (!verify) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_PASSWORD,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
}
}
@@ -559,9 +551,7 @@ export const deleteUser = createAuthEndpoint(
const freshAge = ctx.context.sessionConfig.freshAge * 1000;
const now = Date.now();
if (now - currentAge > freshAge * 1000) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.SESSION_EXPIRED,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.SESSION_EXPIRED);
}
}
@@ -635,26 +625,26 @@ export const deleteUserCallback = createAuthEndpoint(
ctx.context.logger.error(
"Delete user is disabled. Enable it in the options",
);
- throw new APIError("NOT_FOUND");
+ throw APIError.from("NOT_FOUND", {
+ message: "Not found",
+ code: "NOT_FOUND",
+ });
}
const session = await getSessionFromCtx(ctx);
if (!session) {
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
- });
+ throw APIError.from(
+ "NOT_FOUND",
+ BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
+ );
}
const token = await ctx.context.internalAdapter.findVerificationValue(
`delete-account-${ctx.query.token}`,
);
if (!token || token.expiresAt < new Date()) {
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.INVALID_TOKEN,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.INVALID_TOKEN);
}
if (token.value !== session.user.id) {
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.INVALID_TOKEN,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.INVALID_TOKEN);
}
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
if (beforeDelete) {
@@ -751,7 +741,7 @@ export const changeEmail = createAuthEndpoint(
async (ctx) => {
if (!ctx.context.options.user?.changeEmail?.enabled) {
ctx.context.logger.error("Change email is disabled.");
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Change email is disabled",
});
}
@@ -760,7 +750,7 @@ export const changeEmail = createAuthEndpoint(
if (newEmail === ctx.context.session.user.email) {
ctx.context.logger.error("Email is the same");
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Email is the same",
});
}
@@ -768,9 +758,10 @@ export const changeEmail = createAuthEndpoint(
await ctx.context.internalAdapter.findUserByEmail(newEmail);
if (existingUser) {
ctx.context.logger.error("Email already exists");
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
+ );
}
/**
@@ -869,7 +860,7 @@ export const changeEmail = createAuthEndpoint(
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
ctx.context.logger.error("Verification email isn't enabled.");
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Verification email isn't enabled",
});
}
diff --git a/packages/better-auth/src/api/to-auth-endpoints.test.ts b/packages/better-auth/src/api/to-auth-endpoints.test.ts
index 873e22a701..994d704cdf 100644
--- a/packages/better-auth/src/api/to-auth-endpoints.test.ts
+++ b/packages/better-auth/src/api/to-auth-endpoints.test.ts
@@ -2,11 +2,12 @@ import {
createAuthEndpoint,
createAuthMiddleware,
} from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import { describe, expect, it } from "vitest";
import * as z from "zod";
import { init } from "../context/init";
import { getTestInstance } from "../test-utils/test-instance";
+import { isAPIError } from "../utils/is-api-error";
import { toAuthEndpoints } from "./to-auth-endpoints";
describe("before hook", async () => {
@@ -337,7 +338,7 @@ describe("after hook", async () => {
},
})
.catch((e) => {
- expect(e).toBeInstanceOf(APIError);
+ expect(isAPIError(e)).toBeTruthy();
expect(e?.message).toBe("from after hook");
});
});
@@ -770,7 +771,7 @@ describe("debug mode stack trace", () => {
try {
await api.testEndpoint({});
} catch (error: any) {
- expect(error).toBeInstanceOf(APIError);
+ expect(isAPIError(error)).toBeTruthy();
expect(error.stack).toBeDefined();
expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
expect(error.stack).toMatch(/at\s+/);
@@ -799,7 +800,7 @@ describe("debug mode stack trace", () => {
try {
await api.testEndpoint({});
} catch (error: any) {
- expect(error).toBeInstanceOf(APIError);
+ expect(isAPIError(error)).toBeTruthy();
// Stack should exist but may be minimal when not in debug mode
expect(error.stack).toBeDefined();
}
@@ -829,7 +830,7 @@ describe("debug mode stack trace", () => {
try {
await api.testEndpoint({});
} catch (error: any) {
- expect(error).toBeInstanceOf(APIError);
+ expect(isAPIError(error)).toBeTruthy();
expect(error.stack).toBeDefined();
// Check for stack trace format
expect(error.stack).toMatch(/at\s+.*\(.*\)/); // Match "at functionName (file:line:col)"
@@ -864,7 +865,7 @@ describe("debug mode stack trace", () => {
try {
await api.testEndpoint({});
} catch (error: any) {
- expect(error).toBeInstanceOf(APIError);
+ expect(isAPIError(error)).toBeTruthy();
expect(error.stack).toBeDefined();
expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
expect(error.stack).toMatch(/at\s+/);
@@ -901,7 +902,7 @@ describe("debug mode stack trace", () => {
try {
await api.testEndpoint({ asResponse: false });
} catch (error: any) {
- expect(error).toBeInstanceOf(APIError);
+ expect(isAPIError(error)).toBeTruthy();
expect(error.stack).toBeDefined();
expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
}
diff --git a/packages/better-auth/src/api/to-auth-endpoints.ts b/packages/better-auth/src/api/to-auth-endpoints.ts
index e67ecdde2f..7fad0cf68d 100644
--- a/packages/better-auth/src/api/to-auth-endpoints.ts
+++ b/packages/better-auth/src/api/to-auth-endpoints.ts
@@ -11,8 +11,9 @@ import type {
EndpointOptions,
InputContext,
} from "better-call";
-import { APIError, toResponse } from "better-call";
+import { toResponse } from "better-call";
import { createDefu } from "defu";
+import { isAPIError } from "../utils/is-api-error";
type InternalContext = Partial<
InputContext & EndpointContext
@@ -114,7 +115,7 @@ export function toAuthEndpoints<
const result = (await runWithEndpointContext(internalContext, () =>
(endpoint as any)(internalContext as any),
).catch((e: any) => {
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
/**
* API Errors from response are caught
* and returned to hooks
@@ -147,14 +148,14 @@ export function toAuthEndpoints<
}
if (
- result.response instanceof APIError &&
+ isAPIError(result.response) &&
shouldPublishLog(authContext.logger.level, "debug")
) {
// inherit stack from errorStack if debug mode is enabled
result.response.stack = result.response.errorStack;
}
- if (result.response instanceof APIError && !context?.asResponse) {
+ if (isAPIError(result.response) && !context?.asResponse) {
throw result.response;
}
@@ -211,7 +212,7 @@ async function runBeforeHooks(
})
.catch((e: unknown) => {
if (
- e instanceof APIError &&
+ isAPIError(e) &&
shouldPublishLog(context.context.logger.level, "debug")
) {
// inherit stack from errorStack if debug mode is enabled
@@ -253,7 +254,7 @@ async function runAfterHooks(
for (const hook of hooks) {
if (hook.matcher(context)) {
const result = (await hook.handler(context).catch((e) => {
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
if (shouldPublishLog(context.context.logger.level, "debug")) {
// inherit stack from errorStack if debug mode is enabled
e.stack = e.errorStack;
diff --git a/packages/better-auth/src/auth/auth.test.ts b/packages/better-auth/src/auth/auth.test.ts
index 15f5dcec11..034ce16365 100644
--- a/packages/better-auth/src/auth/auth.test.ts
+++ b/packages/better-auth/src/auth/auth.test.ts
@@ -22,18 +22,20 @@ describe("auth type", () => {
{
id: "custom-plugin",
$ERROR_CODES: {
- CUSTOM_ERROR: "Custom error message",
+ CUSTOM_ERROR: {
+ code: "CUSTOM_ERROR",
+ message: "Custom error message",
+ },
},
},
],
});
type T = typeof auth.$ERROR_CODES;
- expectTypeOf().toEqualTypeOf<
- {
- CUSTOM_ERROR: string;
- } & typeof import("@better-auth/core/error").BASE_ERROR_CODES
- >();
+ expectTypeOf().toMatchTypeOf<{
+ code: string;
+ message: string;
+ }>();
});
test("plugin endpoints", () => {
diff --git a/packages/better-auth/src/call.test.ts b/packages/better-auth/src/call.test.ts
index 4f39b9c5dc..96288adbf1 100644
--- a/packages/better-auth/src/call.test.ts
+++ b/packages/better-auth/src/call.test.ts
@@ -3,13 +3,14 @@ import {
createAuthEndpoint,
createAuthMiddleware,
} from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import { describe, expect, it } from "vitest";
import * as z from "zod";
import { getEndpoints, router } from "./api";
import { createAuthClient } from "./client";
import { init } from "./context/init";
import { bearer } from "./plugins";
+import { isAPIError } from "./utils/is-api-error";
describe("call", async () => {
const q = z.optional(
@@ -184,7 +185,7 @@ describe("call", async () => {
message: "from chained hook 1",
});
}
- if (ctx.context.returned instanceof APIError) {
+ if (isAPIError(ctx.context.returned)) {
throw ctx.error("BAD_REQUEST", {
message: "from after hook",
});
@@ -199,7 +200,7 @@ describe("call", async () => {
);
},
handler: createAuthMiddleware(async (ctx) => {
- if (ctx.context.returned instanceof APIError) {
+ if (isAPIError(ctx.context.returned)) {
const returned = ctx.context.returned;
const message = returned.message;
throw new APIError("BAD_REQUEST", {
@@ -376,7 +377,7 @@ describe("call", async () => {
},
})
.catch((e) => {
- expect(e).toBeInstanceOf(APIError);
+ expect(isAPIError(e)).toBeTruthy();
expect(e.status).toBe("FOUND");
expect(e.headers.get("Location")).toBe("/test");
@@ -391,7 +392,7 @@ describe("call", async () => {
},
})
.catch((e) => {
- expect(e).toBeInstanceOf(APIError);
+ expect(isAPIError(e)).toBeTruthy();
expect(e.status).toBe("FOUND");
expect(e.headers.get("Location")).toBe("/test");
expect(e.headers.get("key")).toBe("value");
@@ -406,7 +407,7 @@ describe("call", async () => {
},
})
.catch((e) => {
- expect(e).toBeInstanceOf(APIError);
+ expect(isAPIError(e)).toBeTruthy();
expect(e.status).toBe("BAD_REQUEST");
expect(e.message).toContain("from after hook");
});
@@ -420,7 +421,7 @@ describe("call", async () => {
},
})
.catch((e) => {
- expect(e).toBeInstanceOf(APIError);
+ expect(isAPIError(e)).toBeTruthy();
expect(e.status).toBe("BAD_REQUEST");
expect(e.message).toContain("from chained hook 2");
});
diff --git a/packages/better-auth/src/client/client.test.ts b/packages/better-auth/src/client/client.test.ts
index e34cce9e1c..4ea2d1046b 100644
--- a/packages/better-auth/src/client/client.test.ts
+++ b/packages/better-auth/src/client/client.test.ts
@@ -425,42 +425,42 @@ describe("type", () => {
// Should have organization error codes
expectTypeOf(
- client.$ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ client.$ERROR_CODES.ORGANIZATION_NOT_FOUND.message,
).toEqualTypeOf<"Organization not found">();
// Should have two-factor error codes
expectTypeOf(
- client.$ERROR_CODES.OTP_HAS_EXPIRED,
+ client.$ERROR_CODES.OTP_HAS_EXPIRED.message,
).toEqualTypeOf<"OTP has expired">();
// Should have email-otp error codes
expectTypeOf(
- client.$ERROR_CODES.INVALID_EMAIL,
+ client.$ERROR_CODES.INVALID_EMAIL.message,
).toEqualTypeOf<"Invalid email">();
// Should have admin error codes
expectTypeOf(
- client.$ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
+ client.$ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS.message,
).toEqualTypeOf<"You are not allowed to revoke users sessions">();
// Should have multi-session error codes
expectTypeOf(
- client.$ERROR_CODES.INVALID_SESSION_TOKEN,
+ client.$ERROR_CODES.INVALID_SESSION_TOKEN.message,
).toEqualTypeOf<"Invalid session token">();
// Should have generic-oauth error codes
expectTypeOf(
- client.$ERROR_CODES.PROVIDER_NOT_FOUND,
+ client.$ERROR_CODES.PROVIDER_NOT_FOUND.message,
).toEqualTypeOf<"Provider not found">();
// Should have device-authorization error codes
expectTypeOf(
- client.$ERROR_CODES.INVALID_DEVICE_CODE,
+ client.$ERROR_CODES.INVALID_DEVICE_CODE.message,
).toEqualTypeOf<"Invalid device code">();
// Should have base error codes
expectTypeOf(
- client.$ERROR_CODES.USER_NOT_FOUND,
+ client.$ERROR_CODES.USER_NOT_FOUND.message,
).toEqualTypeOf<"User not found">();
});
});
diff --git a/packages/better-auth/src/client/types.ts b/packages/better-auth/src/client/types.ts
index d2b7beb7b2..fc463dbeb4 100644
--- a/packages/better-auth/src/client/types.ts
+++ b/packages/better-auth/src/client/types.ts
@@ -79,7 +79,13 @@ export type InferErrorCodes =
? UnionToIntersection<
Plugin extends BetterAuthClientPlugin
? Plugin["$InferServerPlugin"] extends { $ERROR_CODES: infer E }
- ? E extends Record
+ ? E extends Record<
+ string,
+ {
+ code: string;
+ message: string;
+ }
+ >
? E
: {}
: {}
diff --git a/packages/better-auth/src/db/schema.ts b/packages/better-auth/src/db/schema.ts
index e4793a2d93..855e733abc 100644
--- a/packages/better-auth/src/db/schema.ts
+++ b/packages/better-auth/src/db/schema.ts
@@ -3,7 +3,7 @@ import type {
BetterAuthPluginDBSchema,
DBFieldAttribute,
} from "@better-auth/core/db";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import type { Account, Session, User } from "../types";
// Cache for parsed schemas to avoid reparsing on every request
@@ -105,7 +105,8 @@ export function parseInputData>(
}
}
if (data[key]) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
+ ...BASE_ERROR_CODES.FIELD_NOT_ALLOWED,
message: `${key} is not allowed to be set`,
});
}
@@ -116,12 +117,14 @@ export function parseInputData>(
data[key],
);
if (result instanceof Promise) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: "Async validation is not supported for additional fields",
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.ASYNC_VALIDATION_NOT_SUPPORTED,
+ );
}
if ("issues" in result && result.issues) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
+ ...BASE_ERROR_CODES.VALIDATION_ERROR,
message: result.issues[0]?.message || "Validation Error",
});
}
@@ -146,7 +149,8 @@ export function parseInputData>(
}
if (fields[key]!.required && action === "create") {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
+ ...BASE_ERROR_CODES.MISSING_FIELD,
message: `${key} is required`,
});
}
diff --git a/packages/better-auth/src/oauth2/link-account.ts b/packages/better-auth/src/oauth2/link-account.ts
index a60bde3d26..a6172179d6 100644
--- a/packages/better-auth/src/oauth2/link-account.ts
+++ b/packages/better-auth/src/oauth2/link-account.ts
@@ -1,8 +1,9 @@
import type { GenericEndpointContext } from "@better-auth/core";
import { isDevelopment, logger } from "@better-auth/core/env";
-import { APIError, createEmailVerificationToken } from "../api";
+import { createEmailVerificationToken } from "../api";
import { setAccountCookie } from "../cookies/session-store";
import type { Account, User } from "../types";
+import { isAPIError } from "../utils/is-api-error";
import { setTokenUtil } from "./utils";
export async function handleOAuthUserInfo(
@@ -198,7 +199,7 @@ export async function handleOAuthUserInfo(
}
} catch (e: any) {
logger.error(e);
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
return {
error: e.message,
data: null,
diff --git a/packages/better-auth/src/oauth2/state.ts b/packages/better-auth/src/oauth2/state.ts
index 00ad2a9cf6..9f47f0db3d 100644
--- a/packages/better-auth/src/oauth2/state.ts
+++ b/packages/better-auth/src/oauth2/state.ts
@@ -1,5 +1,5 @@
import type { GenericEndpointContext } from "@better-auth/core";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { setOAuthState } from "../api/middlewares/oauth";
import {
@@ -20,9 +20,7 @@ export async function generateState(
) {
const callbackURL = c.body?.callbackURL || c.context.options.baseURL;
if (!callbackURL) {
- throw new APIError("BAD_REQUEST", {
- message: "callbackURL is required",
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CALLBACK_URL_REQUIRED);
}
const codeVerifier = generateRandomString(128);
@@ -87,9 +85,10 @@ export async function generateState(
c.context.logger.error(
"Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database",
);
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: "Unable to create verification",
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_VERIFICATION,
+ );
}
return {
state: verification.identifier,
diff --git a/packages/better-auth/src/plugins/admin/admin.test.ts b/packages/better-auth/src/plugins/admin/admin.test.ts
index 1ab071ef43..9475670e89 100644
--- a/packages/better-auth/src/plugins/admin/admin.test.ts
+++ b/packages/better-auth/src/plugins/admin/admin.test.ts
@@ -17,7 +17,7 @@ import { getTestInstance } from "../../test-utils/test-instance";
import { DEFAULT_SECRET } from "../../utils/constants";
import { createAccessControl } from "../access";
import { admin } from "./admin";
-import { adminClient } from "./client";
+import { ADMIN_ERROR_CODES, adminClient } from "./client";
import type { UserWithRole } from "./types";
let testIdToken: string;
@@ -1398,7 +1398,7 @@ describe("access control", async (it) => {
expect(res.error).toBeDefined();
expect(res.error?.status).toBe(400);
expect(res.error?.code).toBe(
- "YOU_ARE_NOT_ALLOWED_TO_SET_A_NONEXISTENT_ROLE_VALUE",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE.code,
);
await client.admin.removeUser(
{ userId: createdUser.data?.user.id || "" },
@@ -1429,7 +1429,7 @@ describe("access control", async (it) => {
expect(res.error).toBeDefined();
expect(res.error?.status).toBe(400);
expect(res.error?.code).toBe(
- "YOU_ARE_NOT_ALLOWED_TO_SET_A_NONEXISTENT_ROLE_VALUE",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE.code,
);
await client.admin.removeUser(
{ userId: createdUser.data?.user.id || "" },
diff --git a/packages/better-auth/src/plugins/admin/admin.ts b/packages/better-auth/src/plugins/admin/admin.ts
index dd460baec4..9156f8c526 100644
--- a/packages/better-auth/src/plugins/admin/admin.ts
+++ b/packages/better-auth/src/plugins/admin/admin.ts
@@ -1,7 +1,6 @@
import type { BetterAuthPlugin } from "@better-auth/core";
import { createAuthMiddleware } from "@better-auth/core/api";
-import { BetterAuthError } from "@better-auth/core/error";
-import { APIError } from "../../api";
+import { APIError, BetterAuthError } from "@better-auth/core/error";
import { mergeSchema } from "../../db/schema";
import { getEndpointResponse } from "../../utils/plugin-helper";
import { defaultRoles } from "./access";
@@ -114,7 +113,7 @@ export const admin = (options?: O | undefined) => {
);
}
- throw new APIError("FORBIDDEN", {
+ throw APIError.from("FORBIDDEN", {
message: opts.bannedUserMessage,
code: "BANNED_USER",
});
diff --git a/packages/better-auth/src/plugins/admin/client.ts b/packages/better-auth/src/plugins/admin/client.ts
index 485e29506a..9ba0982628 100644
--- a/packages/better-auth/src/plugins/admin/client.ts
+++ b/packages/better-auth/src/plugins/admin/client.ts
@@ -3,8 +3,11 @@ import type { AccessControl, Role } from "../access";
import type { defaultStatements } from "./access";
import { adminAc, userAc } from "./access";
import type { admin } from "./admin";
+import { ADMIN_ERROR_CODES } from "./error-codes";
import { hasPermission } from "./has-permission";
+export * from "./error-codes";
+
interface AdminClientOptions {
ac?: AccessControl | undefined;
roles?:
@@ -88,6 +91,7 @@ export const adminClient = (
"/admin/list-users": "GET",
"/admin/stop-impersonating": "POST",
},
+ $ERROR_CODES: ADMIN_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/admin/error-codes.ts b/packages/better-auth/src/plugins/admin/error-codes.ts
index 02df15d837..2b386a5f6e 100644
--- a/packages/better-auth/src/plugins/admin/error-codes.ts
+++ b/packages/better-auth/src/plugins/admin/error-codes.ts
@@ -1,4 +1,3 @@
-// NOTE: Error code const must be all capital of string (ref https://github.com/better-auth/better-auth/issues/4386)
import { defineErrorCodes } from "@better-auth/core/utils";
export const ADMIN_ERROR_CODES = defineErrorCodes({
diff --git a/packages/better-auth/src/plugins/admin/routes.ts b/packages/better-auth/src/plugins/admin/routes.ts
index 77b797b519..e530a0e739 100644
--- a/packages/better-auth/src/plugins/admin/routes.ts
+++ b/packages/better-auth/src/plugins/admin/routes.ts
@@ -4,9 +4,9 @@ import {
} from "@better-auth/core/api";
import type { Session } from "@better-auth/core/db";
import type { Where } from "@better-auth/core/db/adapter";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
-import { APIError, getSessionFromCtx } from "../../api";
+import { getSessionFromCtx } from "../../api";
import { deleteSessionCookie, setSessionCookie } from "../../cookies";
import { parseUserOutput } from "../../db/schema";
import { getDate } from "../../utils/date";
@@ -28,7 +28,7 @@ import type {
const adminMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.fromStatus("UNAUTHORIZED");
}
return {
session,
@@ -129,9 +129,10 @@ export const setRole = (opts: O) =>
},
});
if (!canSetRole) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE,
+ );
}
const roles = opts.roles;
if (roles) {
@@ -140,10 +141,10 @@ export const setRole = (opts: O) =>
: [ctx.body.role];
for (const role of inputRoles) {
if (!roles[role as keyof typeof roles]) {
- throw new APIError("BAD_REQUEST", {
- message:
- ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE,
+ );
}
}
}
@@ -210,18 +211,16 @@ export const getUser = (opts: AdminOptions) =>
});
if (!canGetUser) {
- throw ctx.error("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_USER,
- code: "YOU_ARE_NOT_ALLOWED_TO_GET_USER",
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_USER,
+ );
}
const user = await ctx.context.internalAdapter.findUserById(id);
if (!user) {
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
}
return parseUserOutput(ctx.context.options, user);
@@ -334,26 +333,26 @@ export const createUser = (opts: O) =>
},
});
if (!canCreateUser) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS,
+ );
}
}
const email = ctx.body.email.toLowerCase();
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
const existUser =
await ctx.context.internalAdapter.findUserByEmail(email);
if (existUser) {
- throw new APIError("BAD_REQUEST", {
- message: ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
+ );
}
const user = await ctx.context.internalAdapter.createUser({
email: email,
@@ -366,9 +365,10 @@ export const createUser = (opts: O) =>
});
if (!user) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
const hashedPassword = await ctx.context.password.hash(ctx.body.password);
await ctx.context.internalAdapter.linkAccount({
@@ -449,16 +449,14 @@ export const adminUpdateUser = (opts: AdminOptions) =>
},
});
if (!canUpdateUser) {
- throw ctx.error("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS,
- code: "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS",
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS,
+ );
}
if (Object.keys(ctx.body.data).length === 0) {
- throw new APIError("BAD_REQUEST", {
- message: ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE,
- });
+ throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE);
}
// Role changes must be guarded by `user:set-role` and validated against the role allow-list.
@@ -472,24 +470,26 @@ export const adminUpdateUser = (opts: AdminOptions) =>
},
});
if (!canSetRole) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE,
+ );
}
const roleValue = (ctx.body.data as Record).role;
const inputRoles = Array.isArray(roleValue) ? roleValue : [roleValue];
for (const role of inputRoles) {
if (typeof role !== "string") {
- throw new APIError("BAD_REQUEST", {
- message: ADMIN_ERROR_CODES.INVALID_ROLE_TYPE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ADMIN_ERROR_CODES.INVALID_ROLE_TYPE,
+ );
}
if (opts.roles && !opts.roles[role as keyof typeof opts.roles]) {
- throw new APIError("BAD_REQUEST", {
- message:
- ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE,
+ );
}
}
(ctx.body.data as Record).role = parseRoles(
@@ -627,9 +627,10 @@ export const listUsers = (opts: AdminOptions) =>
},
});
if (!canListUsers) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS,
+ );
}
const where: Where[] = [];
@@ -747,9 +748,10 @@ export const listUserSessions = (opts: AdminOptions) =>
},
});
if (!canListSessions) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS,
+ );
}
const sessions: SessionWithImpersonatedBy[] =
@@ -824,9 +826,10 @@ export const unbanUser = (opts: AdminOptions) =>
},
});
if (!canBanUser) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS,
+ );
}
const user = await ctx.context.internalAdapter.updateUser(
@@ -926,9 +929,10 @@ export const banUser = (opts: AdminOptions) =>
},
});
if (!canBanUser) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS,
+ );
}
const foundUser = await ctx.context.internalAdapter.findUserById(
@@ -936,15 +940,14 @@ export const banUser = (opts: AdminOptions) =>
);
if (!foundUser) {
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
}
if (ctx.body.userId === ctx.context.session.user.id) {
- throw new APIError("BAD_REQUEST", {
- message: ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF,
+ );
}
const user = await ctx.context.internalAdapter.updateUser(
ctx.body.userId,
@@ -1033,9 +1036,10 @@ export const impersonateUser = (opts: AdminOptions) =>
},
});
if (!canImpersonateUser) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS,
+ );
}
const targetUser = (await ctx.context.internalAdapter.findUserById(
@@ -1043,9 +1047,7 @@ export const impersonateUser = (opts: AdminOptions) =>
)) as UserWithRole | null;
if (!targetUser) {
- throw new APIError("NOT_FOUND", {
- message: "User not found",
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const adminRoles = (
@@ -1063,9 +1065,10 @@ export const impersonateUser = (opts: AdminOptions) =>
(targetUserRole.some((role) => adminRoles.includes(role)) ||
opts.adminUserIds?.includes(targetUser.id))
) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_CANNOT_IMPERSONATE_ADMINS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_CANNOT_IMPERSONATE_ADMINS,
+ );
}
const session = await ctx.context.internalAdapter.createSession(
@@ -1080,9 +1083,10 @@ export const impersonateUser = (opts: AdminOptions) =>
true,
);
if (!session) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
const authCookies = ctx.context.authCookies;
deleteSessionCookie(ctx);
@@ -1142,10 +1146,10 @@ export const stopImpersonating = () =>
}
>(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.fromStatus("UNAUTHORIZED");
}
if (!session.session.impersonatedBy) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "You are not impersonating anyone",
});
}
@@ -1153,7 +1157,7 @@ export const stopImpersonating = () =>
session.session.impersonatedBy,
);
if (!user) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.fromStatus("INTERNAL_SERVER_ERROR", {
message: "Failed to find user",
});
}
@@ -1165,7 +1169,7 @@ export const stopImpersonating = () =>
);
if (!adminCookie) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.fromStatus("INTERNAL_SERVER_ERROR", {
message: "Failed to find admin session",
});
}
@@ -1174,7 +1178,7 @@ export const stopImpersonating = () =>
adminSessionToken!,
);
if (!adminSession || adminSession.session.userId !== user.id) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.fromStatus("INTERNAL_SERVER_ERROR", {
message: "Failed to find admin session",
});
}
@@ -1251,10 +1255,10 @@ export const revokeUserSession = (opts: AdminOptions) =>
},
});
if (!canRevokeSession) {
- throw new APIError("FORBIDDEN", {
- message:
- ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
+ );
}
await ctx.context.internalAdapter.deleteSession(ctx.body.sessionToken);
@@ -1327,10 +1331,10 @@ export const revokeUserSessions = (opts: AdminOptions) =>
},
});
if (!canRevokeSession) {
- throw new APIError("FORBIDDEN", {
- message:
- ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
+ );
}
await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
@@ -1405,15 +1409,17 @@ export const removeUser = (opts: AdminOptions) =>
},
});
if (!canDeleteUser) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS,
+ );
}
if (ctx.body.userId === ctx.context.session.user.id) {
- throw new APIError("BAD_REQUEST", {
- message: ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF,
+ );
}
const user = await ctx.context.internalAdapter.findUserById(
@@ -1421,9 +1427,7 @@ export const removeUser = (opts: AdminOptions) =>
);
if (!user) {
- throw new APIError("NOT_FOUND", {
- message: "User not found",
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
}
await ctx.context.internalAdapter.deleteUser(ctx.body.userId);
@@ -1499,25 +1503,22 @@ export const setUserPassword = (opts: AdminOptions) =>
},
});
if (!canSetUserPassword) {
- throw new APIError("FORBIDDEN", {
- message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD,
+ );
}
const { newPassword, userId } = ctx.body;
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (newPassword.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (newPassword.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const hashedPassword = await ctx.context.password.hash(newPassword);
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
diff --git a/packages/better-auth/src/plugins/anonymous/client.ts b/packages/better-auth/src/plugins/anonymous/client.ts
index c47b8e923b..b92d9df6a7 100644
--- a/packages/better-auth/src/plugins/anonymous/client.ts
+++ b/packages/better-auth/src/plugins/anonymous/client.ts
@@ -1,5 +1,6 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { anonymous } from ".";
+import { ANONYMOUS_ERROR_CODES } from "./error-codes";
export const anonymousClient = () => {
return {
@@ -14,8 +15,10 @@ export const anonymousClient = () => {
signal: "$sessionSignal",
},
],
+ $ERROR_CODES: ANONYMOUS_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
+export * from "./error-codes";
export type * from "./schema";
export type * from "./types";
diff --git a/packages/better-auth/src/plugins/anonymous/index.ts b/packages/better-auth/src/plugins/anonymous/index.ts
index 2615765cd2..42fd3c622c 100644
--- a/packages/better-auth/src/plugins/anonymous/index.ts
+++ b/packages/better-auth/src/plugins/anonymous/index.ts
@@ -19,9 +19,10 @@ async function getAnonUserEmail(
if (customEmail) {
const validation = z.email().safeParse(customEmail);
if (!validation.success) {
- throw new APIError("BAD_REQUEST", {
- message: ANONYMOUS_ERROR_CODES.INVALID_EMAIL_FORMAT,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ANONYMOUS_ERROR_CODES.INVALID_EMAIL_FORMAT,
+ );
}
return customEmail;
}
@@ -77,10 +78,10 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
isAnonymous: boolean;
}>(ctx, { disableRefresh: true });
if (existingSession?.user.isAnonymous) {
- throw new APIError("BAD_REQUEST", {
- message:
- ANONYMOUS_ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ANONYMOUS_ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
+ );
}
const email = await getAnonUserEmail(options);
@@ -94,9 +95,10 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
updatedAt: new Date(),
});
if (!newUser) {
- throw ctx.error("INTERNAL_SERVER_ERROR", {
- message: ANONYMOUS_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ ANONYMOUS_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
const session = await ctx.context.internalAdapter.createSession(
newUser.id,
@@ -105,7 +107,7 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
return ctx.json(null, {
status: 400,
body: {
- message: ANONYMOUS_ERROR_CODES.COULD_NOT_CREATE_SESSION,
+ message: ANONYMOUS_ERROR_CODES.COULD_NOT_CREATE_SESSION.message,
},
});
}
@@ -176,10 +178,10 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
}
if (ctx.path === "/sign-in/anonymous" && !ctx.context.newSession) {
- throw new APIError("BAD_REQUEST", {
- message:
- ANONYMOUS_ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ANONYMOUS_ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
+ );
}
const newSession = ctx.context.newSession;
if (!newSession) {
diff --git a/packages/better-auth/src/plugins/api-key/api-key.test.ts b/packages/better-auth/src/plugins/api-key/api-key.test.ts
index c1ca1738a8..a7e56d4a7f 100644
--- a/packages/better-auth/src/plugins/api-key/api-key.test.ts
+++ b/packages/better-auth/src/plugins/api-key/api-key.test.ts
@@ -1,7 +1,8 @@
-import { APIError } from "better-call";
+import type { APIError } from "@better-auth/core/error";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
-import { apiKey, ERROR_CODES } from ".";
+import { isAPIError } from "../../utils/is-api-error";
+import { apiKey, API_KEY_ERROR_CODES as ERROR_CODES } from ".";
import { apiKeyClient } from "./client";
import type { ApiKey } from "./types";
@@ -38,7 +39,9 @@ describe("api-key", async () => {
expect(apiKeyFail.error).toBeDefined();
expect(apiKeyFail.error?.status).toEqual(401);
expect(apiKeyFail.error?.statusText).toEqual("UNAUTHORIZED");
- expect(apiKeyFail.error?.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION);
+ expect(apiKeyFail.error?.message).toEqual(
+ ERROR_CODES.UNAUTHORIZED_SESSION.message,
+ );
});
let firstApiKey: ApiKey;
@@ -95,7 +98,9 @@ describe("api-key", async () => {
expect(res.error).toBeDefined();
expect(res.error?.statusCode).toEqual(401);
expect(res.error?.status).toEqual("UNAUTHORIZED");
- expect(res.error?.body.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION);
+ expect(res.error?.body.message).toEqual(
+ ERROR_CODES.UNAUTHORIZED_SESSION.message,
+ );
});
it("should fail to create api keys from the client if user id is provided", async () => {
@@ -198,7 +203,7 @@ describe("api-key", async () => {
err = error;
}
expect(err).toBeDefined();
- expect(err.body.message).toBe(ERROR_CODES.NAME_REQUIRED);
+ expect(err.body.message).toBe(ERROR_CODES.NAME_REQUIRED.message);
});
it("should respect rateLimit configuration from plugin options", async () => {
@@ -266,7 +271,9 @@ describe("api-key", async () => {
expect(result.data).toBeNull();
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
- expect(result.error?.body.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
+ expect(result.error?.body.message).toEqual(
+ ERROR_CODES.INVALID_NAME_LENGTH.message,
+ );
});
it("should create the API key with a name that's longer than the allowed maximum", async () => {
@@ -288,7 +295,9 @@ describe("api-key", async () => {
expect(result.data).toBeNull();
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
- expect(result.error?.body.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
+ expect(result.error?.body.message).toEqual(
+ ERROR_CODES.INVALID_NAME_LENGTH.message,
+ );
});
it("should create the API key with the given prefix", async () => {
@@ -325,7 +334,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.INVALID_PREFIX_LENGTH,
+ ERROR_CODES.INVALID_PREFIX_LENGTH.message,
);
});
@@ -349,7 +358,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.INVALID_PREFIX_LENGTH,
+ ERROR_CODES.INVALID_PREFIX_LENGTH.message,
);
});
@@ -465,7 +474,7 @@ describe("api-key", async () => {
expect(result.data).toBeNull();
expect(result.error).toBeDefined();
expect(result.error?.body.message).toEqual(
- ERROR_CODES.KEY_DISABLED_EXPIRATION,
+ ERROR_CODES.KEY_DISABLED_EXPIRATION.message,
);
});
@@ -490,7 +499,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
+ ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL.message,
);
});
@@ -515,7 +524,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
+ ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE.message,
);
});
@@ -530,7 +539,9 @@ describe("api-key", async () => {
expect(apiKey.data).toBeNull();
expect(apiKey.error).toBeDefined();
expect(apiKey.error?.statusText).toEqual("BAD_REQUEST");
- expect(apiKey.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);
+ expect(apiKey.error?.message).toEqual(
+ ERROR_CODES.SERVER_ONLY_PROPERTY.message,
+ );
const apiKey2 = await client.apiKey.create(
{
@@ -542,7 +553,9 @@ describe("api-key", async () => {
expect(apiKey2.data).toBeNull();
expect(apiKey2.error).toBeDefined();
expect(apiKey2.error?.statusText).toEqual("BAD_REQUEST");
- expect(apiKey2.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);
+ expect(apiKey2.error?.message).toEqual(
+ ERROR_CODES.SERVER_ONLY_PROPERTY.message,
+ );
});
it("should fail to create API key when refill interval is provided, but no refill amount", async () => {
@@ -566,7 +579,7 @@ describe("api-key", async () => {
expect(res.error).toBeDefined();
expect(res.error?.status).toEqual("BAD_REQUEST");
expect(res.error?.body.message).toEqual(
- ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
+ ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED.message,
);
});
@@ -591,7 +604,7 @@ describe("api-key", async () => {
expect(res.error).toBeDefined();
expect(res.error?.status).toEqual("BAD_REQUEST");
expect(res.error?.body.message).toEqual(
- ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
+ ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED.message,
);
});
@@ -710,7 +723,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.INVALID_METADATA_TYPE,
+ ERROR_CODES.INVALID_METADATA_TYPE.message,
);
});
@@ -796,7 +809,9 @@ describe("api-key", async () => {
expect(result.data).toBeNull();
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
- expect(result.error?.body.message).toEqual(ERROR_CODES.METADATA_DISABLED);
+ expect(result.error?.body.message).toEqual(
+ ERROR_CODES.METADATA_DISABLED.message,
+ );
});
it("should have the first 6 characters of the key as the start property", async () => {
@@ -879,7 +894,9 @@ describe("api-key", async () => {
expect(apiKey.data).toBeNull();
expect(apiKey.error).toBeDefined();
expect(apiKey.error?.statusText).toEqual("BAD_REQUEST");
- expect(apiKey.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);
+ expect(apiKey.error?.message).toEqual(
+ ERROR_CODES.SERVER_ONLY_PROPERTY.message,
+ );
const apiKey2 = await client.apiKey.create(
{
@@ -891,7 +908,9 @@ describe("api-key", async () => {
expect(apiKey2.data).toBeNull();
expect(apiKey2.error).toBeDefined();
expect(apiKey2.error?.statusText).toEqual("BAD_REQUEST");
- expect(apiKey2.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);
+ expect(apiKey2.error?.message).toEqual(
+ ERROR_CODES.SERVER_ONLY_PROPERTY.message,
+ );
});
it("should successfully apply custom rate-limit options on the newly created API key", async () => {
@@ -1098,7 +1117,9 @@ describe("api-key", async () => {
expect(res.error).toBeDefined();
expect(res.error?.statusCode).toEqual(401);
expect(res.error?.status).toEqual("UNAUTHORIZED");
- expect(res.error?.body.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION);
+ expect(res.error?.body.message).toEqual(
+ ERROR_CODES.UNAUTHORIZED_SESSION.message,
+ );
});
it("should update API key name with headers", async () => {
@@ -1126,10 +1147,12 @@ describe("api-key", async () => {
headers,
})
.catch((e) => {
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
error = e;
expect(error?.status).toEqual("BAD_REQUEST");
- expect(error?.body?.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
+ expect(error?.body?.message).toEqual(
+ ERROR_CODES.INVALID_NAME_LENGTH.message,
+ );
}
});
expect(error).not.toBeNull();
@@ -1146,10 +1169,12 @@ describe("api-key", async () => {
headers,
})
.catch((e) => {
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
error = e;
expect(error?.status).toEqual("BAD_REQUEST");
- expect(error?.body?.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
+ expect(error?.body?.message).toEqual(
+ ERROR_CODES.INVALID_NAME_LENGTH.message,
+ );
}
});
expect(error).not.toBeNull();
@@ -1165,10 +1190,12 @@ describe("api-key", async () => {
headers,
})
.catch((e) => {
- if (e instanceof APIError) {
+ if (isAPIError(e)) {
error = e;
expect(error?.status).toEqual("BAD_REQUEST");
- expect(error?.body?.message).toEqual(ERROR_CODES.NO_VALUES_TO_UPDATE);
+ expect(error?.body?.message).toEqual(
+ ERROR_CODES.NO_VALUES_TO_UPDATE.message,
+ );
}
});
expect(error).not.toBeNull();
@@ -1232,7 +1259,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.KEY_DISABLED_EXPIRATION,
+ ERROR_CODES.KEY_DISABLED_EXPIRATION.message,
);
});
@@ -1279,7 +1306,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
+ ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL.message,
);
});
@@ -1326,7 +1353,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
+ ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE.message,
);
});
@@ -1365,7 +1392,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
+ ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED.message,
);
});
@@ -1390,7 +1417,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
+ ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED.message,
);
});
@@ -1446,7 +1473,7 @@ describe("api-key", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("BAD_REQUEST");
expect(result.error?.body.message).toEqual(
- ERROR_CODES.INVALID_METADATA_TYPE,
+ ERROR_CODES.INVALID_METADATA_TYPE.message,
);
});
@@ -1850,7 +1877,9 @@ describe("api-key", async () => {
expect(result.data).toBeNull();
expect(result.error).toBeDefined();
expect(result.error?.status).toEqual("NOT_FOUND");
- expect(result.error?.body.message).toEqual(ERROR_CODES.KEY_NOT_FOUND);
+ expect(result.error?.body.message).toEqual(
+ ERROR_CODES.KEY_NOT_FOUND.message,
+ );
});
it("should create an API key with permissions", async () => {
diff --git a/packages/better-auth/src/plugins/api-key/client.ts b/packages/better-auth/src/plugins/api-key/client.ts
index f9fdc43f2b..20f39c89c8 100644
--- a/packages/better-auth/src/plugins/api-key/client.ts
+++ b/packages/better-auth/src/plugins/api-key/client.ts
@@ -1,5 +1,8 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { apiKey } from ".";
+import { API_KEY_ERROR_CODES } from "./error-codes";
+
+export * from "./error-codes";
export const apiKeyClient = () => {
return {
@@ -10,6 +13,7 @@ export const apiKeyClient = () => {
"/api-key/delete": "POST",
"/api-key/delete-all-expired-api-keys": "POST",
},
+ $ERROR_CODES: API_KEY_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/api-key/error-codes.ts b/packages/better-auth/src/plugins/api-key/error-codes.ts
new file mode 100644
index 0000000000..9c5a37acda
--- /dev/null
+++ b/packages/better-auth/src/plugins/api-key/error-codes.ts
@@ -0,0 +1,35 @@
+import { defineErrorCodes } from "@better-auth/core/utils";
+
+export const API_KEY_ERROR_CODES = defineErrorCodes({
+ INVALID_METADATA_TYPE: "metadata must be an object or undefined",
+ REFILL_AMOUNT_AND_INTERVAL_REQUIRED:
+ "refillAmount is required when refillInterval is provided",
+ REFILL_INTERVAL_AND_AMOUNT_REQUIRED:
+ "refillInterval is required when refillAmount is provided",
+ USER_BANNED: "User is banned",
+ UNAUTHORIZED_SESSION: "Unauthorized or invalid session",
+ KEY_NOT_FOUND: "API Key not found",
+ KEY_DISABLED: "API Key is disabled",
+ KEY_EXPIRED: "API Key has expired",
+ USAGE_EXCEEDED: "API Key has reached its usage limit",
+ KEY_NOT_RECOVERABLE: "API Key is not recoverable",
+ EXPIRES_IN_IS_TOO_SMALL:
+ "The expiresIn is smaller than the predefined minimum value.",
+ EXPIRES_IN_IS_TOO_LARGE:
+ "The expiresIn is larger than the predefined maximum value.",
+ INVALID_REMAINING: "The remaining count is either too large or too small.",
+ INVALID_PREFIX_LENGTH: "The prefix length is either too large or too small.",
+ INVALID_NAME_LENGTH: "The name length is either too large or too small.",
+ METADATA_DISABLED: "Metadata is disabled.",
+ RATE_LIMIT_EXCEEDED: "Rate limit exceeded.",
+ NO_VALUES_TO_UPDATE: "No values to update.",
+ KEY_DISABLED_EXPIRATION: "Custom key expiration values are disabled.",
+ INVALID_API_KEY: "Invalid API key.",
+ INVALID_USER_ID_FROM_API_KEY: "The user id from the API key is invalid.",
+ INVALID_API_KEY_GETTER_RETURN_TYPE:
+ "API Key getter returned an invalid key type. Expected string.",
+ SERVER_ONLY_PROPERTY:
+ "The property you're trying to set can only be set from the server auth instance only.",
+ FAILED_TO_UPDATE_API_KEY: "Failed to update API key",
+ NAME_REQUIRED: "API Key name is required.",
+});
diff --git a/packages/better-auth/src/plugins/api-key/index.ts b/packages/better-auth/src/plugins/api-key/index.ts
index d3a620ccc7..32470e8b0f 100644
--- a/packages/better-auth/src/plugins/api-key/index.ts
+++ b/packages/better-auth/src/plugins/api-key/index.ts
@@ -1,6 +1,5 @@
import type { BetterAuthPlugin } from "@better-auth/core";
import { createAuthMiddleware } from "@better-auth/core/api";
-import { defineErrorCodes } from "@better-auth/core/utils";
import { base64Url } from "@better-auth/utils/base64";
import { createHash } from "@better-auth/utils/hash";
import { APIError } from "../../api";
@@ -23,39 +22,9 @@ export const defaultKeyHasher = async (key: string) => {
return hashed;
};
-export const ERROR_CODES = defineErrorCodes({
- INVALID_METADATA_TYPE: "metadata must be an object or undefined",
- REFILL_AMOUNT_AND_INTERVAL_REQUIRED:
- "refillAmount is required when refillInterval is provided",
- REFILL_INTERVAL_AND_AMOUNT_REQUIRED:
- "refillInterval is required when refillAmount is provided",
- USER_BANNED: "User is banned",
- UNAUTHORIZED_SESSION: "Unauthorized or invalid session",
- KEY_NOT_FOUND: "API Key not found",
- KEY_DISABLED: "API Key is disabled",
- KEY_EXPIRED: "API Key has expired",
- USAGE_EXCEEDED: "API Key has reached its usage limit",
- KEY_NOT_RECOVERABLE: "API Key is not recoverable",
- EXPIRES_IN_IS_TOO_SMALL:
- "The expiresIn is smaller than the predefined minimum value.",
- EXPIRES_IN_IS_TOO_LARGE:
- "The expiresIn is larger than the predefined maximum value.",
- INVALID_REMAINING: "The remaining count is either too large or too small.",
- INVALID_PREFIX_LENGTH: "The prefix length is either too large or too small.",
- INVALID_NAME_LENGTH: "The name length is either too large or too small.",
- METADATA_DISABLED: "Metadata is disabled.",
- RATE_LIMIT_EXCEEDED: "Rate limit exceeded.",
- NO_VALUES_TO_UPDATE: "No values to update.",
- KEY_DISABLED_EXPIRATION: "Custom key expiration values are disabled.",
- INVALID_API_KEY: "Invalid API key.",
- INVALID_USER_ID_FROM_API_KEY: "The user id from the API key is invalid.",
- INVALID_API_KEY_GETTER_RETURN_TYPE:
- "API Key getter returned an invalid key type. Expected string.",
- SERVER_ONLY_PROPERTY:
- "The property you're trying to set can only be set from the server auth instance only.",
- FAILED_TO_UPDATE_API_KEY: "Failed to update API key",
- NAME_REQUIRED: "API Key name is required.",
-});
+import { API_KEY_ERROR_CODES } from "./error-codes";
+
+export { API_KEY_ERROR_CODES } from "./error-codes";
export const API_KEY_TABLE_NAME = "apikey";
@@ -132,7 +101,7 @@ export const apiKey = (options?: ApiKeyOptions | undefined) => {
return {
id: "api-key",
- $ERROR_CODES: ERROR_CODES,
+ $ERROR_CODES: API_KEY_ERROR_CODES,
hooks: {
before: [
{
@@ -141,26 +110,29 @@ export const apiKey = (options?: ApiKeyOptions | undefined) => {
const key = getter(ctx)!;
if (typeof key !== "string") {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_API_KEY_GETTER_RETURN_TYPE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ API_KEY_ERROR_CODES.INVALID_API_KEY_GETTER_RETURN_TYPE,
+ );
}
if (key.length < opts.defaultKeyLength) {
// if the key is shorter than the default key length, than we know the key is invalid.
// we can't check if the key is exactly equal to the default key length, because
// a prefix may be added to the key.
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.INVALID_API_KEY,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ API_KEY_ERROR_CODES.INVALID_API_KEY,
+ );
}
if (opts.customAPIKeyValidator) {
const isValid = await opts.customAPIKeyValidator({ ctx, key });
if (!isValid) {
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.INVALID_API_KEY,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ API_KEY_ERROR_CODES.INVALID_API_KEY,
+ );
}
}
@@ -191,9 +163,10 @@ export const apiKey = (options?: ApiKeyOptions | undefined) => {
apiKey.userId,
);
if (!user) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_USER_ID_FROM_API_KEY,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ API_KEY_ERROR_CODES.INVALID_USER_ID_FROM_API_KEY,
+ );
}
const session = {
diff --git a/packages/better-auth/src/plugins/api-key/rate-limit.ts b/packages/better-auth/src/plugins/api-key/rate-limit.ts
index 7ae88b39dc..cfb544b491 100644
--- a/packages/better-auth/src/plugins/api-key/rate-limit.ts
+++ b/packages/better-auth/src/plugins/api-key/rate-limit.ts
@@ -1,4 +1,4 @@
-import { ERROR_CODES } from ".";
+import { API_KEY_ERROR_CODES as ERROR_CODES } from ".";
import type { PredefinedApiKeyOptions } from "./routes";
import type { ApiKey } from "./types";
@@ -80,7 +80,7 @@ export function isRateLimited(
// Rate limit exceeded.
return {
success: false,
- message: ERROR_CODES.RATE_LIMIT_EXCEEDED,
+ message: ERROR_CODES.RATE_LIMIT_EXCEEDED.message,
update: null,
tryAgainIn: Math.ceil(rateLimitTimeWindow - timeSinceLastRequest),
};
diff --git a/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts b/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts
index d600d0a768..c1a9e99c56 100644
--- a/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts
+++ b/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts
@@ -1,11 +1,12 @@
import type { AuthContext, Awaitable } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import * as z from "zod";
-import { APIError, getSessionFromCtx } from "../../../api";
+import { getSessionFromCtx } from "../../../api";
import { generateId } from "../../../utils";
import { getDate } from "../../../utils/date";
-import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
+import { API_KEY_TABLE_NAME, API_KEY_ERROR_CODES as ERROR_CODES } from "..";
import { defaultKeyHasher } from "../";
import { setApiKey } from "../adapter";
import type { apiKeySchema } from "../schema";
@@ -277,15 +278,11 @@ export function createApiKey({
: session?.user || { id: ctx.body.userId };
if (!user?.id) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.UNAUTHORIZED_SESSION,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.UNAUTHORIZED_SESSION);
}
if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.UNAUTHORIZED_SESSION,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.UNAUTHORIZED_SESSION);
}
if (authRequired) {
@@ -300,86 +297,75 @@ export function createApiKey({
permissions !== undefined ||
remaining !== null
) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.SERVER_ONLY_PROPERTY,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.SERVER_ONLY_PROPERTY);
}
}
// if metadata is defined, than check that it's an object.
if (metadata) {
if (opts.enableMetadata === false) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.METADATA_DISABLED,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.METADATA_DISABLED);
}
if (typeof metadata !== "object") {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_METADATA_TYPE,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_METADATA_TYPE);
}
}
// make sure that if they pass a refill amount, they also pass a refill interval
if (refillAmount && !refillInterval) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
+ );
}
// make sure that if they pass a refill interval, they also pass a refill amount
if (refillInterval && !refillAmount) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
+ );
}
if (expiresIn) {
if (opts.keyExpiration.disableCustomExpiresTime === true) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.KEY_DISABLED_EXPIRATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.KEY_DISABLED_EXPIRATION,
+ );
}
const expiresIn_in_days = expiresIn / (60 * 60 * 24);
if (opts.keyExpiration.minExpiresIn > expiresIn_in_days) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
+ );
} else if (opts.keyExpiration.maxExpiresIn < expiresIn_in_days) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
+ );
}
}
if (prefix) {
if (prefix.length < opts.minimumPrefixLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_PREFIX_LENGTH,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_PREFIX_LENGTH);
}
if (prefix.length > opts.maximumPrefixLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_PREFIX_LENGTH,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_PREFIX_LENGTH);
}
}
if (name) {
if (name.length < opts.minimumNameLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_NAME_LENGTH,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_NAME_LENGTH);
}
if (name.length > opts.maximumNameLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_NAME_LENGTH,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_NAME_LENGTH);
}
} else if (opts.requireName) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.NAME_REQUIRED,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.NAME_REQUIRED);
}
deleteAllExpiredApiKeys(ctx.context);
diff --git a/packages/better-auth/src/plugins/api-key/routes/delete-api-key.ts b/packages/better-auth/src/plugins/api-key/routes/delete-api-key.ts
index 2afe741072..d5531d2607 100644
--- a/packages/better-auth/src/plugins/api-key/routes/delete-api-key.ts
+++ b/packages/better-auth/src/plugins/api-key/routes/delete-api-key.ts
@@ -1,8 +1,9 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import * as z from "zod";
-import { APIError, sessionMiddleware } from "../../../api";
-import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
+import { sessionMiddleware } from "../../../api";
+import { API_KEY_TABLE_NAME, API_KEY_ERROR_CODES as ERROR_CODES } from "..";
import {
deleteApiKey as deleteApiKeyFromStorage,
getApiKeyById,
@@ -81,9 +82,7 @@ export function deleteApiKey({
const { keyId } = ctx.body;
const session = ctx.context.session;
if (session.user.banned === true) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.USER_BANNED,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.USER_BANNED);
}
let apiKey: ApiKey | null = null;
@@ -91,9 +90,7 @@ export function deleteApiKey({
apiKey = await getApiKeyById(ctx, keyId, opts);
if (!apiKey || apiKey.userId !== session.user.id) {
- throw new APIError("NOT_FOUND", {
- message: ERROR_CODES.KEY_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", ERROR_CODES.KEY_NOT_FOUND);
}
try {
@@ -122,7 +119,7 @@ export function deleteApiKey({
await deleteApiKeyFromStorage(ctx, apiKey, opts);
}
} catch (error: any) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.fromStatus("INTERNAL_SERVER_ERROR", {
message: error?.message,
});
}
diff --git a/packages/better-auth/src/plugins/api-key/routes/get-api-key.ts b/packages/better-auth/src/plugins/api-key/routes/get-api-key.ts
index 38b7c2bfa4..4c7726ce0e 100644
--- a/packages/better-auth/src/plugins/api-key/routes/get-api-key.ts
+++ b/packages/better-auth/src/plugins/api-key/routes/get-api-key.ts
@@ -1,9 +1,10 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import * as z from "zod";
-import { APIError, sessionMiddleware } from "../../../api";
-import { ERROR_CODES } from "..";
+import { sessionMiddleware } from "../../../api";
+import { API_KEY_ERROR_CODES as ERROR_CODES } from "..";
import { getApiKeyById } from "../adapter";
import type { apiKeySchema } from "../schema";
import type { ApiKey } from "../types";
@@ -185,9 +186,7 @@ export function getApiKey({
}
if (!apiKey) {
- throw new APIError("NOT_FOUND", {
- message: ERROR_CODES.KEY_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", ERROR_CODES.KEY_NOT_FOUND);
}
deleteAllExpiredApiKeys(ctx.context);
diff --git a/packages/better-auth/src/plugins/api-key/routes/update-api-key.ts b/packages/better-auth/src/plugins/api-key/routes/update-api-key.ts
index 47b46fbcd8..a616aca7ee 100644
--- a/packages/better-auth/src/plugins/api-key/routes/update-api-key.ts
+++ b/packages/better-auth/src/plugins/api-key/routes/update-api-key.ts
@@ -1,10 +1,11 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import * as z from "zod";
-import { APIError, getSessionFromCtx } from "../../../api";
+import { getSessionFromCtx } from "../../../api";
import { getDate } from "../../../utils/date";
-import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
+import { API_KEY_TABLE_NAME, API_KEY_ERROR_CODES as ERROR_CODES } from "..";
import { getApiKeyById, setApiKey } from "../adapter";
import type { apiKeySchema } from "../schema";
import type { ApiKey } from "../types";
@@ -268,15 +269,11 @@ export function updateApiKey({
: session?.user || { id: ctx.body.userId };
if (!user?.id) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.UNAUTHORIZED_SESSION,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.UNAUTHORIZED_SESSION);
}
if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.UNAUTHORIZED_SESSION,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.UNAUTHORIZED_SESSION);
}
if (authRequired) {
@@ -291,9 +288,7 @@ export function updateApiKey({
remaining !== undefined ||
permissions !== undefined
) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.SERVER_ONLY_PROPERTY,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.SERVER_ONLY_PROPERTY);
}
}
@@ -307,22 +302,16 @@ export function updateApiKey({
}
if (!apiKey) {
- throw new APIError("NOT_FOUND", {
- message: ERROR_CODES.KEY_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", ERROR_CODES.KEY_NOT_FOUND);
}
let newValues: Partial = {};
if (name !== undefined) {
if (name.length < opts.minimumNameLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_NAME_LENGTH,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_NAME_LENGTH);
} else if (name.length > opts.maximumNameLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_NAME_LENGTH,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_NAME_LENGTH);
}
newValues.name = name;
}
@@ -332,9 +321,10 @@ export function updateApiKey({
}
if (expiresIn !== undefined) {
if (opts.keyExpiration.disableCustomExpiresTime === true) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.KEY_DISABLED_EXPIRATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.KEY_DISABLED_EXPIRATION,
+ );
}
if (expiresIn !== null) {
// if expires is not null, check if it's under the valid range
@@ -342,13 +332,15 @@ export function updateApiKey({
const expiresIn_in_days = expiresIn / (60 * 60 * 24);
if (expiresIn_in_days < opts.keyExpiration.minExpiresIn) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
+ );
} else if (expiresIn_in_days > opts.keyExpiration.maxExpiresIn) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
+ );
}
}
newValues.expiresAt = expiresIn ? getDate(expiresIn, "sec") : null;
@@ -356,9 +348,7 @@ export function updateApiKey({
if (metadata !== undefined && opts.enableMetadata === true) {
if (typeof metadata !== "object") {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_METADATA_TYPE,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_METADATA_TYPE);
}
//@ts-expect-error - we need this to be a string to save into DB.
newValues.metadata =
@@ -369,13 +359,15 @@ export function updateApiKey({
}
if (refillAmount !== undefined || refillInterval !== undefined) {
if (refillAmount !== undefined && refillInterval === undefined) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
+ );
} else if (refillInterval !== undefined && refillAmount === undefined) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
+ );
}
newValues.refillAmount = refillAmount;
newValues.refillInterval = refillInterval;
@@ -397,9 +389,7 @@ export function updateApiKey({
}
if (Object.keys(newValues).length === 0) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.NO_VALUES_TO_UPDATE,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.NO_VALUES_TO_UPDATE);
}
let newApiKey: ApiKey = apiKey;
@@ -441,7 +431,7 @@ export function updateApiKey({
newApiKey = updated;
}
} catch (error: any) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.fromStatus("INTERNAL_SERVER_ERROR", {
message: error?.message,
});
}
diff --git a/packages/better-auth/src/plugins/api-key/routes/verify-api-key.ts b/packages/better-auth/src/plugins/api-key/routes/verify-api-key.ts
index f868c3a7f6..85bf619f3d 100644
--- a/packages/better-auth/src/plugins/api-key/routes/verify-api-key.ts
+++ b/packages/better-auth/src/plugins/api-key/routes/verify-api-key.ts
@@ -1,10 +1,11 @@
import type { AuthContext, GenericEndpointContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import * as z from "zod";
-import { APIError } from "../../../api";
+import { isAPIError } from "../../../utils/is-api-error";
import { role } from "../../access";
-import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
+import { API_KEY_TABLE_NAME, API_KEY_ERROR_CODES as ERROR_CODES } from "..";
import { defaultKeyHasher } from "../";
import { deleteApiKey, getApiKey, setApiKey } from "../adapter";
import { isRateLimited } from "../rate-limit";
@@ -28,16 +29,11 @@ export async function validateApiKey({
const apiKey = await getApiKey(ctx, hashedKey, opts);
if (!apiKey) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_API_KEY,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.INVALID_API_KEY);
}
if (apiKey.enabled === false) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.KEY_DISABLED,
- code: "KEY_DISABLED" as const,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.KEY_DISABLED);
}
if (apiKey.expiresAt) {
@@ -71,10 +67,7 @@ export async function validateApiKey({
await deleteExpiredKey();
}
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.KEY_EXPIRED,
- code: "KEY_EXPIRED" as const,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.KEY_EXPIRED);
}
}
@@ -86,18 +79,12 @@ export async function validateApiKey({
: null;
if (!apiKeyPermissions) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.KEY_NOT_FOUND,
- code: "KEY_NOT_FOUND" as const,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.KEY_NOT_FOUND);
}
const r = role(apiKeyPermissions as any);
const result = r.authorize(permissions);
if (!result.success) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.KEY_NOT_FOUND,
- code: "KEY_NOT_FOUND" as const,
- });
+ throw APIError.from("UNAUTHORIZED", ERROR_CODES.KEY_NOT_FOUND);
}
}
@@ -132,10 +119,7 @@ export async function validateApiKey({
await deleteExhaustedKey();
}
- throw new APIError("TOO_MANY_REQUESTS", {
- message: ERROR_CODES.USAGE_EXCEEDED,
- code: "USAGE_EXCEEDED" as const,
- });
+ throw APIError.from("TOO_MANY_REQUESTS", ERROR_CODES.USAGE_EXCEEDED);
} else if (remaining !== null) {
let now = Date.now();
const refillInterval = apiKey.refillInterval;
@@ -154,10 +138,7 @@ export async function validateApiKey({
if (remaining === 0) {
// if there are no more remaining requests, than the key is invalid
- throw new APIError("TOO_MANY_REQUESTS", {
- message: ERROR_CODES.USAGE_EXCEEDED,
- code: "USAGE_EXCEEDED" as const,
- });
+ throw APIError.from("TOO_MANY_REQUESTS", ERROR_CODES.USAGE_EXCEEDED);
} else {
remaining--;
}
@@ -223,10 +204,10 @@ export async function validateApiKey({
} else {
newApiKey = await performUpdate();
if (!newApiKey) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: ERROR_CODES.FAILED_TO_UPDATE_API_KEY,
- code: "INTERNAL_SERVER_ERROR" as const,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ ERROR_CODES.FAILED_TO_UPDATE_API_KEY,
+ );
}
}
@@ -317,7 +298,7 @@ export function verifyApiKey({
);
}
} catch (error) {
- if (error instanceof APIError) {
+ if (isAPIError(error)) {
return ctx.json({
valid: false,
error: {
diff --git a/packages/better-auth/src/plugins/captcha/index.ts b/packages/better-auth/src/plugins/captcha/index.ts
index acfffd8c25..fc36bbc40b 100644
--- a/packages/better-auth/src/plugins/captcha/index.ts
+++ b/packages/better-auth/src/plugins/captcha/index.ts
@@ -19,7 +19,7 @@ export const captcha = (options: CaptchaOptions) =>
return undefined;
if (!options.secretKey) {
- throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY);
+ throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
}
const captchaResponse = request.headers.get("x-captcha-response");
@@ -27,7 +27,7 @@ export const captcha = (options: CaptchaOptions) =>
if (!captchaResponse) {
return middlewareResponse({
- message: EXTERNAL_ERROR_CODES.MISSING_RESPONSE,
+ message: EXTERNAL_ERROR_CODES.MISSING_RESPONSE.message,
status: 400,
});
}
@@ -76,7 +76,7 @@ export const captcha = (options: CaptchaOptions) =>
});
return middlewareResponse({
- message: EXTERNAL_ERROR_CODES.UNKNOWN_ERROR,
+ message: EXTERNAL_ERROR_CODES.UNKNOWN_ERROR.message,
status: 500,
});
}
diff --git a/packages/better-auth/src/plugins/captcha/verify-handlers/captchafox.ts b/packages/better-auth/src/plugins/captcha/verify-handlers/captchafox.ts
index c02d502e7f..fe4d24a646 100644
--- a/packages/better-auth/src/plugins/captcha/verify-handlers/captchafox.ts
+++ b/packages/better-auth/src/plugins/captcha/verify-handlers/captchafox.ts
@@ -49,12 +49,12 @@ export const captchaFox = async ({
});
if (!response.data || response.error) {
- throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE);
+ throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
}
if (!response.data.success) {
return middlewareResponse({
- message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED,
+ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
status: 403,
});
}
diff --git a/packages/better-auth/src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts b/packages/better-auth/src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
index 7b9054290e..aba45a0ee7 100644
--- a/packages/better-auth/src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
+++ b/packages/better-auth/src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
@@ -41,12 +41,12 @@ export const cloudflareTurnstile = async ({
});
if (!response.data || response.error) {
- throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE);
+ throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
}
if (!response.data.success) {
return middlewareResponse({
- message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED,
+ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
status: 403,
});
}
diff --git a/packages/better-auth/src/plugins/captcha/verify-handlers/google-recaptcha.ts b/packages/better-auth/src/plugins/captcha/verify-handlers/google-recaptcha.ts
index 5ecb52ad94..beb4f54676 100644
--- a/packages/better-auth/src/plugins/captcha/verify-handlers/google-recaptcha.ts
+++ b/packages/better-auth/src/plugins/captcha/verify-handlers/google-recaptcha.ts
@@ -58,7 +58,7 @@ export const googleRecaptcha = async ({
);
if (!response.data || response.error) {
- throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE);
+ throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
}
if (
@@ -66,7 +66,7 @@ export const googleRecaptcha = async ({
(isV3(response.data) && response.data.score < minScore)
) {
return middlewareResponse({
- message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED,
+ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
status: 403,
});
}
diff --git a/packages/better-auth/src/plugins/captcha/verify-handlers/h-captcha.ts b/packages/better-auth/src/plugins/captcha/verify-handlers/h-captcha.ts
index 772e9bd1c6..0f4adaf497 100644
--- a/packages/better-auth/src/plugins/captcha/verify-handlers/h-captcha.ts
+++ b/packages/better-auth/src/plugins/captcha/verify-handlers/h-captcha.ts
@@ -54,12 +54,12 @@ export const hCaptcha = async ({
});
if (!response.data || response.error) {
- throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE);
+ throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
}
if (!response.data.success) {
return middlewareResponse({
- message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED,
+ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
status: 403,
});
}
diff --git a/packages/better-auth/src/plugins/device-authorization/routes.ts b/packages/better-auth/src/plugins/device-authorization/routes.ts
index 6ebf1868a8..c45451bda1 100644
--- a/packages/better-auth/src/plugins/device-authorization/routes.ts
+++ b/packages/better-auth/src/plugins/device-authorization/routes.ts
@@ -1,5 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx } from "../../api/routes/session";
import { generateRandomString } from "../../crypto";
@@ -309,7 +309,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
throw new APIError("BAD_REQUEST", {
error: "invalid_grant",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE,
+ DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE.message,
});
}
@@ -333,7 +333,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
throw new APIError("BAD_REQUEST", {
error: "slow_down",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.POLLING_TOO_FREQUENTLY,
+ DEVICE_AUTHORIZATION_ERROR_CODES.POLLING_TOO_FREQUENTLY.message,
});
}
}
@@ -365,7 +365,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
throw new APIError("BAD_REQUEST", {
error: "expired_token",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_DEVICE_CODE,
+ DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_DEVICE_CODE.message,
});
}
@@ -373,7 +373,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
throw new APIError("BAD_REQUEST", {
error: "authorization_pending",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.AUTHORIZATION_PENDING,
+ DEVICE_AUTHORIZATION_ERROR_CODES.AUTHORIZATION_PENDING.message,
});
}
@@ -389,7 +389,8 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
});
throw new APIError("BAD_REQUEST", {
error: "access_denied",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.ACCESS_DENIED,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.ACCESS_DENIED.message,
});
}
@@ -401,7 +402,8 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
if (!user) {
throw new APIError("INTERNAL_SERVER_ERROR", {
error: "server_error",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.USER_NOT_FOUND,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.USER_NOT_FOUND.message,
});
}
@@ -413,7 +415,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
throw new APIError("INTERNAL_SERVER_ERROR", {
error: "server_error",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ DEVICE_AUTHORIZATION_ERROR_CODES.FAILED_TO_CREATE_SESSION.message,
});
}
@@ -472,7 +474,7 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
throw new APIError("INTERNAL_SERVER_ERROR", {
error: "server_error",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE_STATUS,
+ DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE_STATUS.message,
});
},
);
@@ -540,14 +542,16 @@ export const deviceVerify = createAuthEndpoint(
if (!deviceCodeRecord) {
throw new APIError("BAD_REQUEST", {
error: "invalid_request",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message,
});
}
if (deviceCodeRecord.expiresAt < new Date()) {
throw new APIError("BAD_REQUEST", {
error: "expired_token",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message,
});
}
@@ -611,7 +615,7 @@ export const deviceApprove = createAuthEndpoint(
throw new APIError("UNAUTHORIZED", {
error: "unauthorized",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED,
+ DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED.message,
});
}
@@ -631,14 +635,16 @@ export const deviceApprove = createAuthEndpoint(
if (!deviceCodeRecord) {
throw new APIError("BAD_REQUEST", {
error: "invalid_request",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message,
});
}
if (deviceCodeRecord.expiresAt < new Date()) {
throw new APIError("BAD_REQUEST", {
error: "expired_token",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message,
});
}
@@ -646,7 +652,8 @@ export const deviceApprove = createAuthEndpoint(
throw new APIError("BAD_REQUEST", {
error: "invalid_request",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED,
+ DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED
+ .message,
});
}
@@ -728,14 +735,16 @@ export const deviceDeny = createAuthEndpoint(
if (!deviceCodeRecord) {
throw new APIError("BAD_REQUEST", {
error: "invalid_request",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message,
});
}
if (deviceCodeRecord.expiresAt < new Date()) {
throw new APIError("BAD_REQUEST", {
error: "expired_token",
- error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE,
+ error_description:
+ DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message,
});
}
@@ -743,7 +752,8 @@ export const deviceDeny = createAuthEndpoint(
throw new APIError("BAD_REQUEST", {
error: "invalid_request",
error_description:
- DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED,
+ DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED
+ .message,
});
}
diff --git a/packages/better-auth/src/plugins/email-otp/client.ts b/packages/better-auth/src/plugins/email-otp/client.ts
index ef40c33eb8..3814fc8f23 100644
--- a/packages/better-auth/src/plugins/email-otp/client.ts
+++ b/packages/better-auth/src/plugins/email-otp/client.ts
@@ -1,6 +1,10 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { emailOTP } from ".";
+import { EMAIL_OTP_ERROR_CODES } from "./error-codes";
+
+export * from "./error-codes";
+
export const emailOTPClient = () => {
return {
id: "email-otp",
@@ -12,5 +16,6 @@ export const emailOTPClient = () => {
signal: "$sessionSignal",
},
],
+ $ERROR_CODES: EMAIL_OTP_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/email-otp/error-codes.ts b/packages/better-auth/src/plugins/email-otp/error-codes.ts
new file mode 100644
index 0000000000..f4a538133e
--- /dev/null
+++ b/packages/better-auth/src/plugins/email-otp/error-codes.ts
@@ -0,0 +1,7 @@
+import { defineErrorCodes } from "@better-auth/core/utils";
+
+export const EMAIL_OTP_ERROR_CODES = defineErrorCodes({
+ OTP_EXPIRED: "OTP expired",
+ INVALID_OTP: "Invalid OTP",
+ TOO_MANY_ATTEMPTS: "Too many attempts",
+});
diff --git a/packages/better-auth/src/plugins/email-otp/index.ts b/packages/better-auth/src/plugins/email-otp/index.ts
index 2fb725c19b..95ef0abd00 100644
--- a/packages/better-auth/src/plugins/email-otp/index.ts
+++ b/packages/better-auth/src/plugins/email-otp/index.ts
@@ -3,11 +3,11 @@ import { createAuthMiddleware } from "@better-auth/core/api";
import { generateRandomString } from "../../crypto";
import { getDate } from "../../utils/date";
import { getEndpointResponse } from "../../utils/plugin-helper";
+import { EMAIL_OTP_ERROR_CODES } from "./error-codes";
import { storeOTP } from "./otp-token";
import {
checkVerificationOTP,
createVerificationOTP,
- ERROR_CODES,
forgetPasswordEmailOTP,
getVerificationOTP,
resetPasswordEmailOTP,
@@ -109,7 +109,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
},
],
},
- $ERROR_CODES: ERROR_CODES,
+
rateLimit: [
{
pathMatcher(path) {
@@ -141,5 +141,6 @@ export const emailOTP = (options: EmailOTPOptions) => {
},
],
options,
+ $ERROR_CODES: EMAIL_OTP_ERROR_CODES,
} satisfies BetterAuthPlugin;
};
diff --git a/packages/better-auth/src/plugins/email-otp/routes.ts b/packages/better-auth/src/plugins/email-otp/routes.ts
index ba64e53662..3c446c0d72 100644
--- a/packages/better-auth/src/plugins/email-otp/routes.ts
+++ b/packages/better-auth/src/plugins/email-otp/routes.ts
@@ -1,6 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { defineErrorCodes } from "@better-auth/core/utils";
import * as z from "zod";
import { APIError, getSessionFromCtx } from "../../api";
import { setCookieCache, setSessionCookie } from "../../cookies";
@@ -19,11 +18,7 @@ type RequiredEmailOTPOptions = WithRequired<
"expiresIn" | "generateOTP" | "storeOTP"
>;
-export const ERROR_CODES = defineErrorCodes({
- OTP_EXPIRED: "OTP expired",
- INVALID_OTP: "Invalid OTP",
- TOO_MANY_ATTEMPTS: "Too many attempts",
-});
+import { EMAIL_OTP_ERROR_CODES as ERROR_CODES } from "./error-codes";
const sendVerificationOTPBodySchema = z.object({
email: z.string({}).meta({
@@ -82,16 +77,14 @@ export const sendVerificationOTP = (opts: RequiredEmailOTPOptions) =>
async (ctx) => {
if (!opts?.sendVerificationOTP) {
ctx.context.logger.error("send email verification is not implemented");
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "send email verification is not implemented",
});
}
const email = ctx.body.email.toLowerCase();
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw ctx.error("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
let otp =
opts.generateOTP({ email, type: ctx.body.type }, ctx) ||
@@ -266,7 +259,7 @@ export const getVerificationOTP = (opts: RequiredEmailOTPOptions) =>
opts.storeOTP === "hashed" ||
(typeof opts.storeOTP === "object" && "hash" in opts.storeOTP)
) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "OTP is hashed, cannot return the plain text OTP",
});
}
@@ -350,32 +343,24 @@ export const checkVerificationOTP = (opts: RequiredEmailOTPOptions) =>
const email = ctx.body.email.toLowerCase();
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const verificationValue =
await ctx.context.internalAdapter.findVerificationValue(
`${ctx.body.type}-otp-${email}`,
);
if (!verificationValue) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
if (verificationValue.expiresAt < new Date()) {
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
);
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.OTP_EXPIRED,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
}
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
@@ -384,9 +369,7 @@ export const checkVerificationOTP = (opts: RequiredEmailOTPOptions) =>
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
);
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.TOO_MANY_ATTEMPTS,
- });
+ throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
}
const verified = await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp);
if (!verified) {
@@ -396,9 +379,7 @@ export const checkVerificationOTP = (opts: RequiredEmailOTPOptions) =>
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
return ctx.json({
success: true,
@@ -477,9 +458,7 @@ export const verifyEmailOTP = (opts: RequiredEmailOTPOptions) =>
const email = ctx.body.email.toLowerCase();
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
const verificationValue =
await ctx.context.internalAdapter.findVerificationValue(
@@ -487,14 +466,10 @@ export const verifyEmailOTP = (opts: RequiredEmailOTPOptions) =>
);
if (!verificationValue) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
if (verificationValue.expiresAt < new Date()) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.OTP_EXPIRED,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
}
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
@@ -503,9 +478,7 @@ export const verifyEmailOTP = (opts: RequiredEmailOTPOptions) =>
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
);
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.TOO_MANY_ATTEMPTS,
- });
+ throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
}
const verified = await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp);
if (!verified) {
@@ -515,9 +488,7 @@ export const verifyEmailOTP = (opts: RequiredEmailOTPOptions) =>
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
@@ -528,9 +499,7 @@ export const verifyEmailOTP = (opts: RequiredEmailOTPOptions) =>
* safe to leak the existence of a user, given the user has already the OTP from the
* email
*/
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const updatedUser = await ctx.context.internalAdapter.updateUser(
user.user.id,
@@ -668,14 +637,10 @@ export const signInEmailOTP = (opts: RequiredEmailOTPOptions) =>
`sign-in-otp-${email}`,
);
if (!verificationValue) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
if (verificationValue.expiresAt < new Date()) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.OTP_EXPIRED,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
}
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
const allowedAttempts = opts?.allowedAttempts || 3;
@@ -683,9 +648,7 @@ export const signInEmailOTP = (opts: RequiredEmailOTPOptions) =>
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
);
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.TOO_MANY_ATTEMPTS,
- });
+ throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
}
const verified = await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp);
if (!verified) {
@@ -695,9 +658,7 @@ export const signInEmailOTP = (opts: RequiredEmailOTPOptions) =>
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
@@ -705,9 +666,7 @@ export const signInEmailOTP = (opts: RequiredEmailOTPOptions) =>
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
if (opts.disableSignUp) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const newUser = await ctx.context.internalAdapter.createUser({
email,
@@ -916,17 +875,13 @@ export const resetPasswordEmailOTP = (opts: RequiredEmailOTPOptions) =>
`forget-password-otp-${email}`,
);
if (!verificationValue) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
if (verificationValue.expiresAt < new Date()) {
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
);
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.OTP_EXPIRED,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
}
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
const allowedAttempts = opts?.allowedAttempts || 3;
@@ -934,9 +889,7 @@ export const resetPasswordEmailOTP = (opts: RequiredEmailOTPOptions) =>
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
);
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.TOO_MANY_ATTEMPTS,
- });
+ throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
}
const verified = await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp);
if (!verified) {
@@ -946,9 +899,7 @@ export const resetPasswordEmailOTP = (opts: RequiredEmailOTPOptions) =>
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
}
await ctx.context.internalAdapter.deleteVerificationValue(
verificationValue.id,
@@ -957,21 +908,15 @@ export const resetPasswordEmailOTP = (opts: RequiredEmailOTPOptions) =>
includeAccounts: true,
});
if (!user) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (ctx.body.password.length < minPasswordLength) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (ctx.body.password.length > maxPasswordLength) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const passwordHash = await ctx.context.password.hash(ctx.body.password);
let account = user.accounts?.find(
diff --git a/packages/better-auth/src/plugins/generic-oauth/client.ts b/packages/better-auth/src/plugins/generic-oauth/client.ts
index 90379a55e5..31ce0f66cb 100644
--- a/packages/better-auth/src/plugins/generic-oauth/client.ts
+++ b/packages/better-auth/src/plugins/generic-oauth/client.ts
@@ -1,13 +1,16 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { genericOAuth } from ".";
+import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes";
export const genericOAuthClient = () => {
return {
id: "generic-oauth-client",
$InferServerPlugin: {} as ReturnType,
+ $ERROR_CODES: GENERIC_OAUTH_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
+export * from "./error-codes";
export type {
BaseOAuthProviderOptions,
GenericOAuthConfig,
diff --git a/packages/better-auth/src/plugins/generic-oauth/index.ts b/packages/better-auth/src/plugins/generic-oauth/index.ts
index 9d94772d0c..c38e66c59d 100644
--- a/packages/better-auth/src/plugins/generic-oauth/index.ts
+++ b/packages/better-auth/src/plugins/generic-oauth/index.ts
@@ -1,4 +1,5 @@
import type { AuthContext, BetterAuthPlugin } from "@better-auth/core";
+import { APIError } from "@better-auth/core/error";
import type { OAuth2Tokens, OAuthProvider } from "@better-auth/core/oauth2";
import {
createAuthorizationURL,
@@ -6,7 +7,6 @@ import {
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { betterFetch } from "@better-fetch/fetch";
-import { APIError } from "better-call";
import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes";
import {
getUserInfo,
@@ -77,9 +77,10 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
}
}
if (!finalAuthUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
+ );
}
return createAuthorizationURL({
id: c.providerId,
@@ -122,9 +123,10 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
}
}
if (!finalTokenUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND,
+ );
}
return validateAuthorizationCode({
headers: c.authorizationHeaders,
@@ -156,9 +158,10 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
}
}
if (!finalTokenUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND,
+ );
}
return refreshAccessToken({
refreshToken,
diff --git a/packages/better-auth/src/plugins/generic-oauth/routes.ts b/packages/better-auth/src/plugins/generic-oauth/routes.ts
index e0c51e431a..51f8138f9e 100644
--- a/packages/better-auth/src/plugins/generic-oauth/routes.ts
+++ b/packages/better-auth/src/plugins/generic-oauth/routes.ts
@@ -7,10 +7,9 @@ import {
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { betterFetch } from "@better-fetch/fetch";
-import { APIError } from "better-call";
import { decodeJwt } from "jose";
import * as z from "zod";
-import { sessionMiddleware } from "../../api";
+import { APIError, sessionMiddleware } from "../../api";
import { setSessionCookie } from "../../cookies";
import { handleOAuthUserInfo } from "../../oauth2/link-account";
import { generateState, parseState } from "../../oauth2/state";
@@ -119,7 +118,7 @@ export const signInWithOAuth2 = (options: GenericOAuthOptions) =>
const { providerId } = ctx.body;
const config = options.config.find((c) => c.providerId === providerId);
if (!config) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: `${GENERIC_OAUTH_ERROR_CODES.PROVIDER_CONFIG_NOT_FOUND} ${providerId}`,
});
}
@@ -159,9 +158,10 @@ export const signInWithOAuth2 = (options: GenericOAuthOptions) =>
}
}
if (!finalAuthUrl || !finalTokenUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
+ );
}
if (authorizationUrlParams) {
const withAdditionalParams = new URL(finalAuthUrl);
@@ -283,16 +283,17 @@ export const oAuth2Callback = (options: GenericOAuthOptions) =>
}
const providerId = ctx.params?.providerId;
if (!providerId) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.PROVIDER_ID_REQUIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.PROVIDER_ID_REQUIRED,
+ );
}
const providerConfig = options.config.find(
(p) => p.providerId === providerId,
);
if (!providerConfig) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: `${GENERIC_OAUTH_ERROR_CODES.PROVIDER_CONFIG_NOT_FOUND} ${providerId}`,
});
}
@@ -348,9 +349,10 @@ export const oAuth2Callback = (options: GenericOAuthOptions) =>
} else {
// Standard token exchange with tokenUrlParams support
if (!finalTokenUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG,
+ );
}
const additionalParams =
typeof providerConfig.tokenUrlParams === "function"
@@ -379,9 +381,10 @@ export const oAuth2Callback = (options: GenericOAuthOptions) =>
throw redirectOnError("oauth_code_verification_failed");
}
if (!tokens) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG,
+ );
}
const userInfo: Omit =
await (async function handleUserInfo() {
@@ -600,17 +603,16 @@ export const oAuth2LinkAccount = (options: GenericOAuthOptions) =>
async (c: GenericEndpointContext) => {
const session = c.context.session;
if (!session) {
- throw new APIError("UNAUTHORIZED", {
- message: GENERIC_OAUTH_ERROR_CODES.SESSION_REQUIRED,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ GENERIC_OAUTH_ERROR_CODES.SESSION_REQUIRED,
+ );
}
const provider = options.config.find(
(p) => p.providerId === c.body.providerId,
);
if (!provider) {
- throw new APIError("NOT_FOUND", {
- message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
}
const {
providerId,
@@ -629,9 +631,10 @@ export const oAuth2LinkAccount = (options: GenericOAuthOptions) =>
let finalAuthUrl = authorizationUrl;
if (!finalAuthUrl) {
if (!discoveryUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
+ );
}
const discovery = await betterFetch<{
authorization_endpoint: string;
@@ -651,9 +654,10 @@ export const oAuth2LinkAccount = (options: GenericOAuthOptions) =>
}
if (!finalAuthUrl) {
- throw new APIError("BAD_REQUEST", {
- message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
+ );
}
const state = await generateState(
diff --git a/packages/better-auth/src/plugins/haveibeenpwned/index.ts b/packages/better-auth/src/plugins/haveibeenpwned/index.ts
index 3818a0c3ed..087646e4fd 100644
--- a/packages/better-auth/src/plugins/haveibeenpwned/index.ts
+++ b/packages/better-auth/src/plugins/haveibeenpwned/index.ts
@@ -4,6 +4,7 @@ import { defineErrorCodes } from "@better-auth/core/utils";
import { createHash } from "@better-auth/utils/hash";
import { betterFetch } from "@better-fetch/fetch";
import { APIError } from "../../api";
+import { isAPIError } from "../../utils/is-api-error";
const ERROR_CODES = defineErrorCodes({
PASSWORD_COMPROMISED:
@@ -43,13 +44,13 @@ async function checkPasswordCompromise(
);
if (found) {
- throw new APIError("BAD_REQUEST", {
- message: customMessage || ERROR_CODES.PASSWORD_COMPROMISED,
- code: "PASSWORD_COMPROMISED",
+ throw APIError.from("BAD_REQUEST", {
+ message: customMessage || ERROR_CODES.PASSWORD_COMPROMISED.message,
+ code: ERROR_CODES.PASSWORD_COMPROMISED.code,
});
}
} catch (error) {
- if (error instanceof APIError) throw error;
+ if (isAPIError(error)) throw error;
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to check password. Please try again later.",
});
diff --git a/packages/better-auth/src/plugins/mcp/authorize.ts b/packages/better-auth/src/plugins/mcp/authorize.ts
index 9c6a3daf3d..e876346aad 100644
--- a/packages/better-auth/src/plugins/mcp/authorize.ts
+++ b/packages/better-auth/src/plugins/mcp/authorize.ts
@@ -1,5 +1,5 @@
import type { GenericEndpointContext } from "@better-auth/core";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import { getSessionFromCtx } from "../../api";
import { generateRandomString } from "../../crypto";
import type { OAuthApplication } from "../oidc-provider/schema";
diff --git a/packages/better-auth/src/plugins/multi-session/client.ts b/packages/better-auth/src/plugins/multi-session/client.ts
index 82c411a449..2dea78df4f 100644
--- a/packages/better-auth/src/plugins/multi-session/client.ts
+++ b/packages/better-auth/src/plugins/multi-session/client.ts
@@ -1,5 +1,8 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { multiSession } from ".";
+import { MULTI_SESSION_ERROR_CODES } from "./error-codes";
+
+export * from "./error-codes";
export const multiSessionClient = () => {
return {
@@ -13,6 +16,7 @@ export const multiSessionClient = () => {
signal: "$sessionSignal",
},
],
+ $ERROR_CODES: MULTI_SESSION_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/multi-session/error-codes.ts b/packages/better-auth/src/plugins/multi-session/error-codes.ts
new file mode 100644
index 0000000000..5d5e2d3590
--- /dev/null
+++ b/packages/better-auth/src/plugins/multi-session/error-codes.ts
@@ -0,0 +1,5 @@
+import { defineErrorCodes } from "@better-auth/core/utils";
+
+export const MULTI_SESSION_ERROR_CODES = defineErrorCodes({
+ INVALID_SESSION_TOKEN: "Invalid session token",
+});
diff --git a/packages/better-auth/src/plugins/multi-session/index.ts b/packages/better-auth/src/plugins/multi-session/index.ts
index 3f91f74d28..805419a7a0 100644
--- a/packages/better-auth/src/plugins/multi-session/index.ts
+++ b/packages/better-auth/src/plugins/multi-session/index.ts
@@ -3,7 +3,6 @@ import {
createAuthEndpoint,
createAuthMiddleware,
} from "@better-auth/core/api";
-import { defineErrorCodes } from "@better-auth/core/utils";
import * as z from "zod";
import { APIError, sessionMiddleware } from "../../api";
import {
@@ -22,9 +21,9 @@ export interface MultiSessionConfig {
maximumSessions?: number | undefined;
}
-const ERROR_CODES = defineErrorCodes({
- INVALID_SESSION_TOKEN: "Invalid session token",
-});
+import { MULTI_SESSION_ERROR_CODES as ERROR_CODES } from "./error-codes";
+
+export { MULTI_SESSION_ERROR_CODES as ERROR_CODES } from "./error-codes";
const setActiveSessionBodySchema = z.object({
sessionToken: z.string().meta({
@@ -159,9 +158,10 @@ export const multiSession = (options?: MultiSessionConfig | undefined) => {
ctx.context.secret,
);
if (!sessionCookie) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_SESSION_TOKEN,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_SESSION_TOKEN,
+ );
}
const session =
await ctx.context.internalAdapter.findSession(sessionToken);
@@ -170,9 +170,10 @@ export const multiSession = (options?: MultiSessionConfig | undefined) => {
...ctx.context.authCookies.sessionToken.options,
maxAge: 0,
});
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_SESSION_TOKEN,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_SESSION_TOKEN,
+ );
}
await setSessionCookie(ctx, session);
return ctx.json(session);
@@ -233,9 +234,10 @@ export const multiSession = (options?: MultiSessionConfig | undefined) => {
ctx.context.secret,
);
if (!sessionCookie) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_SESSION_TOKEN,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_SESSION_TOKEN,
+ );
}
await ctx.context.internalAdapter.deleteSession(sessionToken);
diff --git a/packages/better-auth/src/plugins/oidc-provider/authorize.ts b/packages/better-auth/src/plugins/oidc-provider/authorize.ts
index 22a243413d..77b1f314b8 100644
--- a/packages/better-auth/src/plugins/oidc-provider/authorize.ts
+++ b/packages/better-auth/src/plugins/oidc-provider/authorize.ts
@@ -1,5 +1,5 @@
import type { GenericEndpointContext } from "@better-auth/core";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import { getSessionFromCtx } from "../../api";
import { generateRandomString } from "../../crypto";
import { getClient } from "./index";
diff --git a/packages/better-auth/src/plugins/oidc-provider/error.ts b/packages/better-auth/src/plugins/oidc-provider/error.ts
index 41bee4dac2..696a803d9d 100644
--- a/packages/better-auth/src/plugins/oidc-provider/error.ts
+++ b/packages/better-auth/src/plugins/oidc-provider/error.ts
@@ -1,4 +1,4 @@
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
class OIDCProviderError extends APIError {}
diff --git a/packages/better-auth/src/plugins/one-time-token/one-time-token.test.ts b/packages/better-auth/src/plugins/one-time-token/one-time-token.test.ts
index 700fa70b58..935f28f85e 100644
--- a/packages/better-auth/src/plugins/one-time-token/one-time-token.test.ts
+++ b/packages/better-auth/src/plugins/one-time-token/one-time-token.test.ts
@@ -1,6 +1,6 @@
-import { APIError } from "better-call";
import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
+import { isAPIError } from "../../utils/is-api-error";
import { oneTimeToken } from ".";
import { oneTimeTokenClient } from "./client";
import { defaultKeyHasher } from "./utils";
@@ -35,7 +35,7 @@ describe("One-time token", async () => {
},
})
.catch((e) => e);
- expect(shouldFail).toBeInstanceOf(APIError);
+ expect(isAPIError(shouldFail)).toBeTruthy();
});
it("should expire", async () => {
@@ -52,7 +52,7 @@ describe("One-time token", async () => {
},
})
.catch((e) => e);
- expect(shouldFail).toBeInstanceOf(APIError);
+ expect(isAPIError(shouldFail)).toBeTruthy();
vi.useRealTimers();
});
@@ -105,7 +105,7 @@ describe("One-time token", async () => {
})
.catch((e) => e);
- expect(shouldFail).toBeInstanceOf(APIError);
+ expect(isAPIError(shouldFail)).toBeTruthy();
expect(shouldFail.body.message).toBe("Session expired");
vi.useRealTimers();
diff --git a/packages/better-auth/src/plugins/organization/client.ts b/packages/better-auth/src/plugins/organization/client.ts
index 733c4771cd..baf2e56f95 100644
--- a/packages/better-auth/src/plugins/organization/client.ts
+++ b/packages/better-auth/src/plugins/organization/client.ts
@@ -14,11 +14,14 @@ import type { Prettify } from "../../types/helper";
import type { AccessControl, Role } from "../access";
import type { defaultStatements } from "./access";
import { adminAc, defaultRoles, memberAc, ownerAc } from "./access";
+import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import type { OrganizationPlugin } from "./organization";
import type { HasPermissionBaseInput } from "./permission";
import { hasPermissionFn } from "./permission";
import type { OrganizationOptions } from "./types";
+export * from "./error-codes";
+
/**
* Using the same `hasPermissionFn` function, but without the need for a `ctx` parameter or the `organizationId` parameter.
*/
@@ -275,6 +278,7 @@ export const organizationClient = (
signal: "$activeMemberRoleSignal",
},
],
+ $ERROR_CODES: ORGANIZATION_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts
index bb43548cdc..cdb88e6308 100644
--- a/packages/better-auth/src/plugins/organization/organization.test.ts
+++ b/packages/better-auth/src/plugins/organization/organization.test.ts
@@ -1,5 +1,5 @@
+import type { APIError } from "@better-auth/core/error";
import type { Prettify } from "better-call";
-import { APIError } from "better-call";
import { describe, expect, expectTypeOf, it } from "vitest";
import { memoryAdapter } from "../../adapters/memory-adapter";
import type {
@@ -12,6 +12,7 @@ import { nextCookies } from "../../integrations/next-js";
import { getTestInstance } from "../../test-utils/test-instance";
import type { User } from "../../types";
import type { PrettifyDeep } from "../../types/helper";
+import { isAPIError } from "../../utils/is-api-error";
import { createAccessControl } from "../access";
import { admin } from "../admin";
import { adminAc, defaultStatements, memberAc, ownerAc } from "./access";
@@ -144,7 +145,9 @@ describe("organization", async (it) => {
},
});
expect(existingSlug.error?.status).toBe(400);
- expect(existingSlug.error?.message).toBe("slug is taken");
+ expect(existingSlug.error?.message).toBe(
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_SLUG_ALREADY_TAKEN.message,
+ );
});
it("should prevent creating organization with empty slug", async () => {
@@ -455,7 +458,8 @@ describe("organization", async (it) => {
},
});
expect(inviteAgain.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION
+ .message,
);
const inviteAgainUpper = await client.organization.inviteMember({
@@ -467,7 +471,8 @@ describe("organization", async (it) => {
},
});
expect(inviteAgainUpper.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION
+ .message,
);
await client.signUp.email({
@@ -496,7 +501,8 @@ describe("organization", async (it) => {
},
});
expect(inviteMemberAgain.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION
+ .message,
);
const inviteMemberAgainUpper = await client.organization.inviteMember({
@@ -508,7 +514,8 @@ describe("organization", async (it) => {
},
});
expect(inviteMemberAgainUpper.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION
+ .message,
);
});
@@ -642,7 +649,8 @@ describe("organization", async (it) => {
},
});
expect(invite.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE
+ .message,
);
});
@@ -837,7 +845,7 @@ describe("organization", async (it) => {
expect(deleteResult.error?.status).toBe(400);
expect(deleteResult.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION.message,
);
});
@@ -1001,7 +1009,7 @@ describe("organization", async (it) => {
})
.catch((e: APIError) => {
expect(e).not.toBeNull();
- expect(e).toBeInstanceOf(APIError);
+ expect(isAPIError(e)).toBeTruthy();
expect(e.message).toBe(
ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
);
@@ -1043,7 +1051,7 @@ describe("organization", async (it) => {
},
});
expect(invitation.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED.message,
);
const getFullOrganization = await client.organization.getFullOrganization({
@@ -1336,7 +1344,7 @@ describe("invitation limit", async () => {
});
expect(invite.error?.status).toBe(403);
expect(invite.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
+ ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED.message,
);
});
@@ -1369,7 +1377,7 @@ describe("invitation limit", async () => {
})
.catch((e: APIError) => {
expect(e.message).toBe(
- ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
+ ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED.message,
);
});
});
@@ -2283,7 +2291,7 @@ describe("Additional Fields", async () => {
},
headers,
});
- type Result = PrettifyDeep;
+ type Result = typeof removedMember extends infer U | null ? U : never;
type ExpectedResult = {
member: {
id: string;
@@ -2296,14 +2304,14 @@ describe("Additional Fields", async () => {
id: string;
email: string;
name: string;
- image?: string;
+ image?: string | undefined;
};
memberRequiredField: string;
memberOptionalField?: string | undefined;
memberHiddenField?: string | undefined;
};
- } | null;
- expectTypeOf().toEqualTypeOf();
+ };
+ expectTypeOf().toMatchObjectType();
expect(removedMember?.member.user.email).toBe(addedMember.user.email);
expect(removedMember?.member.memberRequiredField).toBe("hey");
expect(removedMember?.member.memberOptionalField).toBe("hey2");
diff --git a/packages/better-auth/src/plugins/organization/organization.ts b/packages/better-auth/src/plugins/organization/organization.ts
index 1a7f1633b5..d208bd0b92 100644
--- a/packages/better-auth/src/plugins/organization/organization.ts
+++ b/packages/better-auth/src/plugins/organization/organization.ts
@@ -1,7 +1,7 @@
import type { AuthContext, BetterAuthPlugin } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx } from "../../api";
import { shimContext } from "../../utils/shim";
@@ -223,9 +223,10 @@ const createHasPermission = (options: O) => {
ctx.body.organizationId ||
ctx.context.session.session.activeOrganizationId;
if (!activeOrganizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
@@ -233,10 +234,10 @@ const createHasPermission = (options: O) => {
organizationId: activeOrganizationId,
});
if (!member) {
- throw new APIError("UNAUTHORIZED", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const result = await hasPermission(
{
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-access-control.test.ts b/packages/better-auth/src/plugins/organization/routes/crud-access-control.test.ts
index 17938ee962..dd7d6ce840 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-access-control.test.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-access-control.test.ts
@@ -254,7 +254,7 @@ describe("dynamic access control", async (it) => {
expect(testRole.data).toBeNull();
if (!testRole.error) throw new Error("Test role error not found");
expect(testRole.error.message).toEqual(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE.message,
);
});
@@ -277,7 +277,7 @@ describe("dynamic access control", async (it) => {
if (testRole.data) throw new Error("Test role created");
expect(
testRole.error.message?.startsWith(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE.message,
),
).toBe(true);
expect("missingPermissions" in testRole.error).toBe(true);
@@ -306,7 +306,7 @@ describe("dynamic access control", async (it) => {
expect(testRole.data).toBeNull();
if (!testRole.error) throw new Error("Test role error not found");
expect(testRole.error.message).toEqual(
- ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
+ ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN.message,
);
const testRole2 = await authClient.organization.createRole(
@@ -326,7 +326,7 @@ describe("dynamic access control", async (it) => {
expect(testRole2.data).toBeNull();
if (!testRole2.error) throw new Error("Test role error not found");
expect(testRole2.error.message).toEqual(
- ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
+ ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN.message,
);
});
@@ -402,7 +402,7 @@ describe("dynamic access control", async (it) => {
headers: normalHeaders,
}),
).rejects.toThrow(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE.message,
);
});
@@ -416,7 +416,7 @@ describe("dynamic access control", async (it) => {
} catch (error: any) {
if ("body" in error && "message" in error.body) {
expect(error.body.message).toBe(
- ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.message,
);
} else {
throw error;
@@ -458,7 +458,7 @@ describe("dynamic access control", async (it) => {
it("should not be allowed to list roles without necessary permissions", async () => {
expect(auth.api.listOrgRoles({ headers: normalHeaders })).rejects.toThrow(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE.message,
);
});
@@ -809,7 +809,7 @@ describe("dynamic access control", async (it) => {
headers: freshMemberHeaders,
}),
).rejects.toThrow(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE.message,
);
// Admin should be able to update (has ac:update)
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-access-control.ts b/packages/better-auth/src/plugins/organization/routes/crud-access-control.ts
index ad34fa0aec..f7f5009b0e 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-access-control.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-access-control.ts
@@ -1,8 +1,8 @@
import type { GenericEndpointContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import type { Where } from "@better-auth/core/db/adapter";
+import { APIError } from "@better-auth/core/error";
import * as z from "zod";
-import { APIError } from "../../../api";
import type { InferAdditionalFieldsFromPluginOptions } from "../../../db";
import { toZodSchema } from "../../../db";
import type { User } from "../../../types";
@@ -112,9 +112,10 @@ export const createOrgRole = (options: O) => {
`[Dynamic Access Control] The organization plugin is missing a pre-defined ac instance.`,
`\nPlease refer to the documentation here: https://better-auth.com/docs/plugins/organization#dynamic-access-control`,
);
- throw new APIError("NOT_IMPLEMENTED", {
- message: ORGANIZATION_ERROR_CODES.MISSING_AC_INSTANCE,
- });
+ throw APIError.from(
+ "NOT_IMPLEMENTED",
+ ORGANIZATION_ERROR_CODES.MISSING_AC_INSTANCE,
+ );
}
// Get the organization id where the role will be created.
@@ -125,10 +126,10 @@ export const createOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The session is missing an active organization id to create a role. Either set an active org id, or pass an organizationId in the request body.`,
);
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE,
+ );
}
roleName = normalizeRoleName(roleName);
@@ -167,10 +168,10 @@ export const createOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const canCreateRole = await hasPermission(
@@ -193,10 +194,10 @@ export const createOrgRole = (options: O) => {
role: member.role,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
+ );
}
const maximumRolesPerOrganization =
@@ -227,9 +228,10 @@ export const createOrgRole = (options: O) => {
rolesInDB,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TOO_MANY_ROLES,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TOO_MANY_ROLES,
+ );
}
await checkForInvalidResources({ ac, ctx, permission });
@@ -327,9 +329,10 @@ export const deleteOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The session is missing an active organization id to delete a role. Either set an active org id, or pass an organizationId in the request body.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const member = await ctx.context.adapter.findOne({
@@ -357,10 +360,10 @@ export const deleteOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const canDeleteRole = await hasPermission(
@@ -383,10 +386,10 @@ export const deleteOrgRole = (options: O) => {
role: member.role,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE,
+ );
}
if (ctx.body.roleName) {
@@ -403,9 +406,10 @@ export const deleteOrgRole = (options: O) => {
defaultRoles,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.CANNOT_DELETE_A_PRE_DEFINED_ROLE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.CANNOT_DELETE_A_PRE_DEFINED_ROLE,
+ );
}
}
@@ -430,9 +434,10 @@ export const deleteOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The role name/id is not provided in the request body.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ );
}
const existingRoleInDB =
await ctx.context.adapter.findOne({
@@ -457,9 +462,10 @@ export const deleteOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ );
}
existingRoleInDB.permission = JSON.parse(
@@ -516,9 +522,10 @@ export const listOrgRoles = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The session is missing an active organization id to list roles. Either set an active org id, or pass an organizationId in the request query.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const member = await ctx.context.adapter.findOne({
@@ -546,10 +553,10 @@ export const listOrgRoles = (options: O) => {
organizationId,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const canListRoles = await hasPermission(
@@ -572,9 +579,10 @@ export const listOrgRoles = (options: O) => {
role: member.role,
},
);
- throw new APIError("FORBIDDEN", {
- message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE,
+ );
}
let roles = await ctx.context.adapter.findMany<
@@ -653,9 +661,10 @@ export const getOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The session is missing an active organization id to read a role. Either set an active org id, or pass an organizationId in the request query.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const member = await ctx.context.adapter.findOne({
@@ -683,10 +692,10 @@ export const getOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const canListRoles = await hasPermission(
@@ -709,9 +718,10 @@ export const getOrgRole = (options: O) => {
role: member.role,
},
);
- throw new APIError("FORBIDDEN", {
- message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE,
+ );
}
let condition: Where;
@@ -735,9 +745,10 @@ export const getOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The role name/id is not provided in the request query.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ );
}
let role = await ctx.context.adapter.findOne({
model: "organizationRole",
@@ -761,9 +772,10 @@ export const getOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ );
}
role.permission = JSON.parse(role.permission as never as string);
@@ -841,9 +853,10 @@ export const updateOrgRole = (options: O) => {
`[Dynamic Access Control] The organization plugin is missing a pre-defined ac instance.`,
`\nPlease refer to the documentation here: https://better-auth.com/docs/plugins/organization#dynamic-access-control`,
);
- throw new APIError("NOT_IMPLEMENTED", {
- message: ORGANIZATION_ERROR_CODES.MISSING_AC_INSTANCE,
- });
+ throw APIError.from(
+ "NOT_IMPLEMENTED",
+ ORGANIZATION_ERROR_CODES.MISSING_AC_INSTANCE,
+ );
}
const organizationId =
@@ -852,9 +865,10 @@ export const updateOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The session is missing an active organization id to update a role. Either set an active org id, or pass an organizationId in the request body.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const member = await ctx.context.adapter.findOne({
@@ -882,10 +896,10 @@ export const updateOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const canUpdateRole = await hasPermission(
@@ -903,10 +917,10 @@ export const updateOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The user is not permitted to update a role.`,
);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE,
+ );
}
let condition: Where;
@@ -930,9 +944,10 @@ export const updateOrgRole = (options: O) => {
ctx.context.logger.error(
`[Dynamic Access Control] The role name/id is not provided in the request body.`,
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ );
}
let role = await ctx.context.adapter.findOne({
model: "organizationRole",
@@ -956,9 +971,10 @@ export const updateOrgRole = (options: O) => {
organizationId,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
+ );
}
role.permission = role.permission
? JSON.parse(role.permission as never as string)
@@ -1069,9 +1085,10 @@ async function checkForInvalidResources({
validResources,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.INVALID_RESOURCE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.INVALID_RESOURCE,
+ );
}
}
@@ -1130,27 +1147,22 @@ async function checkIfMemberHasPermission({
missingPermissions,
},
);
- let errorMessage: string;
+ let error: { code: string; message: string };
if (action === "create")
- errorMessage =
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE;
+ error = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE;
else if (action === "update")
- errorMessage =
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE;
+ error = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE;
else if (action === "delete")
- errorMessage =
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE;
+ error = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE;
else if (action === "read")
- errorMessage =
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE;
+ error = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE;
else if (action === "list")
- errorMessage =
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE;
- else
- errorMessage = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE;
+ error = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE;
+ else error = ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE;
- throw new APIError("FORBIDDEN", {
- message: errorMessage,
+ throw APIError.fromStatus("FORBIDDEN", {
+ message: error.message,
+ code: error.code,
missingPermissions,
});
}
@@ -1179,9 +1191,10 @@ async function checkIfRoleNameIsTakenByPreDefinedRole({
defaultRoles,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
+ );
}
}
@@ -1219,8 +1232,9 @@ async function checkIfRoleNameIsTakenByRoleInDB({
organizationId,
},
);
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
+ );
}
}
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts
index 23b493edce..35ddd93713 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts
@@ -1,6 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx } from "../../../api/routes";
import { setSessionCookie } from "../../../cookies";
@@ -179,17 +178,16 @@ export const createInvitation = (option: O) => {
const organizationId =
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const email = ctx.body.email.toLowerCase();
const isValidEmail = z.email().safeParse(email);
if (!isValidEmail.success) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_EMAIL,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
}
const adapter = getOrgAdapter(ctx.context, option as O);
@@ -198,9 +196,10 @@ export const createInvitation = (option: O) => {
organizationId: organizationId,
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const canInvite = await hasPermission(
{
@@ -215,10 +214,10 @@ export const createInvitation = (option: O) => {
);
if (!canInvite) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
+ );
}
const creatorRole = ctx.context.orgOptions.creatorRole || "owner";
@@ -267,10 +266,10 @@ export const createInvitation = (option: O) => {
member.role !== creatorRole &&
roles.split(",").includes(creatorRole)
) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE,
+ );
}
const alreadyMember = await adapter.findMemberByEmail({
@@ -278,27 +277,28 @@ export const createInvitation = (option: O) => {
organizationId: organizationId,
});
if (alreadyMember) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const alreadyInvited = await adapter.findPendingInvitation({
email: email,
organizationId: organizationId,
});
if (alreadyInvited.length && !ctx.body.resend) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
+ );
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
// If resend is true and there's an existing invitation, reuse it
@@ -379,9 +379,10 @@ export const createInvitation = (option: O) => {
});
if (pendingInvitations.length >= invitationLimit) {
- throw new APIError("FORBIDDEN", {
- message: ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
+ );
}
if (
@@ -405,9 +406,10 @@ export const createInvitation = (option: O) => {
});
if (!team) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
const maximumMembersPerTeam =
@@ -420,9 +422,10 @@ export const createInvitation = (option: O) => {
})
: ctx.context.orgOptions.teams.maximumMembersPerTeam;
if (team.members.length >= maximumMembersPerTeam) {
- throw new APIError("FORBIDDEN", {
- message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
+ );
}
}
}
@@ -561,26 +564,27 @@ export const acceptInvitation = (options: O) =>
invitation.expiresAt < new Date() ||
invitation.status !== "pending"
) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
+ );
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
+ );
}
if (
ctx.context.orgOptions.requireEmailVerificationOnInvitation &&
!session.user.emailVerified
) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION,
+ );
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
@@ -589,19 +593,20 @@ export const acceptInvitation = (options: O) =>
});
if (membersCount >= membershipLimit) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
+ );
}
const organization = await adapter.findOrganizationById(
invitation.organizationId,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
// Run beforeAcceptInvitation hook
@@ -618,9 +623,10 @@ export const acceptInvitation = (options: O) =>
status: "accepted",
});
if (!acceptedI) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION,
+ );
}
if (
ctx.context.orgOptions.teams &&
@@ -654,9 +660,10 @@ export const acceptInvitation = (options: O) =>
: ctx.context.orgOptions.teams.maximumMembersPerTeam;
if (members >= maximumMembersPerTeam) {
- throw new APIError("FORBIDDEN", {
- message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
+ );
}
}
}
@@ -692,7 +699,7 @@ export const acceptInvitation = (options: O) =>
return ctx.json(null, {
status: 400,
body: {
- message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
+ message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND.message,
},
});
}
@@ -763,34 +770,36 @@ export const rejectInvitation = (options: O) =>
invitation.expiresAt < new Date() ||
invitation.status !== "pending"
) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "Invitation not found!",
+ code: "INVITATION_NOT_FOUND",
});
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
+ );
}
if (
ctx.context.orgOptions.requireEmailVerificationOnInvitation &&
!session.user.emailVerified
) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION,
+ );
}
const organization = await adapter.findOrganizationById(
invitation.organizationId,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
// Run beforeRejectInvitation hook
@@ -866,18 +875,20 @@ export const cancelInvitation = (options: O) =>
ctx.body.invitationId,
);
if (!invitation) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
+ );
}
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: invitation.organizationId,
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const canCancel = await hasPermission(
{
@@ -892,19 +903,20 @@ export const cancelInvitation = (options: O) =>
);
if (!canCancel) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION,
+ );
}
const organization = await adapter.findOrganizationById(
invitation.organizationId,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
// Run beforeCancelInvitation hook
@@ -1013,7 +1025,7 @@ export const getInvitation = (options: O) =>
async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.fromStatus("UNAUTHORIZED", {
message: "Not authenticated",
});
}
@@ -1024,33 +1036,34 @@ export const getInvitation = (options: O) =>
invitation.status !== "pending" ||
invitation.expiresAt < new Date()
) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Invitation not found!",
});
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
+ );
}
const organization = await adapter.findOrganizationById(
invitation.organizationId,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const member = await adapter.findMemberByOrgId({
userId: invitation.inviterId,
organizationId: invitation.organizationId,
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
return ctx.json({
@@ -1085,14 +1098,14 @@ export const listInvitations = (options: O) =>
async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.fromStatus("UNAUTHORIZED", {
message: "Not authenticated",
});
}
const orgId =
ctx.query?.organizationId || session.session.activeOrganizationId;
if (!orgId) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Organization ID is required",
});
}
@@ -1102,7 +1115,7 @@ export const listInvitations = (options: O) =>
organizationId: orgId,
});
if (!isMember) {
- throw new APIError("FORBIDDEN", {
+ throw APIError.fromStatus("FORBIDDEN", {
message: "You are not a member of this organization",
});
}
@@ -1208,14 +1221,14 @@ export const listUserInvitations = (
const session = await getSessionFromCtx(ctx);
if (ctx.request && ctx.query?.email) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "User email cannot be passed for client side API calls.",
});
}
const userEmail = session?.user.email || ctx.query?.email;
if (!userEmail) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Missing session headers, or email query parameter.",
});
}
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts b/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts
index c4992e2cea..3f31ce9a81 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts
@@ -236,7 +236,8 @@ describe("listMembers", async () => {
});
expect(members.error).toBeTruthy();
expect(members.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION
+ .message,
);
});
});
@@ -356,7 +357,8 @@ describe("updateMemberRole", async () => {
);
expect(updatedMember.error).toBeTruthy();
expect(updatedMember.error?.message).toBe(
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER
+ .message,
);
});
});
@@ -494,7 +496,9 @@ describe("inviteMember role validation", async () => {
expect(error).toBeTruthy();
expect(error?.status).toBe(400);
- expect(error?.message).toContain(ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND);
+ expect(error?.message).toContain(
+ ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.message,
+ );
});
it("should succeed when inviting with a valid default role", async () => {
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-members.ts b/packages/better-auth/src/plugins/organization/routes/crud-members.ts
index 2f40deb1ea..2b687d5aa8 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-members.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-members.ts
@@ -1,7 +1,6 @@
import type { LiteralString } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx, sessionMiddleware } from "../../../api";
import type { InferAdditionalFieldsFromPluginOptions } from "../../../db";
@@ -85,19 +84,17 @@ export const addMember = (option: O) => {
const orgId =
ctx.body.organizationId || session?.session.activeOrganizationId;
if (!orgId) {
- return ctx.json(null, {
- status: 400,
- body: {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- },
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const teamId =
"teamId" in ctx.body ? (ctx.body.teamId as string) : undefined;
if (teamId && !ctx.context.orgOptions.teams?.enabled) {
ctx.context.logger.error("Teams are not enabled");
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Teams are not enabled",
});
}
@@ -109,9 +106,7 @@ export const addMember = (option: O) => {
);
if (!user) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const alreadyMember = await adapter.findMemberByEmail({
@@ -120,10 +115,10 @@ export const addMember = (option: O) => {
});
if (alreadyMember) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
if (teamId) {
@@ -132,9 +127,10 @@ export const addMember = (option: O) => {
organizationId: orgId,
});
if (!team || team.organizationId !== orgId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
}
@@ -142,10 +138,10 @@ export const addMember = (option: O) => {
const count = await adapter.countMembers({ organizationId: orgId });
if (count >= membershipLimit) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
+ );
}
const {
@@ -157,9 +153,10 @@ export const addMember = (option: O) => {
const organization = await adapter.findOrganizationById(orgId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
let memberData = {
@@ -281,12 +278,10 @@ export const removeMember = (options: O) =>
const organizationId =
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
- return ctx.json(null, {
- status: 400,
- body: {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- },
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
@@ -294,9 +289,10 @@ export const removeMember = (options: O) =>
organizationId: organizationId,
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
let toBeRemovedMember: InferMember | null = null;
if (ctx.body.memberIdOrEmail.includes("@")) {
@@ -313,19 +309,20 @@ export const removeMember = (options: O) =>
}
}
if (!toBeRemovedMember) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const roles = toBeRemovedMember.role.split(",");
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const isOwner = roles.includes(creatorRole);
if (isOwner) {
if (member.role !== creatorRole) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
+ );
}
const { members } = await adapter.listMembers({
organizationId: organizationId,
@@ -335,10 +332,10 @@ export const removeMember = (options: O) =>
return roles.includes(creatorRole);
});
if (owners.length <= 1) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
+ );
}
}
const canDeleteMember = await hasPermission(
@@ -354,30 +351,32 @@ export const removeMember = (options: O) =>
);
if (!canDeleteMember) {
- throw new APIError("UNAUTHORIZED", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER,
+ );
}
if (toBeRemovedMember?.organizationId !== organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const userBeingRemoved = await ctx.context.internalAdapter.findUserById(
toBeRemovedMember.userId,
);
if (!userBeingRemoved) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "User not found",
});
}
@@ -501,16 +500,17 @@ export const updateMemberRole = (option: O) =>
const session = ctx.context.session;
if (!ctx.body.role) {
- throw new APIError("BAD_REQUEST");
+ throw APIError.fromStatus("BAD_REQUEST");
}
const organizationId =
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
@@ -526,9 +526,10 @@ export const updateMemberRole = (option: O) =>
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const toBeUpdatedMember =
@@ -537,19 +538,20 @@ export const updateMemberRole = (option: O) =>
: member;
if (!toBeUpdatedMember) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const memberBelongsToOrganization =
toBeUpdatedMember.organizationId === organizationId;
if (!memberBelongsToOrganization) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
+ );
}
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
@@ -568,10 +570,10 @@ export const updateMemberRole = (option: O) =>
(isUpdatingCreator && !updaterIsCreator) ||
(isSettingCreatorRole && !updaterIsCreator)
) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
+ );
}
if (updaterIsCreator && memberIsUpdatingThemselves) {
@@ -589,10 +591,10 @@ export const updateMemberRole = (option: O) =>
return roles.includes(creatorRole);
});
if (owners.length <= 1 && !isSettingCreatorRole) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER,
+ );
}
}
@@ -610,24 +612,25 @@ export const updateMemberRole = (option: O) =>
);
if (!canUpdateMember) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
+ );
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const userBeingUpdated = await ctx.context.internalAdapter.findUserById(
toBeUpdatedMember.userId,
);
if (!userBeingUpdated) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "User not found",
});
}
@@ -652,9 +655,10 @@ export const updateMemberRole = (option: O) =>
response.data.role || newRole,
);
if (!updatedMember) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
// Run afterUpdateMemberRole hook
@@ -676,9 +680,10 @@ export const updateMemberRole = (option: O) =>
newRole,
);
if (!updatedMember) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
// Run afterUpdateMemberRole hook
@@ -739,12 +744,10 @@ export const getActiveMember = (options: O) =>
const session = ctx.context.session;
const organizationId = session.session.activeOrganizationId;
if (!organizationId) {
- return ctx.json(null, {
- status: 400,
- body: {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- },
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
@@ -752,12 +755,10 @@ export const getActiveMember = (options: O) =>
organizationId: organizationId,
});
if (!member) {
- return ctx.json(null, {
- status: 400,
- body: {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- },
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
return ctx.json(member);
},
@@ -788,9 +789,10 @@ export const leaveOrganization = (options: O) =>
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
+ );
}
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const isOwnerLeaving = member.role.split(",").includes(creatorRole);
@@ -808,10 +810,10 @@ export const leaveOrganization = (options: O) =>
member.role.split(",").includes(creatorRole),
);
if (owners.length <= 1) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
+ );
}
}
await adapter.deleteMember({
@@ -908,16 +910,18 @@ export const listMembers = (options: O) =>
ctx.query?.organizationSlug,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
organizationId = organization.id;
}
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const isMember = await adapter.findMemberByOrgId({
@@ -925,10 +929,10 @@ export const listMembers = (options: O) =>
organizationId,
});
if (!isMember) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
const { members, total } = await adapter.listMembers({
organizationId,
@@ -998,26 +1002,28 @@ export const getActiveMemberRole = (
ctx.query?.organizationSlug,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
organizationId = organization.id;
}
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const isMember = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId,
});
if (!isMember) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
if (!ctx.query?.userId) {
return ctx.json({
@@ -1030,10 +1036,10 @@ export const getActiveMemberRole = (
organizationId,
});
if (!member) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
+ );
}
return ctx.json({
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-org.ts b/packages/better-auth/src/plugins/organization/routes/crud-org.ts
index 66bcfbb76f..58c8658137 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-org.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-org.ts
@@ -1,5 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx, requestOnlySessionMiddleware } from "../../../api";
import { setSessionCookie } from "../../../cookies";
@@ -101,12 +101,12 @@ export const createOrganization = (
const session = await getSessionFromCtx(ctx);
if (!session && (ctx.request || ctx.headers)) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.fromStatus("UNAUTHORIZED");
}
let user = session?.user || null;
if (!user) {
if (!ctx.body.userId) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.fromStatus("UNAUTHORIZED");
}
user = await ctx.context.internalAdapter.findUserById(ctx.body.userId);
}
@@ -126,10 +126,10 @@ export const createOrganization = (
const isSystemAction = !session && ctx.body.userId;
if (!canCreateOrg && !isSystemAction) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options as O);
@@ -142,19 +142,20 @@ export const createOrganization = (
: false;
if (hasReachedOrgLimit) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS,
+ );
}
const existingOrganization = await adapter.findOrganizationBySlug(
ctx.body.slug,
);
if (existingOrganization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS,
+ );
}
let {
@@ -357,9 +358,10 @@ export const checkOrganizationSlug = (
status: true,
});
}
- throw new APIError("BAD_REQUEST", {
- message: "slug is taken",
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_SLUG_ALREADY_TAKEN,
+ );
},
);
@@ -454,16 +456,17 @@ export const updateOrganization = (
async (ctx) => {
const session = await ctx.context.getSession(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.fromStatus("UNAUTHORIZED", {
message: "User not found",
});
}
const organizationId =
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
@@ -471,10 +474,10 @@ export const updateOrganization = (
organizationId: organizationId,
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const canUpdateOrg = await hasPermission(
{
@@ -488,10 +491,10 @@ export const updateOrganization = (
ctx,
);
if (!canUpdateOrg) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
+ );
}
// Check if slug is being updated and validate uniqueness
if (typeof ctx.body.data.slug === "string") {
@@ -502,9 +505,10 @@ export const updateOrganization = (
existingOrganization &&
existingOrganization.id !== organizationId
) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_SLUG_ALREADY_TAKEN,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_SLUG_ALREADY_TAKEN,
+ );
}
}
if (options?.organizationHooks?.beforeUpdateOrganization) {
@@ -582,13 +586,14 @@ export const deleteOrganization = (
"`organizationDeletion.disabled` is deprecated. Use `disableOrganizationDeletion` instead",
);
}
- throw new APIError("NOT_FOUND", {
+ throw APIError.from("NOT_FOUND", {
message: "Organization deletion is disabled",
+ code: "ORGANIZATION_DELETION_DISABLED",
});
}
const session = await ctx.context.getSession(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED", { status: 401 });
+ throw APIError.fromStatus("UNAUTHORIZED");
}
const organizationId = ctx.body.organizationId;
@@ -596,7 +601,7 @@ export const deleteOrganization = (
return ctx.json(null, {
status: 400,
body: {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND.message,
},
});
}
@@ -606,10 +611,10 @@ export const deleteOrganization = (
organizationId: organizationId,
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const canDeleteOrg = await hasPermission(
{
@@ -623,10 +628,10 @@ export const deleteOrganization = (
ctx,
);
if (!canDeleteOrg) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION,
+ );
}
if (organizationId === session.session.activeOrganizationId) {
/**
@@ -637,7 +642,7 @@ export const deleteOrganization = (
const org = await adapter.findOrganizationById(organizationId);
if (!org) {
- throw new APIError("BAD_REQUEST");
+ throw APIError.fromStatus("BAD_REQUEST");
}
if (options?.organizationHooks?.beforeDeleteOrganization) {
await options.organizationHooks.beforeDeleteOrganization({
@@ -733,9 +738,10 @@ export const getFullOrganization = (
membersLimit: ctx.query?.membersLimit,
});
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const isMember = await adapter.checkMembership({
userId: session.user.id,
@@ -745,7 +751,8 @@ export const getFullOrganization = (
await adapter.setActiveOrganization(session.session.token, null, ctx);
throw new APIError("FORBIDDEN", {
message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION
+ .message,
});
}
@@ -847,17 +854,19 @@ export const setActiveOrganization = (
const organization =
await adapter.findOrganizationBySlug(organizationSlug);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
organizationId = organization.id;
}
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const isMember = await adapter.checkMembership({
@@ -866,17 +875,18 @@ export const setActiveOrganization = (
});
if (!isMember) {
await adapter.setActiveOrganization(session.session.token, null, ctx);
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
let organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const updatedSession = await adapter.setActiveOrganization(
session.session.token,
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-team.ts b/packages/better-auth/src/plugins/organization/routes/crud-team.ts
index f71345368a..d522e0ca07 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-team.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-team.ts
@@ -1,5 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx } from "../../../api";
import { setSessionCookie } from "../../../cookies";
@@ -99,13 +99,14 @@ export const createTeam = (options: O) => {
const organizationId =
ctx.body.organizationId || session?.session.activeOrganizationId;
if (!session && (ctx.request || ctx.headers)) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.fromStatus("UNAUTHORIZED");
}
if (!organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options as O);
if (session) {
@@ -114,10 +115,10 @@ export const createTeam = (options: O) => {
organizationId,
});
if (!member) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
+ );
}
const canCreate = await hasPermission(
{
@@ -132,10 +133,10 @@ export const createTeam = (options: O) => {
);
if (!canCreate) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION,
+ );
}
}
@@ -153,18 +154,19 @@ export const createTeam = (options: O) => {
const maxTeamsReached = maximum ? existingTeams.length >= maximum : false;
if (maxTeamsReached) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS,
+ );
}
const { name, organizationId: _, ...additionalFields } = ctx.body;
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
let teamData = {
@@ -261,15 +263,13 @@ export const removeTeam = (options: O) =>
const organizationId =
ctx.body.organizationId || session?.session.activeOrganizationId;
if (!organizationId) {
- return ctx.json(null, {
- status: 400,
- body: {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- },
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
if (!session && (ctx.request || ctx.headers)) {
- throw new APIError("UNAUTHORIZED");
+ throw APIError.fromStatus("UNAUTHORIZED");
}
const adapter = getOrgAdapter(ctx.context, options);
if (session) {
@@ -279,10 +279,10 @@ export const removeTeam = (options: O) =>
});
if (!member || session.session?.activeTeamId === ctx.body.teamId) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM,
+ );
}
const canRemove = await hasPermission(
@@ -298,10 +298,10 @@ export const removeTeam = (options: O) =>
);
if (!canRemove) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION,
+ );
}
}
const team = await adapter.findTeamById({
@@ -309,25 +309,28 @@ export const removeTeam = (options: O) =>
organizationId,
});
if (!team || team.organizationId !== organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
if (!ctx.context.orgOptions.teams?.allowRemovingAllTeams) {
const teams = await adapter.listTeams(organizationId);
if (teams.length <= 1) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.UNABLE_TO_REMOVE_LAST_TEAM,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.UNABLE_TO_REMOVE_LAST_TEAM,
+ );
}
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
// Run beforeDeleteTeam hook
@@ -443,12 +446,10 @@ export const updateTeam = (options: O) => {
const organizationId =
ctx.body.data.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
- return ctx.json(null, {
- status: 400,
- body: {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- },
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
@@ -457,10 +458,10 @@ export const updateTeam = (options: O) => {
});
if (!member) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
+ );
}
const canUpdate = await hasPermission(
@@ -476,10 +477,10 @@ export const updateTeam = (options: O) => {
);
if (!canUpdate) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
+ );
}
const team = await adapter.findTeamById({
@@ -488,18 +489,20 @@ export const updateTeam = (options: O) => {
});
if (!team || team.organizationId !== organizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
const { name, organizationId: __, ...additionalFields } = ctx.body.data;
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const updates = {
@@ -634,9 +637,10 @@ export const listOrganizationTeams = (
const organizationId =
ctx.query?.organizationId || session?.session.activeOrganizationId;
if (!organizationId) {
- throw ctx.error("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
@@ -644,10 +648,10 @@ export const listOrganizationTeams = (
organizationId: organizationId || "",
});
if (!member) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION,
+ );
}
const teams = await adapter.listTeams(organizationId);
return ctx.json(teams);
@@ -733,9 +737,10 @@ export const setActiveTeam = (options: O) =>
const team = await adapter.findTeamById({ teamId });
if (!team) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
const member = await adapter.findTeamMember({
@@ -744,9 +749,10 @@ export const setActiveTeam = (options: O) =>
});
if (!member) {
- throw new APIError("FORBIDDEN", {
- message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
+ );
}
const updatedSession = await adapter.setActiveTeam(
@@ -874,9 +880,10 @@ export const listTeamMembers = (options: O) =>
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
let teamId = ctx.query?.teamId || session?.session.activeTeamId;
if (!teamId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM,
+ );
}
const member = await adapter.findTeamMember({
userId: session.user.id,
@@ -884,9 +891,10 @@ export const listTeamMembers = (options: O) =>
});
if (!member) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
+ );
}
const members = await adapter.listTeamMembers({
teamId,
@@ -960,9 +968,10 @@ export const addTeamMember = (options: O) =>
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
if (!session.session.activeOrganizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const currentMember = await adapter.findMemberByOrgId({
@@ -971,10 +980,10 @@ export const addTeamMember = (options: O) =>
});
if (!currentMember) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const canUpdateMember = await hasPermission(
@@ -990,10 +999,10 @@ export const addTeamMember = (options: O) =>
);
if (!canUpdateMember) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER,
+ );
}
const toBeAddedMember = await adapter.findMemberByOrgId({
@@ -1002,10 +1011,10 @@ export const addTeamMember = (options: O) =>
});
if (!toBeAddedMember) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const team = await adapter.findTeamById({
@@ -1014,25 +1023,27 @@ export const addTeamMember = (options: O) =>
});
if (!team) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
const organization = await adapter.findOrganizationById(
session.session.activeOrganizationId,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const userBeingAdded = await ctx.context.internalAdapter.findUserById(
ctx.body.userId,
);
if (!userBeingAdded) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "User not found",
});
}
@@ -1122,9 +1133,10 @@ export const removeTeamMember = (options: O) =>
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
if (!session.session.activeOrganizationId) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
}
const currentMember = await adapter.findMemberByOrgId({
@@ -1133,10 +1145,10 @@ export const removeTeamMember = (options: O) =>
});
if (!currentMember) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const canDeleteMember = await hasPermission(
@@ -1152,10 +1164,10 @@ export const removeTeamMember = (options: O) =>
);
if (!canDeleteMember) {
- throw new APIError("FORBIDDEN", {
- message:
- ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER,
+ );
}
const toBeAddedMember = await adapter.findMemberByOrgId({
@@ -1164,10 +1176,10 @@ export const removeTeamMember = (options: O) =>
});
if (!toBeAddedMember) {
- throw new APIError("BAD_REQUEST", {
- message:
- ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
+ );
}
const team = await adapter.findTeamById({
@@ -1176,25 +1188,27 @@ export const removeTeamMember = (options: O) =>
});
if (!team) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
+ );
}
const organization = await adapter.findOrganizationById(
session.session.activeOrganizationId,
);
if (!organization) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
+ );
}
const userBeingRemoved = await ctx.context.internalAdapter.findUserById(
ctx.body.userId,
);
if (!userBeingRemoved) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "User not found",
});
}
@@ -1205,9 +1219,10 @@ export const removeTeamMember = (options: O) =>
});
if (!teamMember) {
- throw new APIError("BAD_REQUEST", {
- message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
+ );
}
// Run beforeRemoveTeamMember hook
diff --git a/packages/better-auth/src/plugins/phone-number/client.ts b/packages/better-auth/src/plugins/phone-number/client.ts
index 543256901d..c27a16d394 100644
--- a/packages/better-auth/src/plugins/phone-number/client.ts
+++ b/packages/better-auth/src/plugins/phone-number/client.ts
@@ -1,6 +1,10 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { phoneNumber } from ".";
+import { PHONE_NUMBER_ERROR_CODES } from "./error-codes";
+
+export * from "./error-codes";
+
export const phoneNumberClient = () => {
return {
id: "phoneNumber",
@@ -17,6 +21,7 @@ export const phoneNumberClient = () => {
signal: "$sessionSignal",
},
],
+ $ERROR_CODES: PHONE_NUMBER_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/phone-number/index.ts b/packages/better-auth/src/plugins/phone-number/index.ts
index 37a71dfd75..abdc1ad3b7 100644
--- a/packages/better-auth/src/plugins/phone-number/index.ts
+++ b/packages/better-auth/src/plugins/phone-number/index.ts
@@ -1,6 +1,6 @@
import type { BetterAuthPlugin } from "@better-auth/core";
import { createAuthMiddleware } from "@better-auth/core/api";
-import { APIError } from "better-call";
+import { APIError } from "@better-auth/core/error";
import { mergeSchema } from "../../db/schema";
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes";
import type { RequiredPhoneNumberOptions } from "./routes";
@@ -36,9 +36,10 @@ export const phoneNumber = (options?: PhoneNumberOptions | undefined) => {
matcher: (ctx) =>
ctx.path === "/update-user" && "phoneNumber" in ctx.body,
handler: createAuthMiddleware(async (_ctx) => {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_CANNOT_BE_UPDATED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_CANNOT_BE_UPDATED,
+ );
}),
},
],
diff --git a/packages/better-auth/src/plugins/phone-number/routes.ts b/packages/better-auth/src/plugins/phone-number/routes.ts
index 42768f6aeb..f1b9e94417 100644
--- a/packages/better-auth/src/plugins/phone-number/routes.ts
+++ b/packages/better-auth/src/plugins/phone-number/routes.ts
@@ -1,6 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { getSessionFromCtx } from "../../api";
import { setSessionCookie } from "../../cookies";
@@ -93,9 +92,10 @@ export const signInPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
ctx.body.phoneNumber,
);
if (!isValidNumber) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER,
+ );
}
}
@@ -109,9 +109,10 @@ export const signInPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
],
});
if (!user) {
- throw new APIError("UNAUTHORIZED", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
+ );
}
if (opts.requireVerification) {
if (!user.phoneNumberVerified) {
@@ -132,9 +133,10 @@ export const signInPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
),
);
}
- throw new APIError("UNAUTHORIZED", {
- message: PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_NOT_VERIFIED,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_NOT_VERIFIED,
+ );
}
}
const accounts = await ctx.context.internalAdapter.findAccountByUserId(
@@ -147,16 +149,18 @@ export const signInPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
ctx.context.logger.error("Credential account not found", {
phoneNumber,
});
- throw new APIError("UNAUTHORIZED", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
+ );
}
const currentPassword = credentialAccount?.password;
if (!currentPassword) {
ctx.context.logger.error("Password not found", { phoneNumber });
- throw new APIError("UNAUTHORIZED", {
- message: PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR,
+ );
}
const validPassword = await ctx.context.password.verify({
hash: currentPassword,
@@ -164,9 +168,10 @@ export const signInPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
});
if (!validPassword) {
ctx.context.logger.error("Invalid password");
- throw new APIError("UNAUTHORIZED", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
+ );
}
const session = await ctx.context.internalAdapter.createSession(
user.id,
@@ -174,9 +179,10 @@ export const signInPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
);
if (!session) {
ctx.context.logger.error("Failed to create session");
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
await setSessionCookie(
@@ -258,9 +264,10 @@ export const sendPhoneNumberOTP = (opts: RequiredPhoneNumberOptions) =>
async (ctx) => {
if (!opts?.sendOTP) {
ctx.context.logger.warn("sendOTP not implemented");
- throw new APIError("NOT_IMPLEMENTED", {
- message: PHONE_NUMBER_ERROR_CODES.SEND_OTP_NOT_IMPLEMENTED,
- });
+ throw APIError.from(
+ "NOT_IMPLEMENTED",
+ PHONE_NUMBER_ERROR_CODES.SEND_OTP_NOT_IMPLEMENTED,
+ );
}
if (opts.phoneNumberValidator) {
@@ -268,9 +275,10 @@ export const sendPhoneNumberOTP = (opts: RequiredPhoneNumberOptions) =>
ctx.body.phoneNumber,
);
if (!isValidNumber) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER,
+ );
}
}
@@ -460,9 +468,10 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
);
if (!isValid) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.INVALID_OTP,
+ );
}
// Clean up verification value
@@ -480,29 +489,33 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
if (!otp || otp.expiresAt < new Date()) {
if (otp && otp.expiresAt < new Date()) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.OTP_EXPIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.OTP_EXPIRED,
+ );
}
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.OTP_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.OTP_NOT_FOUND,
+ );
}
const [otpValue, attempts] = otp.value.split(":");
const allowedAttempts = opts?.allowedAttempts || 3;
if (attempts && parseInt(attempts) >= allowedAttempts) {
await ctx.context.internalAdapter.deleteVerificationValue(otp.id);
- throw new APIError("FORBIDDEN", {
- message: PHONE_NUMBER_ERROR_CODES.TOO_MANY_ATTEMPTS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ PHONE_NUMBER_ERROR_CODES.TOO_MANY_ATTEMPTS,
+ );
}
if (otpValue !== ctx.body.code) {
await ctx.context.internalAdapter.updateVerificationValue(otp.id, {
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
});
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.INVALID_OTP,
+ );
}
await ctx.context.internalAdapter.deleteVerificationValue(otp.id);
@@ -511,9 +524,7 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
if (ctx.body.updatePhoneNumber) {
const session = await getSessionFromCtx(ctx);
if (!session) {
- throw new APIError("UNAUTHORIZED", {
- message: BASE_ERROR_CODES.USER_NOT_FOUND,
- });
+ throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.USER_NOT_FOUND);
}
const existingUser =
await ctx.context.adapter.findMany({
@@ -526,9 +537,10 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
],
});
if (existingUser.length) {
- throw ctx.error("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_EXIST,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_EXIST,
+ );
}
let user =
await ctx.context.internalAdapter.updateUser(
@@ -578,9 +590,10 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
[opts.phoneNumberVerified]: true,
});
if (!user) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
+ );
}
}
} else {
@@ -593,9 +606,10 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
);
}
if (!user) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: BASE_ERROR_CODES.FAILED_TO_UPDATE_USER,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_UPDATE_USER,
+ );
}
await opts?.callbackOnVerification?.(
@@ -611,9 +625,10 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
user.id,
);
if (!session) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
await setSessionCookie(ctx, {
session,
@@ -784,14 +799,16 @@ export const resetPasswordPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
`${ctx.body.phoneNumber}-request-password-reset`,
);
if (!verification) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.OTP_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.OTP_NOT_FOUND,
+ );
}
if (verification.expiresAt < new Date()) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.OTP_EXPIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.OTP_EXPIRED,
+ );
}
const [otpValue, attempts] = verification.value.split(":");
const allowedAttempts = opts?.allowedAttempts || 3;
@@ -799,9 +816,10 @@ export const resetPasswordPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
await ctx.context.internalAdapter.deleteVerificationValue(
verification.id,
);
- throw new APIError("FORBIDDEN", {
- message: PHONE_NUMBER_ERROR_CODES.TOO_MANY_ATTEMPTS,
- });
+ throw APIError.from(
+ "FORBIDDEN",
+ PHONE_NUMBER_ERROR_CODES.TOO_MANY_ATTEMPTS,
+ );
}
if (ctx.body.otp !== otpValue) {
await ctx.context.internalAdapter.updateVerificationValue(
@@ -810,9 +828,10 @@ export const resetPasswordPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
},
);
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.INVALID_OTP,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.INVALID_OTP,
+ );
}
const user = await ctx.context.adapter.findOne({
model: "user",
@@ -824,21 +843,18 @@ export const resetPasswordPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
],
});
if (!user) {
- throw new APIError("BAD_REQUEST", {
- message: PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR,
+ );
}
const minLength = ctx.context.password.config.minPasswordLength;
const maxLength = ctx.context.password.config.maxPasswordLength;
if (ctx.body.newPassword.length < minLength) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
if (ctx.body.newPassword.length > maxLength) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const hashedPassword = await ctx.context.password.hash(
ctx.body.newPassword,
diff --git a/packages/better-auth/src/plugins/siwe/index.ts b/packages/better-auth/src/plugins/siwe/index.ts
index 304403fcc7..5a46291e75 100644
--- a/packages/better-auth/src/plugins/siwe/index.ts
+++ b/packages/better-auth/src/plugins/siwe/index.ts
@@ -6,6 +6,7 @@ import { setSessionCookie } from "../../cookies";
import { mergeSchema } from "../../db/schema";
import type { InferOptionSchema, User } from "../../types";
import { toChecksumAddress } from "../../utils/hashing";
+import { isAPIError } from "../../utils/is-api-error";
import { getOrigin } from "../../utils/url";
import { schema } from "./schema";
import type {
@@ -99,7 +100,7 @@ export const siwe = (options: SIWEPluginOptions) =>
const isAnon = options.anonymous ?? true;
if (!isAnon && !email) {
- throw new APIError("BAD_REQUEST", {
+ throw APIError.fromStatus("BAD_REQUEST", {
message: "Email is required when anonymous is disabled.",
status: 400,
});
@@ -114,7 +115,7 @@ export const siwe = (options: SIWEPluginOptions) =>
// Ensure nonce is valid and not expired
if (!verification || new Date() > verification.expiresAt) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.fromStatus("UNAUTHORIZED", {
message: "Unauthorized: Invalid or expired nonce",
status: 401,
code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE",
@@ -142,7 +143,7 @@ export const siwe = (options: SIWEPluginOptions) =>
});
if (!verified) {
- throw new APIError("UNAUTHORIZED", {
+ throw APIError.fromStatus("UNAUTHORIZED", {
message: "Unauthorized: Invalid SIWE signature",
status: 401,
});
@@ -270,7 +271,7 @@ export const siwe = (options: SIWEPluginOptions) =>
);
if (!session) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.fromStatus("INTERNAL_SERVER_ERROR", {
message: "Internal Server Error",
status: 500,
});
@@ -288,8 +289,8 @@ export const siwe = (options: SIWEPluginOptions) =>
},
});
} catch (error: unknown) {
- if (error instanceof APIError) throw error;
- throw new APIError("UNAUTHORIZED", {
+ if (isAPIError(error)) throw error;
+ throw APIError.fromStatus("UNAUTHORIZED", {
message: "Something went wrong. Please try again later.",
error: error instanceof Error ? error.message : "Unknown error",
status: 401,
diff --git a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts
index 516d5ddfcd..bfba8b4d28 100644
--- a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts
@@ -1,6 +1,6 @@
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
-import { APIError } from "better-call";
import * as z from "zod";
import { sessionMiddleware } from "../../../api";
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
@@ -317,9 +317,10 @@ export const backupCode2fa = (opts: BackupCodeOptions) => {
],
});
if (!twoFactor) {
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
+ );
}
const validate = await verifyBackupCode(
{
@@ -330,9 +331,10 @@ export const backupCode2fa = (opts: BackupCodeOptions) => {
opts,
);
if (!validate.status) {
- throw new APIError("UNAUTHORIZED", {
- message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
+ );
}
const updatedBackupCodes = await symmetricEncrypt({
key: ctx.context.secret,
@@ -356,7 +358,7 @@ export const backupCode2fa = (opts: BackupCodeOptions) => {
],
});
if (!updated) {
- throw new APIError("CONFLICT", {
+ throw APIError.fromStatus("CONFLICT", {
message: "Failed to verify backup code. Please try again.",
});
}
@@ -436,9 +438,10 @@ export const backupCode2fa = (opts: BackupCodeOptions) => {
async (ctx) => {
const user = ctx.context.session.user as UserWithTwoFactor;
if (!user.twoFactorEnabled) {
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED,
+ );
}
await ctx.context.password.checkPassword(user.id, ctx);
const backupCodes = await generateBackupCodes(
@@ -491,9 +494,10 @@ export const backupCode2fa = (opts: BackupCodeOptions) => {
],
});
if (!twoFactor) {
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
+ );
}
const decryptedBackupCodes = await getBackupCodes(
twoFactor.backupCodes,
@@ -502,9 +506,10 @@ export const backupCode2fa = (opts: BackupCodeOptions) => {
);
if (!decryptedBackupCodes) {
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
+ );
}
return ctx.json({
status: true,
diff --git a/packages/better-auth/src/plugins/two-factor/client.ts b/packages/better-auth/src/plugins/two-factor/client.ts
index 9f2d776c84..b9621b1312 100644
--- a/packages/better-auth/src/plugins/two-factor/client.ts
+++ b/packages/better-auth/src/plugins/two-factor/client.ts
@@ -1,5 +1,8 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { twoFactor as twoFa } from ".";
+import { TWO_FACTOR_ERROR_CODES } from "./error-code";
+
+export * from "./error-code";
export const twoFactorClient = (
options?:
@@ -42,6 +45,7 @@ export const twoFactorClient = (
},
},
],
+ $ERROR_CODES: TWO_FACTOR_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts
index 050685b53f..75db7b5bdb 100644
--- a/packages/better-auth/src/plugins/two-factor/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/index.ts
@@ -3,10 +3,9 @@ import {
createAuthEndpoint,
createAuthMiddleware,
} from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { createHMAC } from "@better-auth/utils/hmac";
import { createOTP } from "@better-auth/utils/otp";
-import { APIError } from "better-call";
import * as z from "zod";
import { sessionMiddleware } from "../../api";
import { deleteSessionCookie, setSessionCookie } from "../../cookies";
@@ -127,9 +126,10 @@ export const twoFactor = (options?: O) => {
userId: user.id,
});
if (!isPasswordValid) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_PASSWORD,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.INVALID_PASSWORD,
+ );
}
const secret = generateRandomString(32);
const encryptedSecret = await symmetricEncrypt({
@@ -245,9 +245,10 @@ export const twoFactor = (options?: O) => {
userId: user.id,
});
if (!isPasswordValid) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.INVALID_PASSWORD,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.INVALID_PASSWORD,
+ );
}
const updatedUser = await ctx.context.internalAdapter.updateUser(
user.id,
diff --git a/packages/better-auth/src/plugins/two-factor/otp/index.ts b/packages/better-auth/src/plugins/two-factor/otp/index.ts
index 63116fe7d0..93382e80cf 100644
--- a/packages/better-auth/src/plugins/two-factor/otp/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/otp/index.ts
@@ -1,7 +1,6 @@
import type { Awaitable, GenericEndpointContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { setSessionCookie } from "../../../cookies";
import {
@@ -189,8 +188,9 @@ export const otp2fa = (options?: OTPOptions | undefined) => {
ctx.context.logger.error(
"send otp isn't configured. Please configure the send otp function on otp options.",
);
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "otp isn't configured",
+ code: "OTP_NOT_CONFIGURED",
});
}
const { session, key } = await verifyTwoFactor(ctx);
@@ -306,18 +306,20 @@ export const otp2fa = (options?: OTPOptions | undefined) => {
toCheckOtp.id,
);
}
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED,
+ );
}
const allowedAttempts = options?.allowedAttempts || 5;
if (parseInt(counter!) >= allowedAttempts) {
await ctx.context.internalAdapter.deleteVerificationValue(
toCheckOtp.id,
);
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE,
+ );
}
const isCodeValid = constantTimeEqual(
new TextEncoder().encode(decryptedOtp),
@@ -326,9 +328,10 @@ export const otp2fa = (options?: OTPOptions | undefined) => {
if (isCodeValid) {
if (!session.user.twoFactorEnabled) {
if (!session.session) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
const updatedUser = await ctx.context.internalAdapter.updateUser(
session.user.id,
diff --git a/packages/better-auth/src/plugins/two-factor/totp/index.ts b/packages/better-auth/src/plugins/two-factor/totp/index.ts
index f03f1a3ae6..cc33d6b658 100644
--- a/packages/better-auth/src/plugins/two-factor/totp/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/totp/index.ts
@@ -1,7 +1,6 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { createOTP } from "@better-auth/utils/otp";
-import { APIError } from "better-call";
import * as z from "zod";
import { sessionMiddleware } from "../../../api";
import { setSessionCookie } from "../../../cookies";
@@ -113,8 +112,9 @@ export const totp2fa = (options?: TOTPOptions | undefined) => {
ctx.context.logger.error(
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
);
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "totp isn't configured",
+ code: "TOTP_NOT_CONFIGURED",
});
}
const code = await createOTP(ctx.body.secret, {
@@ -160,8 +160,9 @@ export const totp2fa = (options?: TOTPOptions | undefined) => {
ctx.context.logger.error(
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
);
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "totp isn't configured",
+ code: "TOTP_NOT_CONFIGURED",
});
}
const user = ctx.context.session.user as UserWithTwoFactor;
@@ -175,9 +176,10 @@ export const totp2fa = (options?: TOTPOptions | undefined) => {
],
});
if (!twoFactor) {
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
+ );
}
const secret = await symmetricDecrypt({
key: ctx.context.secret,
@@ -228,8 +230,9 @@ export const totp2fa = (options?: TOTPOptions | undefined) => {
ctx.context.logger.error(
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
);
- throw new APIError("BAD_REQUEST", {
+ throw APIError.from("BAD_REQUEST", {
message: "totp isn't configured",
+ code: "TOTP_NOT_CONFIGURED",
});
}
const { session, valid, invalid } = await verifyTwoFactor(ctx);
@@ -245,9 +248,10 @@ export const totp2fa = (options?: TOTPOptions | undefined) => {
});
if (!twoFactor) {
- throw new APIError("BAD_REQUEST", {
- message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
+ );
}
const decrypted = await symmetricDecrypt({
key: ctx.context.secret,
@@ -263,9 +267,10 @@ export const totp2fa = (options?: TOTPOptions | undefined) => {
if (!user.twoFactorEnabled) {
if (!session.session) {
- throw new APIError("BAD_REQUEST", {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ );
}
const updatedUser = await ctx.context.internalAdapter.updateUser(
user.id,
diff --git a/packages/better-auth/src/plugins/two-factor/two-factor.test.ts b/packages/better-auth/src/plugins/two-factor/two-factor.test.ts
index 28b3256cc7..7a51dbb4fa 100644
--- a/packages/better-auth/src/plugins/two-factor/two-factor.test.ts
+++ b/packages/better-auth/src/plugins/two-factor/two-factor.test.ts
@@ -245,7 +245,7 @@ describe("two factor", async () => {
},
});
expect(verifyRes.error?.message).toBe(
- TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
+ TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE.message,
);
});
@@ -256,7 +256,9 @@ describe("two factor", async () => {
headers,
},
});
- expect(res.error?.message).toBe(TWO_FACTOR_ERROR_CODES.INVALID_CODE);
+ expect(res.error?.message).toBe(
+ TWO_FACTOR_ERROR_CODES.INVALID_CODE.message,
+ );
});
let backupCodes: string[] = [];
diff --git a/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts b/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts
index 29c24a7107..361a271d81 100644
--- a/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts
+++ b/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts
@@ -1,6 +1,6 @@
import type { GenericEndpointContext } from "@better-auth/core";
+import { APIError } from "@better-auth/core/error";
import { createHMAC } from "@better-auth/utils/hmac";
-import { APIError } from "better-call";
import { getSessionFromCtx } from "../../api";
import { setSessionCookie } from "../../cookies";
import {
@@ -13,9 +13,7 @@ import type { UserWithTwoFactor } from "./types";
export async function verifyTwoFactor(ctx: GenericEndpointContext) {
const invalid = (errorKey: keyof typeof TWO_FACTOR_ERROR_CODES) => {
- throw new APIError("UNAUTHORIZED", {
- message: TWO_FACTOR_ERROR_CODES[errorKey],
- });
+ throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES[errorKey]);
};
const session = await getSessionFromCtx(ctx);
@@ -26,24 +24,27 @@ export async function verifyTwoFactor(ctx: GenericEndpointContext) {
ctx.context.secret,
);
if (!twoFactorCookie) {
- throw new APIError("UNAUTHORIZED", {
- message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
+ );
}
const verificationToken =
await ctx.context.internalAdapter.findVerificationValue(twoFactorCookie);
if (!verificationToken) {
- throw new APIError("UNAUTHORIZED", {
- message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
+ );
}
const user = (await ctx.context.internalAdapter.findUserById(
verificationToken.value,
)) as UserWithTwoFactor;
if (!user) {
- throw new APIError("UNAUTHORIZED", {
- message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
+ );
}
const dontRememberMe = await ctx.getSignedCookie(
ctx.context.authCookies.dontRememberToken.name,
@@ -56,8 +57,9 @@ export async function verifyTwoFactor(ctx: GenericEndpointContext) {
!!dontRememberMe,
);
if (!session) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
+ throw APIError.from("INTERNAL_SERVER_ERROR", {
message: "failed to create session",
+ code: "FAILED_TO_CREATE_SESSION",
});
}
// Delete the verification token from the database after successful verification
diff --git a/packages/better-auth/src/plugins/username/client.ts b/packages/better-auth/src/plugins/username/client.ts
index 4eba6e1218..1b4cb225c4 100644
--- a/packages/better-auth/src/plugins/username/client.ts
+++ b/packages/better-auth/src/plugins/username/client.ts
@@ -1,6 +1,10 @@
import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { username } from ".";
+import { USERNAME_ERROR_CODES } from "./error-codes";
+
+export * from "./error-codes";
+
export const usernameClient = () => {
return {
id: "username",
@@ -11,5 +15,6 @@ export const usernameClient = () => {
signal: "$sessionSignal",
},
],
+ $ERROR_CODES: USERNAME_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
diff --git a/packages/better-auth/src/plugins/username/index.ts b/packages/better-auth/src/plugins/username/index.ts
index ff950936cf..1a069dbb35 100644
--- a/packages/better-auth/src/plugins/username/index.ts
+++ b/packages/better-auth/src/plugins/username/index.ts
@@ -4,8 +4,7 @@ import {
createAuthMiddleware,
} from "@better-auth/core/api";
import type { Account, User } from "@better-auth/core/db";
-import { BASE_ERROR_CODES } from "@better-auth/core/error";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { createEmailVerificationToken } from "../../api";
import { setSessionCookie } from "../../cookies";
@@ -240,9 +239,10 @@ export const username = (options?: UsernameOptions | undefined) => {
async (ctx) => {
if (!ctx.body.username || !ctx.body.password) {
ctx.context.logger.error("Username or password not found");
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
+ );
}
const username =
@@ -257,19 +257,20 @@ export const username = (options?: UsernameOptions | undefined) => {
ctx.context.logger.error("Username too short", {
username,
});
- throw new APIError("UNPROCESSABLE_ENTITY", {
- code: "USERNAME_TOO_SHORT",
- message: ERROR_CODES.USERNAME_TOO_SHORT,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.USERNAME_TOO_SHORT,
+ );
}
if (username.length > maxUsernameLength) {
ctx.context.logger.error("Username too long", {
username,
});
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: ERROR_CODES.USERNAME_TOO_LONG,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.USERNAME_TOO_LONG,
+ );
}
const validator =
@@ -277,9 +278,10 @@ export const username = (options?: UsernameOptions | undefined) => {
const valid = await validator(username);
if (!valid) {
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: ERROR_CODES.INVALID_USERNAME,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.INVALID_USERNAME,
+ );
}
const user = await ctx.context.adapter.findOne<
@@ -300,9 +302,10 @@ export const username = (options?: UsernameOptions | undefined) => {
ctx.context.logger.error("User not found", {
username,
});
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
+ );
}
const account = await ctx.context.adapter.findOne({
@@ -319,18 +322,20 @@ export const username = (options?: UsernameOptions | undefined) => {
],
});
if (!account) {
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
+ );
}
const currentPassword = account?.password;
if (!currentPassword) {
ctx.context.logger.error("Password not found", {
username,
});
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
+ );
}
const validPassword = await ctx.context.password.verify({
hash: currentPassword,
@@ -338,9 +343,10 @@ export const username = (options?: UsernameOptions | undefined) => {
});
if (!validPassword) {
ctx.context.logger.error("Invalid password");
- throw new APIError("UNAUTHORIZED", {
- message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
+ );
}
if (
@@ -350,9 +356,7 @@ export const username = (options?: UsernameOptions | undefined) => {
if (
!ctx.context.options?.emailVerification?.sendVerificationEmail
) {
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.EMAIL_NOT_VERIFIED,
- });
+ throw APIError.from("FORBIDDEN", ERROR_CODES.EMAIL_NOT_VERIFIED);
}
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
@@ -377,9 +381,7 @@ export const username = (options?: UsernameOptions | undefined) => {
);
}
- throw new APIError("FORBIDDEN", {
- message: ERROR_CODES.EMAIL_NOT_VERIFIED,
- });
+ throw APIError.from("FORBIDDEN", ERROR_CODES.EMAIL_NOT_VERIFIED);
}
const session = await ctx.context.internalAdapter.createSession(
@@ -390,7 +392,7 @@ export const username = (options?: UsernameOptions | undefined) => {
return ctx.json(null, {
status: 500,
body: {
- message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
+ message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION.message,
},
});
}
@@ -424,25 +426,27 @@ export const username = (options?: UsernameOptions | undefined) => {
async (ctx) => {
const username = ctx.body.username;
if (!username) {
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: ERROR_CODES.INVALID_USERNAME,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.INVALID_USERNAME,
+ );
}
const minUsernameLength = options?.minUsernameLength || 3;
const maxUsernameLength = options?.maxUsernameLength || 30;
if (username.length < minUsernameLength) {
- throw new APIError("UNPROCESSABLE_ENTITY", {
- code: "USERNAME_TOO_SHORT",
- message: ERROR_CODES.USERNAME_TOO_SHORT,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.USERNAME_TOO_SHORT,
+ );
}
if (username.length > maxUsernameLength) {
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: ERROR_CODES.USERNAME_TOO_LONG,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.USERNAME_TOO_LONG,
+ );
}
const validator =
@@ -450,9 +454,10 @@ export const username = (options?: UsernameOptions | undefined) => {
const valid = await validator(username);
if (!valid) {
- throw new APIError("UNPROCESSABLE_ENTITY", {
- message: ERROR_CODES.INVALID_USERNAME,
- });
+ throw APIError.from(
+ "UNPROCESSABLE_ENTITY",
+ ERROR_CODES.INVALID_USERNAME,
+ );
}
const user = await ctx.context.adapter.findOne({
model: "user",
@@ -501,16 +506,17 @@ export const username = (options?: UsernameOptions | undefined) => {
const minUsernameLength = options?.minUsernameLength || 3;
const maxUsernameLength = options?.maxUsernameLength || 30;
if (username.length < minUsernameLength) {
- throw new APIError("BAD_REQUEST", {
- code: "USERNAME_TOO_SHORT",
- message: ERROR_CODES.USERNAME_TOO_SHORT,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.USERNAME_TOO_SHORT,
+ );
}
if (username.length > maxUsernameLength) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.USERNAME_TOO_LONG,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.USERNAME_TOO_LONG,
+ );
}
const validator =
@@ -518,9 +524,10 @@ export const username = (options?: UsernameOptions | undefined) => {
const valid = await validator(username);
if (!valid) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_USERNAME,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.INVALID_USERNAME,
+ );
}
const user = await ctx.context.adapter.findOne({
model: "user",
@@ -539,9 +546,10 @@ export const username = (options?: UsernameOptions | undefined) => {
ctx.context.session &&
user.id !== ctx.context.session.session.userId;
if (blockChangeSignUp || blockChangeUpdateUser) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.USERNAME_IS_ALREADY_TAKEN,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.USERNAME_IS_ALREADY_TAKEN,
+ );
}
}
@@ -559,9 +567,10 @@ export const username = (options?: UsernameOptions | undefined) => {
const valid =
await options.displayUsernameValidator(displayUsername);
if (!valid) {
- throw new APIError("BAD_REQUEST", {
- message: ERROR_CODES.INVALID_DISPLAY_USERNAME,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ ERROR_CODES.INVALID_DISPLAY_USERNAME,
+ );
}
}
}
diff --git a/packages/better-auth/src/plugins/username/username.test.ts b/packages/better-auth/src/plugins/username/username.test.ts
index 3f66efa55e..874e925c52 100644
--- a/packages/better-auth/src/plugins/username/username.test.ts
+++ b/packages/better-auth/src/plugins/username/username.test.ts
@@ -1,6 +1,6 @@
import { describe, expect } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
-import { username } from ".";
+import { USERNAME_ERROR_CODES, username } from ".";
import { usernameClient } from "./client";
describe("username", async (it) => {
@@ -149,7 +149,7 @@ describe("username", async (it) => {
name: "new-name",
});
expect(res.error?.status).toBe(400);
- expect(res.error?.code).toBe("USERNAME_IS_INVALID");
+ expect(res.error?.code).toBe(USERNAME_ERROR_CODES.INVALID_USERNAME.code);
});
it("should fail on too short username", async () => {
@@ -199,7 +199,7 @@ describe("username", async (it) => {
username: "invalid username!",
});
expect(res.error?.status).toBe(422);
- expect(res.error?.code).toBe("USERNAME_IS_INVALID");
+ expect(res.error?.code).toBe(USERNAME_ERROR_CODES.INVALID_USERNAME.code);
});
it("should reject too short username in isUsernameAvailable", async () => {
@@ -216,7 +216,7 @@ describe("username", async (it) => {
username: longUsername,
});
expect(res.error?.status).toBe(422);
- expect(res.error?.code).toBe("USERNAME_IS_TOO_LONG");
+ expect(res.error?.code).toBe(USERNAME_ERROR_CODES.USERNAME_TOO_LONG.code);
});
it("should not normalize displayUsername", async () => {
@@ -397,7 +397,9 @@ describe("username with displayUsername validation", async (it) => {
name: "test-name",
});
expect(res.error?.status).toBe(400);
- expect(res.error?.code).toBe("DISPLAY_USERNAME_IS_INVALID");
+ expect(res.error?.code).toBe(
+ USERNAME_ERROR_CODES.INVALID_DISPLAY_USERNAME.code,
+ );
});
it("should update displayUsername with valid value", async () => {
@@ -463,7 +465,9 @@ describe("username with displayUsername validation", async (it) => {
});
expect(res.error?.status).toBe(400);
- expect(res.error?.code).toBe("DISPLAY_USERNAME_IS_INVALID");
+ expect(res.error?.code).toBe(
+ USERNAME_ERROR_CODES.INVALID_DISPLAY_USERNAME.code,
+ );
});
});
@@ -497,7 +501,7 @@ describe("isUsernameAvailable with custom validator", async (it) => {
username: "invalid_user",
});
expect(res.error?.status).toBe(422);
- expect(res.error?.code).toBe("USERNAME_IS_INVALID");
+ expect(res.error?.code).toBe(USERNAME_ERROR_CODES.INVALID_USERNAME.code);
});
it("should reject username that doesn't match custom validator during sign-up/sign-in", async () => {
@@ -509,7 +513,9 @@ describe("isUsernameAvailable with custom validator", async (it) => {
});
expect(signUpRes.error).toBeDefined();
- expect(signUpRes.error?.code).toBe("USERNAME_IS_INVALID");
+ expect(signUpRes.error?.code).toBe(
+ USERNAME_ERROR_CODES.INVALID_USERNAME.code,
+ );
const signInRes = await client.signIn.username({
username: "invalid_user",
@@ -517,7 +523,9 @@ describe("isUsernameAvailable with custom validator", async (it) => {
});
expect(signInRes.error).toBeDefined();
- expect(signInRes.error?.code).toBe("USERNAME_IS_INVALID");
+ expect(signInRes.error?.code).toBe(
+ USERNAME_ERROR_CODES.INVALID_USERNAME.code,
+ );
});
});
diff --git a/packages/better-auth/src/utils/is-api-error.ts b/packages/better-auth/src/utils/is-api-error.ts
new file mode 100644
index 0000000000..fd1c1037c0
--- /dev/null
+++ b/packages/better-auth/src/utils/is-api-error.ts
@@ -0,0 +1,10 @@
+import { APIError } from "@better-auth/core/error";
+import { APIError as BaseAPIError } from "better-call";
+
+export function isAPIError(error: unknown): error is APIError {
+ return (
+ error instanceof BaseAPIError ||
+ error instanceof APIError ||
+ (error as any)?.name === "APIError"
+ );
+}
diff --git a/packages/better-auth/src/utils/password.ts b/packages/better-auth/src/utils/password.ts
index 32b599231c..b93f8059bc 100644
--- a/packages/better-auth/src/utils/password.ts
+++ b/packages/better-auth/src/utils/password.ts
@@ -1,5 +1,5 @@
import type { GenericEndpointContext } from "@better-auth/core";
-import { APIError } from "better-call";
+import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
export async function validatePassword(
ctx: GenericEndpointContext,
@@ -30,18 +30,17 @@ export async function checkPassword(userId: string, c: GenericEndpointContext) {
);
const currentPassword = credentialAccount?.password;
if (!credentialAccount || !currentPassword || !c.body.password) {
- throw new APIError("BAD_REQUEST", {
- message: "No password credential found",
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
+ );
}
const compare = await c.context.password.verify({
hash: currentPassword,
password: c.body.password,
});
if (!compare) {
- throw new APIError("BAD_REQUEST", {
- message: "Invalid password",
- });
+ throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
}
return true;
}
diff --git a/packages/better-auth/src/utils/plugin-helper.ts b/packages/better-auth/src/utils/plugin-helper.ts
index 7daa2cb8ef..d5d2d58955 100644
--- a/packages/better-auth/src/utils/plugin-helper.ts
+++ b/packages/better-auth/src/utils/plugin-helper.ts
@@ -1,4 +1,4 @@
-import { APIError } from "better-call";
+import { isAPIError } from "./is-api-error";
export const getEndpointResponse = async (ctx: {
context: {
@@ -15,7 +15,7 @@ export const getEndpointResponse = async (ctx: {
}
return (await returned.clone().json()) as T;
}
- if (returned instanceof APIError) {
+ if (isAPIError(returned)) {
return null;
}
return returned as T;
diff --git a/packages/core/src/error/codes.ts b/packages/core/src/error/codes.ts
index 0f9e1043a3..6a8912a10f 100644
--- a/packages/core/src/error/codes.ts
+++ b/packages/core/src/error/codes.ts
@@ -9,9 +9,11 @@ export const BASE_ERROR_CODES = defineErrorCodes({
INVALID_PASSWORD: "Invalid password",
INVALID_EMAIL: "Invalid email",
INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
+ INVALID_USER: "Invalid user",
SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
PROVIDER_NOT_FOUND: "Provider not found",
INVALID_TOKEN: "Invalid token",
+ TOKEN_EXPIRED: "Token expired",
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
FAILED_TO_GET_USER_INFO: "Failed to get user info",
USER_EMAIL_NOT_FOUND: "User email not found",
@@ -28,4 +30,23 @@ export const BASE_ERROR_CODES = defineErrorCodes({
ACCOUNT_NOT_FOUND: "Account not found",
USER_ALREADY_HAS_PASSWORD:
"User already has a password. Provide that to delete the account.",
+ VERIFICATION_EMAIL_NOT_ENABLED: "Verification email isn't enabled",
+ EMAIL_ALREADY_VERIFIED: "Email is already verified",
+ EMAIL_MISMATCH: "Email mismatch",
+ SESSION_NOT_FRESH: "Session is not fresh",
+ LINKED_ACCOUNT_ALREADY_EXISTS: "Linked account already exists",
+ INVALID_ORIGIN: "Invalid origin",
+ INVALID_CALLBACK_URL: "Invalid callbackURL",
+ INVALID_REDIRECT_URL: "Invalid redirectURL",
+ INVALID_ERROR_CALLBACK_URL: "Invalid errorCallbackURL",
+ INVALID_NEW_USER_CALLBACK_URL: "Invalid newUserCallbackURL",
+ MISSING_OR_NULL_ORIGIN: "Missing or null Origin",
+ CALLBACK_URL_REQUIRED: "callbackURL is required",
+ FAILED_TO_CREATE_VERIFICATION: "Unable to create verification",
+ FIELD_NOT_ALLOWED: "Field not allowed to be set",
+ ASYNC_VALIDATION_NOT_SUPPORTED: "Async validation is not supported",
+ VALIDATION_ERROR: "Validation Error",
+ MISSING_FIELD: "Field is required",
});
+
+export type APIErrorCode = keyof typeof BASE_ERROR_CODES;
diff --git a/packages/core/src/error/index.ts b/packages/core/src/error/index.ts
index 2947d42e13..7182a39f12 100644
--- a/packages/core/src/error/index.ts
+++ b/packages/core/src/error/index.ts
@@ -1,3 +1,5 @@
+import { APIError as BaseAPIError } from "better-call/error";
+
export class BetterAuthError extends Error {
constructor(message: string, cause?: string | undefined) {
super(message);
@@ -8,4 +10,27 @@ export class BetterAuthError extends Error {
}
}
-export { BASE_ERROR_CODES } from "./codes";
+export { type APIErrorCode, BASE_ERROR_CODES } from "./codes";
+
+export class APIError extends BaseAPIError {
+ constructor(...args: ConstructorParameters) {
+ super(...args);
+ }
+
+ static fromStatus(
+ status: ConstructorParameters[0],
+ body?: ConstructorParameters[1],
+ ) {
+ return new APIError(status, body);
+ }
+
+ static from(
+ status: ConstructorParameters[0],
+ error: { code: string; message: string },
+ ) {
+ return new APIError(status, {
+ message: error.message,
+ code: error.code,
+ });
+ }
+}
diff --git a/packages/core/src/social-providers/apple.ts b/packages/core/src/social-providers/apple.ts
index c90d245ac9..5bfc285b2f 100644
--- a/packages/core/src/social-providers/apple.ts
+++ b/packages/core/src/social-providers/apple.ts
@@ -1,6 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
-import { APIError } from "better-call";
+
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
+import { APIError } from "../error";
import type { OAuthProvider, ProviderOptions } from "../oauth2";
import {
createAuthorizationURL,
diff --git a/packages/core/src/social-providers/cognito.ts b/packages/core/src/social-providers/cognito.ts
index 420cec7431..7a55054a7c 100644
--- a/packages/core/src/social-providers/cognito.ts
+++ b/packages/core/src/social-providers/cognito.ts
@@ -1,8 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
-import { APIError } from "better-call";
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
import { logger } from "../env";
-import { BetterAuthError } from "../error";
+import { APIError, BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "../oauth2";
import {
createAuthorizationURL,
diff --git a/packages/core/src/social-providers/google.ts b/packages/core/src/social-providers/google.ts
index 58e04b0dd3..6728ce41ab 100644
--- a/packages/core/src/social-providers/google.ts
+++ b/packages/core/src/social-providers/google.ts
@@ -1,8 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
-import { APIError } from "better-call";
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
import { logger } from "../env";
-import { BetterAuthError } from "../error";
+import { APIError, BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "../oauth2";
import {
createAuthorizationURL,
diff --git a/packages/core/src/types/plugin-client.ts b/packages/core/src/types/plugin-client.ts
index 93494587ab..18b0df9ea9 100644
--- a/packages/core/src/types/plugin-client.ts
+++ b/packages/core/src/types/plugin-client.ts
@@ -114,4 +114,14 @@ export interface BetterAuthClientPlugin {
* plugin or any plugin the user might have added.
*/
atomListeners?: ClientAtomListener[] | undefined;
+ /**
+ * The error codes returned by the plugin
+ */
+ $ERROR_CODES?: Record<
+ string,
+ {
+ code: string;
+ message: string;
+ }
+ >;
}
diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts
index aa300834fd..5e6ff01af2 100644
--- a/packages/core/src/types/plugin.ts
+++ b/packages/core/src/types/plugin.ts
@@ -145,7 +145,7 @@ export type BetterAuthPlugin = {
/**
* The error codes returned by the plugin
*/
- $ERROR_CODES?: Record | undefined;
+ $ERROR_CODES?: Record | undefined;
/**
* All database operations that are performed by the plugin
*
diff --git a/packages/core/src/utils/error-codes.ts b/packages/core/src/utils/error-codes.ts
index 795e8512da..0c321c519b 100644
--- a/packages/core/src/utils/error-codes.ts
+++ b/packages/core/src/utils/error-codes.ts
@@ -46,6 +46,20 @@ type ValidateErrorCodes = {
export function defineErrorCodes>(
codes: ValidateErrorCodes,
-): T {
- return codes as T;
+): {
+ [K in keyof T]: {
+ code: K;
+ message: T[K];
+ };
+} {
+ return Object.fromEntries(
+ Object.entries(codes).map(([key, value]) => [
+ key,
+ {
+ code: key,
+ message: value,
+ toString: () => value,
+ },
+ ]),
+ ) as any;
}
diff --git a/packages/passkey/src/client.ts b/packages/passkey/src/client.ts
index 39f5ff171f..74903f01ab 100644
--- a/packages/passkey/src/client.ts
+++ b/packages/passkey/src/client.ts
@@ -17,6 +17,7 @@ import { useAuthQuery } from "better-auth/client";
import type { Session, User } from "better-auth/types";
import { atom } from "nanostores";
import type { passkey } from ".";
+import { PASSKEY_ERROR_CODES } from "./error-codes";
import type { Passkey } from "./types";
export const getPasskeyActions = (
@@ -261,8 +262,10 @@ export const passkeyClient = () => {
signal: "$sessionSignal",
},
],
+ $ERROR_CODES: PASSKEY_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
export type * from "@simplewebauthn/server";
+export * from "./error-codes";
export type * from "./types";
diff --git a/packages/passkey/src/passkey.test.ts b/packages/passkey/src/passkey.test.ts
index de55662e1a..6fd463c21b 100644
--- a/packages/passkey/src/passkey.test.ts
+++ b/packages/passkey/src/passkey.test.ts
@@ -1,6 +1,6 @@
+import { APIError } from "@better-auth/core/error";
import { createAuthClient } from "better-auth/client";
import { getTestInstance } from "better-auth/test";
-import { APIError } from "better-call";
import { describe, expect, it } from "vitest";
import type { Passkey } from ".";
import { passkey } from ".";
diff --git a/packages/passkey/src/routes.ts b/packages/passkey/src/routes.ts
index b2e57eee9a..c094bd8643 100644
--- a/packages/passkey/src/routes.ts
+++ b/packages/passkey/src/routes.ts
@@ -1,4 +1,5 @@
import { createAuthEndpoint } from "@better-auth/core/api";
+import { APIError } from "@better-auth/core/error";
import { base64 } from "@better-auth/utils/base64";
import type {
AuthenticationResponseJSON,
@@ -18,7 +19,6 @@ import {
} from "better-auth/api";
import { setSessionCookie } from "better-auth/cookies";
import { generateRandomString } from "better-auth/crypto";
-import { APIError } from "better-call";
import * as z from "zod";
import { PASSKEY_ERROR_CODES } from "./error-codes";
import type { Passkey, PasskeyOptions, WebAuthnChallengeValue } from "./types";
@@ -456,9 +456,10 @@ export const verifyPasskeyRegistration = (options: RequiredPassKeyOptions) =>
ctx.context.secret,
);
if (!verificationToken) {
- throw new APIError("BAD_REQUEST", {
- message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
+ );
}
const data =
@@ -475,10 +476,10 @@ export const verifyPasskeyRegistration = (options: RequiredPassKeyOptions) =>
) as WebAuthnChallengeValue;
if (userData.id !== ctx.context.session.user.id) {
- throw new APIError("UNAUTHORIZED", {
- message:
- PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
+ );
}
try {
@@ -525,9 +526,10 @@ export const verifyPasskeyRegistration = (options: RequiredPassKeyOptions) =>
});
} catch (e) {
ctx.context.logger.error("Failed to verify registration", e);
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
+ );
}
},
);
@@ -590,9 +592,10 @@ export const verifyPasskeyAuthentication = (options: RequiredPassKeyOptions) =>
ctx.context.secret,
);
if (!verificationToken) {
- throw new APIError("BAD_REQUEST", {
- message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
+ );
}
const data =
@@ -600,9 +603,10 @@ export const verifyPasskeyAuthentication = (options: RequiredPassKeyOptions) =>
verificationToken,
);
if (!data) {
- throw new APIError("BAD_REQUEST", {
- message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
+ );
}
const { expectedChallenge } = JSON.parse(
data.value,
@@ -617,9 +621,10 @@ export const verifyPasskeyAuthentication = (options: RequiredPassKeyOptions) =>
],
});
if (!passkey) {
- throw new APIError("UNAUTHORIZED", {
- message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
+ );
}
try {
const verification = await verifyAuthenticationResponse({
@@ -639,9 +644,10 @@ export const verifyPasskeyAuthentication = (options: RequiredPassKeyOptions) =>
});
const { verified } = verification;
if (!verified)
- throw new APIError("UNAUTHORIZED", {
- message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED,
+ );
await ctx.context.adapter.update({
model: "passkey",
@@ -659,9 +665,10 @@ export const verifyPasskeyAuthentication = (options: RequiredPassKeyOptions) =>
passkey.userId,
);
if (!s) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: PASSKEY_ERROR_CODES.UNABLE_TO_CREATE_SESSION,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ PASSKEY_ERROR_CODES.UNABLE_TO_CREATE_SESSION,
+ );
}
const user = await ctx.context.internalAdapter.findUserById(
passkey.userId,
@@ -689,9 +696,10 @@ export const verifyPasskeyAuthentication = (options: RequiredPassKeyOptions) =>
);
} catch (e) {
ctx.context.logger.error("Failed to verify authentication", e);
- throw new APIError("BAD_REQUEST", {
- message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED,
+ );
}
},
);
@@ -821,9 +829,7 @@ export const deletePasskey = createAuthEndpoint(
],
});
if (!passkey) {
- throw new APIError("NOT_FOUND", {
- message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND);
}
if (passkey.userId !== ctx.context.session.user.id) {
throw new APIError("UNAUTHORIZED");
@@ -904,16 +910,14 @@ export const updatePasskey = createAuthEndpoint(
});
if (!passkey) {
- throw new APIError("NOT_FOUND", {
- message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
- });
+ throw APIError.from("NOT_FOUND", PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND);
}
if (passkey.userId !== ctx.context.session.user.id) {
- throw new APIError("UNAUTHORIZED", {
- message:
- PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
- });
+ throw APIError.from(
+ "UNAUTHORIZED",
+ PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
+ );
}
const updatedPasskey = await ctx.context.adapter.update({
@@ -930,9 +934,10 @@ export const updatePasskey = createAuthEndpoint(
});
if (!updatedPasskey) {
- throw new APIError("INTERNAL_SERVER_ERROR", {
- message: PASSKEY_ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
- });
+ throw APIError.from(
+ "INTERNAL_SERVER_ERROR",
+ PASSKEY_ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
+ );
}
return ctx.json(
{
diff --git a/packages/stripe/src/client.ts b/packages/stripe/src/client.ts
index ae8f37281d..3415ede4a3 100644
--- a/packages/stripe/src/client.ts
+++ b/packages/stripe/src/client.ts
@@ -1,4 +1,5 @@
import type { BetterAuthClientPlugin } from "better-auth";
+import { STRIPE_ERROR_CODES } from "./error-codes";
import type { stripe } from "./index";
export const stripeClient = <
@@ -28,8 +29,9 @@ export const stripeClient = <
>
>,
pathMethods: {
- "/subscription/restore": "POST",
"/subscription/billing-portal": "POST",
},
+ $ERROR_CODES: STRIPE_ERROR_CODES,
} satisfies BetterAuthClientPlugin;
};
+export * from "./error-codes";
diff --git a/packages/stripe/src/error-codes.ts b/packages/stripe/src/error-codes.ts
new file mode 100644
index 0000000000..70cc6d5484
--- /dev/null
+++ b/packages/stripe/src/error-codes.ts
@@ -0,0 +1,14 @@
+import { defineErrorCodes } from "@better-auth/core/utils";
+
+export const STRIPE_ERROR_CODES = defineErrorCodes({
+ SUBSCRIPTION_NOT_FOUND: "Subscription not found",
+ SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
+ ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
+ UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
+ FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
+ EMAIL_VERIFICATION_REQUIRED:
+ "Email verification is required before you can subscribe to a plan",
+ SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
+ SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:
+ "Subscription is not scheduled for cancellation",
+});
diff --git a/packages/stripe/src/routes.ts b/packages/stripe/src/routes.ts
index f2b3837138..edee45b59a 100644
--- a/packages/stripe/src/routes.ts
+++ b/packages/stripe/src/routes.ts
@@ -1,9 +1,8 @@
import { createAuthEndpoint } from "@better-auth/core/api";
-import { defineErrorCodes } from "@better-auth/core/utils";
+import { APIError } from "@better-auth/core/error";
import type { GenericEndpointContext } from "better-auth";
import { HIDE_METADATA } from "better-auth";
import {
- APIError,
getSessionFromCtx,
originCheck,
sessionMiddleware,
@@ -11,6 +10,7 @@ import {
import type Stripe from "stripe";
import type { Stripe as StripeType } from "stripe";
import * as z from "zod/v4";
+import { STRIPE_ERROR_CODES } from "./error-codes";
import {
onCheckoutSessionCompleted,
onSubscriptionDeleted,
@@ -25,19 +25,6 @@ import type {
} from "./types";
import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
-const STRIPE_ERROR_CODES = defineErrorCodes({
- SUBSCRIPTION_NOT_FOUND: "Subscription not found",
- SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
- ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
- UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
- FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
- EMAIL_VERIFICATION_REQUIRED:
- "Email verification is required before you can subscribe to a plan",
- SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
- SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:
- "Subscription is not scheduled for cancellation",
-});
-
const upgradeSubscriptionBodySchema = z.object({
/**
* The name of the plan to subscribe
@@ -171,16 +158,18 @@ export const upgradeSubscription = (options: StripeOptions) => {
async (ctx) => {
const { user, session } = ctx.context.session;
if (!user.emailVerified && subscriptionOptions.requireEmailVerification) {
- throw new APIError("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED,
+ );
}
const referenceId = ctx.body.referenceId || user.id;
const plan = await getPlanByName(options, ctx.body.plan);
if (!plan) {
- throw new APIError("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
+ );
}
let subscriptionToUpdate = ctx.body.subscriptionId
? await ctx.context.adapter.findOne({
@@ -208,9 +197,10 @@ export const upgradeSubscription = (options: StripeOptions) => {
}
if (ctx.body.subscriptionId && !subscriptionToUpdate) {
- throw new APIError("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ );
}
let customerId =
@@ -254,9 +244,10 @@ export const upgradeSubscription = (options: StripeOptions) => {
customerId = stripeCustomer.id;
} catch (e: any) {
ctx.context.logger.error(e);
- throw new APIError("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
+ );
}
}
@@ -315,9 +306,10 @@ export const upgradeSubscription = (options: StripeOptions) => {
activeOrTrialingSubscription.plan === ctx.body.plan &&
activeOrTrialingSubscription.seats === (ctx.body.seats || 1)
) {
- throw new APIError("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
+ );
}
if (activeSubscription && customerId) {
@@ -732,9 +724,10 @@ export const cancelSubscription = (options: StripeOptions) => {
}
if (!subscription || !subscription.stripeCustomerId) {
- throw ctx.error("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ );
}
const activeSubscriptions = await client.subscriptions
.list({
@@ -759,17 +752,19 @@ export const cancelSubscription = (options: StripeOptions) => {
},
],
});
- throw ctx.error("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ );
}
const activeSubscription = activeSubscriptions.find(
(sub) => sub.id === subscription.stripeSubscriptionId,
);
if (!activeSubscription) {
- throw ctx.error("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ );
}
const { url } = await client.billingPortal.sessions
.create({
@@ -893,23 +888,25 @@ export const restoreSubscription = (options: StripeOptions) => {
subscription = undefined;
}
if (!subscription || !subscription.stripeCustomerId) {
- throw ctx.error("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ );
}
if (
subscription.status != "active" &&
subscription.status != "trialing"
) {
- throw ctx.error("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
+ );
}
if (!subscription.cancelAtPeriodEnd) {
- throw ctx.error("BAD_REQUEST", {
- message:
- STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
+ );
}
const activeSubscription = await client.subscriptions
@@ -923,9 +920,10 @@ export const restoreSubscription = (options: StripeOptions) => {
)[0],
);
if (!activeSubscription) {
- throw ctx.error("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ );
}
try {
@@ -953,9 +951,10 @@ export const restoreSubscription = (options: StripeOptions) => {
return ctx.json(newSub);
} catch (error) {
ctx.context.logger.error("Error restoring subscription", error);
- throw new APIError("BAD_REQUEST", {
- message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
- });
+ throw APIError.from(
+ "BAD_REQUEST",
+ STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
+ );
}
},
);