feat: error codes (#771)

This commit is contained in:
Bereket Engida
2024-12-08 10:54:58 +03:00
committed by GitHub
parent e7b9f8b886
commit 95fd568231
38 changed files with 443 additions and 211 deletions
@@ -4,6 +4,7 @@ import { socialProviderList } from "../../social-providers";
import { APIError } from "better-call";
import { generateState } from "../../oauth2";
import { sessionMiddleware } from "./session";
import { BASE_ERROR_CODES } from "../../error/codes";
export const listUserAccounts = createAuthEndpoint(
"/list-accounts",
@@ -123,7 +124,7 @@ export const linkSocialAccount = createAuthEndpoint(
);
if (existingAccount) {
throw new APIError("BAD_REQUEST", {
message: "Social Account is already linked.",
message: BASE_ERROR_CODES.SOCIAL_ACCOUNT_ALREADY_LINKED,
});
}
const provider = c.context.socialProviders.find(
@@ -137,7 +138,7 @@ export const linkSocialAccount = createAuthEndpoint(
},
);
throw new APIError("NOT_FOUND", {
message: "Provider not found",
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
});
}
const state = await generateState(c, {
@@ -6,6 +6,7 @@ import { APIError } from "better-call";
import { getSessionFromCtx } from "./session";
import { setSessionCookie } from "../../cookies";
import type { GenericEndpointContext, User } from "../../types";
import { BASE_ERROR_CODES } from "../../error/codes";
export async function createEmailVerificationToken(
secret: string,
@@ -143,7 +144,7 @@ export const sendVerificationEmail = createAuthEndpoint(
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
throw new APIError("BAD_REQUEST", {
message: "User not found",
message: BASE_ERROR_CODES.USER_NOT_FOUND,
});
}
await sendVerificationEmailFn(ctx, user.user);
@@ -4,6 +4,7 @@ import { APIError } from "better-call";
import type { AuthContext } from "../../init";
import { getDate } from "../../utils/date";
import { generateId } from "../../utils";
import { BASE_ERROR_CODES } from "../../error/codes";
function redirectError(
ctx: AuthContext,
@@ -241,7 +242,7 @@ export const resetPassword = createAuthEndpoint(
: "");
if (!token) {
throw new APIError("BAD_REQUEST", {
message: "Token not found",
message: BASE_ERROR_CODES.INVALID_TOKEN,
});
}
@@ -252,7 +253,7 @@ export const resetPassword = createAuthEndpoint(
await ctx.context.internalAdapter.findVerificationValue(id);
if (!verification || verification.expiresAt < new Date()) {
throw new APIError("BAD_REQUEST", {
message: "Invalid token",
message: BASE_ERROR_CODES.INVALID_TOKEN,
});
}
await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
@@ -14,6 +14,7 @@ import type {
} from "../../types";
import { hmac } from "../../crypto/hash";
import { safeJSONParse } from "../../utils/json";
import { BASE_ERROR_CODES } from "../../error/codes";
export const getSession = <Option extends BetterAuthOptions>() =>
createAuthEndpoint(
@@ -238,7 +239,7 @@ export const getSession = <Option extends BetterAuthOptions>() =>
} catch (error) {
ctx.context.logger.error("INTERNAL_SERVER_ERROR", error);
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "internal server error",
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
});
}
},
+14 -14
View File
@@ -6,6 +6,7 @@ import { socialProviderList } from "../../social-providers";
import { createEmailVerificationToken } from "./email-verification";
import { generateState } from "../../utils";
import { handleOAuthUserInfo } from "../../oauth2/link-account";
import { BASE_ERROR_CODES } from "../../error/codes";
export const signInSocial = createAuthEndpoint(
"/sign-in/social",
@@ -163,7 +164,7 @@ export const signInSocial = createAuthEndpoint(
},
);
throw new APIError("NOT_FOUND", {
message: "Provider not found",
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
});
}
@@ -176,7 +177,7 @@ export const signInSocial = createAuthEndpoint(
},
);
throw new APIError("NOT_FOUND", {
message: "Provider does not support id token verification",
message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
});
}
const { token, nonce } = c.body.idToken;
@@ -186,7 +187,7 @@ export const signInSocial = createAuthEndpoint(
provider: c.body.provider,
});
throw new APIError("UNAUTHORIZED", {
message: "Invalid id token",
message: BASE_ERROR_CODES.INVALID_TOKEN,
});
}
const userInfo = await provider.getUserInfo({
@@ -199,7 +200,7 @@ export const signInSocial = createAuthEndpoint(
provider: c.body.provider,
});
throw new APIError("UNAUTHORIZED", {
message: "Failed to get user info",
message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
});
}
if (!userInfo.user.email) {
@@ -207,7 +208,7 @@ export const signInSocial = createAuthEndpoint(
provider: c.body.provider,
});
throw new APIError("UNAUTHORIZED", {
message: "User email not found",
message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
});
}
const data = await handleOAuthUserInfo(c, {
@@ -337,7 +338,7 @@ export const signInEmail = createAuthEndpoint(
const isValidEmail = z.string().email().safeParse(email);
if (!isValidEmail.success) {
throw new APIError("BAD_REQUEST", {
message: "Invalid email",
message: BASE_ERROR_CODES.INVALID_EMAIL,
});
}
const user = await ctx.context.internalAdapter.findUserByEmail(email, {
@@ -348,7 +349,7 @@ export const signInEmail = createAuthEndpoint(
await ctx.context.password.hash(password);
ctx.context.logger.error("User not found", { email });
throw new APIError("UNAUTHORIZED", {
message: "Invalid email or password",
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
});
}
@@ -358,14 +359,14 @@ export const signInEmail = createAuthEndpoint(
if (!credentialAccount) {
ctx.context.logger.error("Credential account not found", { email });
throw new APIError("UNAUTHORIZED", {
message: "Invalid email or password",
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
});
}
const currentPassword = credentialAccount?.password;
if (!currentPassword) {
ctx.context.logger.error("Password not found", { email });
throw new APIError("UNAUTHORIZED", {
message: "Unexpected error",
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
});
}
const validPassword = await ctx.context.password.verify({
@@ -375,7 +376,7 @@ export const signInEmail = createAuthEndpoint(
if (!validPassword) {
ctx.context.logger.error("Invalid password");
throw new APIError("UNAUTHORIZED", {
message: "Invalid email or password",
message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD,
});
}
@@ -385,7 +386,7 @@ export const signInEmail = createAuthEndpoint(
) {
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) {
throw new APIError("UNAUTHORIZED", {
message: "Email is not verified.",
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
});
}
const token = await createEmailVerificationToken(
@@ -405,8 +406,7 @@ export const signInEmail = createAuthEndpoint(
);
ctx.context.logger.error("Email not verified", { email });
throw new APIError("FORBIDDEN", {
message:
"Email is not verified. Check your email for a verification link",
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
});
}
@@ -419,7 +419,7 @@ export const signInEmail = createAuthEndpoint(
if (!session) {
ctx.context.logger.error("Failed to create session");
throw new APIError("UNAUTHORIZED", {
message: "Failed to create session",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
});
}
@@ -2,6 +2,7 @@ import { z } from "zod";
import { createAuthEndpoint } from "../call";
import { deleteSessionCookie } from "../../cookies";
import { APIError } from "better-call";
import { BASE_ERROR_CODES } from "../../error/codes";
export const signOut = createAuthEndpoint(
"/sign-out",
@@ -39,7 +40,7 @@ export const signOut = createAuthEndpoint(
if (!sessionCookieToken) {
deleteSessionCookie(ctx);
throw new APIError("BAD_REQUEST", {
message: "Session not found",
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
});
}
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
+26 -22
View File
@@ -12,6 +12,7 @@ import type {
} from "../../types";
import type { toZod } from "../../types/to-zod";
import { parseUserInput } from "../../db/schema";
import { BASE_ERROR_CODES } from "../../error/codes";
export const signUpEmail = <O extends BetterAuthOptions>() =>
createAuthEndpoint(
@@ -102,13 +103,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
if (!isValidEmail.success) {
throw new APIError("BAD_REQUEST", {
message: "Invalid email",
});
}
if (typeof password !== "string") {
throw new APIError("BAD_REQUEST", {
message: "Password must be a string",
message: BASE_ERROR_CODES.INVALID_EMAIL,
});
}
@@ -116,7 +111,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
if (password.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
throw new APIError("BAD_REQUEST", {
message: "Password is too short",
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
});
}
@@ -124,14 +119,14 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
if (password.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
throw new APIError("BAD_REQUEST", {
message: "Password is too long",
message: 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: "User with this email already exists",
message: BASE_ERROR_CODES.USER_ALREADY_EXISTS,
});
}
@@ -139,21 +134,30 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
ctx.context.options,
additionalFields as any,
);
const createdUser = await ctx.context.internalAdapter.createUser({
email: email.toLowerCase(),
name,
image,
...additionalData,
emailVerified: false,
});
if (!createdUser) {
throw new APIError("BAD_REQUEST", {
message: "Failed to create user",
let createdUser: User;
try {
createdUser = await ctx.context.internalAdapter.createUser({
email: email.toLowerCase(),
name,
image,
...additionalData,
emailVerified: false,
});
if (!createdUser) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
});
}
} catch (e) {
ctx.context.logger.error("Failed to create user", e);
throw new APIError("UNPROCESSABLE_ENTITY", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
details: e,
});
}
if (!createdUser) {
throw new APIError("UNPROCESSABLE_ENTITY", {
message: "Failed to create user",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
});
}
/**
@@ -202,7 +206,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
);
if (!session) {
throw new APIError("BAD_REQUEST", {
message: "Failed to create session",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
});
}
await setSessionCookie(ctx, {
@@ -13,6 +13,7 @@ import type { toZod } from "../../types/to-zod";
import type { AdditionalUserFieldsInput, BetterAuthOptions } from "../../types";
import { parseUserInput } from "../../db/schema";
import { alphabet, generateRandomString } from "../../crypto";
import { BASE_ERROR_CODES } from "../../error/codes";
export const updateUser = <O extends BetterAuthOptions>() =>
createAuthEndpoint(
@@ -78,7 +79,7 @@ export const updateUser = <O extends BetterAuthOptions>() =>
if (body.email) {
throw new APIError("BAD_REQUEST", {
message: "You can't update email",
message: BASE_ERROR_CODES.EMAIL_CAN_NOT_BE_UPDATED,
});
}
const { name, image, ...rest } = body;
@@ -173,7 +174,7 @@ export const changePassword = createAuthEndpoint(
if (newPassword.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
throw new APIError("BAD_REQUEST", {
message: "Password is too short",
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
});
}
@@ -182,7 +183,7 @@ export const changePassword = createAuthEndpoint(
if (newPassword.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
throw new APIError("BAD_REQUEST", {
message: "Password too long",
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
});
}
@@ -194,7 +195,7 @@ export const changePassword = createAuthEndpoint(
);
if (!account || !account.password) {
throw new APIError("BAD_REQUEST", {
message: "User does not have a password",
message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND,
});
}
const passwordHash = await ctx.context.password.hash(newPassword);
@@ -204,7 +205,7 @@ export const changePassword = createAuthEndpoint(
});
if (!verify) {
throw new APIError("BAD_REQUEST", {
message: "Incorrect password",
message: BASE_ERROR_CODES.INVALID_PASSWORD,
});
}
await ctx.context.internalAdapter.updateAccount(account.id, {
@@ -218,7 +219,7 @@ export const changePassword = createAuthEndpoint(
);
if (!newSession) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Unable to create session",
message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION,
});
}
// set the new session cookie
@@ -254,7 +255,7 @@ export const setPassword = createAuthEndpoint(
if (newPassword.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
throw new APIError("BAD_REQUEST", {
message: "Password is too short",
message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
});
}
@@ -263,7 +264,7 @@ export const setPassword = createAuthEndpoint(
if (newPassword.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
throw new APIError("BAD_REQUEST", {
message: "Password too long",
message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
});
}
@@ -382,7 +383,7 @@ export const deleteUserCallback = createAuthEndpoint(
const session = await getSessionFromCtx(ctx);
if (!session) {
throw new APIError("NOT_FOUND", {
message: "No session found",
message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
});
}
const token = await ctx.context.internalAdapter.findVerificationValue(
@@ -393,12 +394,12 @@ export const deleteUserCallback = createAuthEndpoint(
await ctx.context.internalAdapter.deleteVerificationValue(token.id);
}
throw new APIError("NOT_FOUND", {
message: "Invalid token",
message: BASE_ERROR_CODES.INVALID_TOKEN,
});
}
if (token.value !== session.user.id) {
throw new APIError("NOT_FOUND", {
message: "Invalid token",
message: BASE_ERROR_CODES.INVALID_TOKEN,
});
}
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
+16 -1
View File
@@ -2,6 +2,7 @@ import { getEndpoints, router } from "./api";
import { init } from "./init";
import type { BetterAuthOptions } from "./types/options";
import type {
InferPluginErrorCodes,
InferPluginTypes,
InferSession,
InferUser,
@@ -9,11 +10,20 @@ import type {
} from "./types";
import { getBaseURL } from "./utils/url";
import type { FilterActions, InferAPI } from "./types/api";
import { BASE_ERROR_CODES } from "./error/codes";
export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
const authContext = init(options);
const { api } = getEndpoints(authContext, options);
const errorCodes = options.plugins?.reduce((acc, plugin) => {
if (plugin.$ERROR_CODES) {
return {
...acc,
...plugin.$ERROR_CODES,
};
}
return acc;
}, {});
return {
handler: async (request: Request) => {
const ctx = await authContext;
@@ -42,6 +52,10 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
user: PrettifyDeep<InferUser<O>>;
};
} & InferPluginTypes<O>,
$ErrorCodes: {
...errorCodes,
...BASE_ERROR_CODES,
} as InferPluginErrorCodes<O> & typeof BASE_ERROR_CODES,
};
};
@@ -49,4 +63,5 @@ export type Auth = {
handler: (request: Request) => Promise<Response>;
api: FilterActions<ReturnType<typeof router>["endpoints"]>;
options: BetterAuthOptions;
$ERROR_CODES: typeof BASE_ERROR_CODES;
};
@@ -4,17 +4,19 @@ import type {
ClientOptions,
InferActions,
InferClientAPI,
InferErrorCodes,
InferSessionFromClient,
InferUserFromClient,
IsSignal,
} from "../types";
import { createDynamicPathProxy } from "../proxy";
import type { UnionToIntersection } from "../../types/helper";
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
import { useStore } from "./react-store";
import type { BASE_ERROR_CODES } from "../../error/codes";
function getAtomKey(str: string) {
return `use${capitalizeFirstLetter(str)}`;
@@ -92,6 +94,9 @@ export function createAuthClient<Option extends ClientOptions>(
};
$fetch: typeof $fetch;
$store: typeof $store;
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
}
@@ -6,17 +6,17 @@ import type {
ClientOptions,
InferActions,
InferClientAPI,
InferSessionFromClient,
InferUserFromClient,
InferErrorCodes,
IsSignal,
} from "../types";
import type { Accessor } from "solid-js";
import type { UnionToIntersection } from "../../types/helper";
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
import { useStore } from "./solid-store";
import type { BASE_ERROR_CODES } from "../../error/codes";
function getAtomKey(str: string) {
return `use${capitalizeFirstLetter(str)}`;
@@ -84,5 +84,8 @@ export function createAuthClient<Option extends ClientOptions>(
Session: NonNullable<Session>;
};
$fetch: typeof $fetch;
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
}
@@ -5,17 +5,19 @@ import type {
ClientOptions,
InferActions,
InferClientAPI,
InferErrorCodes,
InferSessionFromClient,
InferUserFromClient,
IsSignal,
} from "../types";
import { createDynamicPathProxy } from "../proxy";
import type { UnionToIntersection } from "../../types/helper";
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
import type { Atom } from "nanostores";
import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
import type { BASE_ERROR_CODES } from "../../error/codes";
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
infer Plugin
@@ -83,5 +85,8 @@ export function createAuthClient<Option extends ClientOptions>(
$Infer: {
Session: NonNullable<Session>;
};
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
}
+11
View File
@@ -97,6 +97,17 @@ export type InferActions<O extends ClientOptions> = O["plugins"] extends Array<
: {}
>
: {};
export type InferErrorCodes<O extends ClientOptions> =
O["plugins"] extends Array<infer Plugin>
? UnionToIntersection<
Plugin extends BetterAuthClientPlugin
? Plugin["$InferServerPlugin"] extends BetterAuthPlugin
? Plugin["$InferServerPlugin"]["$ERROR_CODES"]
: {}
: {}
>
: {};
/**
* signals are just used to recall a computed value.
* as a convention they start with "$"
+6 -3
View File
@@ -5,17 +5,17 @@ import type {
ClientOptions,
InferActions,
InferClientAPI,
InferSessionFromClient,
InferUserFromClient,
InferErrorCodes,
IsSignal,
} from "./types";
import { createDynamicPathProxy } from "./proxy";
import type { UnionToIntersection } from "../types/helper";
import type { PrettifyDeep, UnionToIntersection } from "../types/helper";
import type { Atom } from "nanostores";
import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
import type { BASE_ERROR_CODES } from "../error/codes";
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
infer Plugin
@@ -82,5 +82,8 @@ export function createAuthClient<Option extends ClientOptions>(
$Infer: {
Session: NonNullable<Session>;
};
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
}
+6 -1
View File
@@ -7,14 +7,16 @@ import type {
ClientOptions,
InferActions,
InferClientAPI,
InferErrorCodes,
IsSignal,
} from "../types";
import { createDynamicPathProxy } from "../proxy";
import type { UnionToIntersection } from "../../types/helper";
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
import type { BASE_ERROR_CODES } from "../../error/codes";
function getAtomKey(str: string) {
return `use${capitalizeFirstLetter(str)}`;
@@ -125,5 +127,8 @@ export function createAuthClient<Option extends ClientOptions>(
};
$fetch: typeof $fetch;
$store: typeof $store;
$ERROR_CODES: PrettifyDeep<
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
>;
};
}
+22
View File
@@ -0,0 +1,22 @@
export const BASE_ERROR_CODES = {
USER_NOT_FOUND: "User not found",
FAILED_TO_CREATE_USER: "Failed to create user",
FAILED_TO_CREATE_SESSION: "Failed to create session",
FAILED_TO_UPDATE_USER: "Failed to update user",
FAILED_TO_GET_SESSION: "Failed to get session",
INVALID_PASSWORD: "Invalid password",
INVALID_EMAIL: "Invalid email",
INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
PROVIDER_NOT_FOUND: "Provider not found",
INVALID_TOKEN: "invalid token",
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",
EMAIL_NOT_VERIFIED: "Email not verified",
PASSWORD_TOO_SHORT: "Password too short",
PASSWORD_TOO_LONG: "Password too long",
USER_ALREADY_EXISTS: "User already exists",
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
};
@@ -4,36 +4,35 @@ import { admin, type UserWithRole } from ".";
import { adminClient } from "./client";
describe("Admin plugin", async () => {
const { client, auth, signInWithTestUser, signInWithUser } =
await getTestInstance(
{
plugins: [admin()],
databaseHooks: {
user: {
create: {
before: async (user) => {
if (user.name === "Admin") {
return {
data: {
...user,
role: "admin",
},
};
}
},
const { client, signInWithTestUser, signInWithUser } = await getTestInstance(
{
plugins: [admin()],
databaseHooks: {
user: {
create: {
before: async (user) => {
if (user.name === "Admin") {
return {
data: {
...user,
role: "admin",
},
};
}
},
},
},
},
{
testUser: {
name: "Admin",
},
clientOptions: {
plugins: [adminClient()],
},
},
{
testUser: {
name: "Admin",
},
);
clientOptions: {
plugins: [adminClient()],
},
},
);
const { headers: adminHeaders } = await signInWithTestUser();
let newUser: UserWithRole | undefined;
@@ -74,6 +74,14 @@ export const admin = <O extends AdminOptions>(options?: O) => {
adminRole: "admin",
...options,
};
const ERROR_CODES = {
FAILED_TO_CREATE_USER: "Failed to create user",
USER_ALREADY_EXISTS: "User already exists",
USER_NOT_FOUND: "User not found",
YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself",
ONLY_ADMINS_CAN_ACCESS_THIS_ENDPOINT:
"Only admins can access this endpoint",
} as const;
const adminMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) {
@@ -278,7 +286,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
);
if (existUser) {
throw new APIError("BAD_REQUEST", {
message: "User already exists",
message: ERROR_CODES.USER_ALREADY_EXISTS,
});
}
const user =
@@ -291,7 +299,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
if (!user) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to create user",
message: ERROR_CODES.FAILED_TO_CREATE_USER,
});
}
const hashedPassword = await ctx.context.password.hash(
@@ -589,7 +597,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
async (ctx) => {
if (ctx.body.userId === ctx.context.session.user.id) {
throw new APIError("BAD_REQUEST", {
message: "You cannot ban yourself",
message: ERROR_CODES.YOU_CANNOT_BAN_YOURSELF,
});
}
const user = await ctx.context.internalAdapter.updateUser(
@@ -674,7 +682,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
);
if (!session) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to create session",
message: ERROR_CODES.FAILED_TO_CREATE_USER,
});
}
await setSessionCookie(
@@ -821,6 +829,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
},
),
},
$ERROR_CODES: ERROR_CODES,
schema: mergeSchema(schema, opts.schema),
} satisfies BetterAuthPlugin;
};
@@ -6,22 +6,23 @@ import { anonymousClient } from "./client";
describe("anonymous", async () => {
const linkAccountFn = vi.fn();
const { customFetchImpl, sessionSetter, testUser } = await getTestInstance({
plugins: [
anonymous({
async onLinkAccount(data) {
linkAccountFn(data);
},
schema: {
user: {
fields: {
isAnonymous: "is_anon",
const { customFetchImpl, auth, sessionSetter, testUser } =
await getTestInstance({
plugins: [
anonymous({
async onLinkAccount(data) {
linkAccountFn(data);
},
schema: {
user: {
fields: {
isAnonymous: "is_anon",
},
},
},
},
}),
],
});
}),
],
});
const headers = new Headers();
const client = createAuthClient({
plugins: [anonymousClient()],
@@ -12,7 +12,6 @@ import type {
User,
} from "../../types";
import { parseSetCookieHeader, setSessionCookie } from "../../cookies";
import { z } from "zod";
import { getOrigin } from "../../utils/url";
import { mergeSchema } from "../../db/schema";
@@ -62,6 +61,12 @@ const schema = {
} satisfies PluginSchema;
export const anonymous = (options?: AnonymousOptions) => {
const ERROR_CODES = {
FAILED_TO_CREATE_USER: "Failed to create user",
COULD_NOT_CREATE_SESSION: "Could not create session",
ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY:
"Anonymous users cannot sign in again anonymously",
} as const;
return {
id: "anonymous",
endpoints: {
@@ -113,7 +118,7 @@ export const anonymous = (options?: AnonymousOptions) => {
return ctx.json(null, {
status: 500,
body: {
message: "Failed to create user",
message: ERROR_CODES.FAILED_TO_CREATE_USER,
status: 500,
},
});
@@ -126,7 +131,7 @@ export const anonymous = (options?: AnonymousOptions) => {
return ctx.json(null, {
status: 400,
body: {
message: "Could not create session",
message: ERROR_CODES.COULD_NOT_CREATE_SESSION,
},
});
}
@@ -175,7 +180,8 @@ export const anonymous = (options?: AnonymousOptions) => {
}
if (ctx.path === "/sign-in/anonymous") {
throw new APIError("BAD_REQUEST", {
message: "Anonymous users cannot sign in again anonymously",
message:
ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
});
}
@@ -198,5 +204,6 @@ export const anonymous = (options?: AnonymousOptions) => {
],
},
schema: mergeSchema(schema, options?.schema),
$ERROR_CODES: ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -1,4 +1,4 @@
import { z } from "zod";
import { INVALID, z } from "zod";
import { APIError, createAuthEndpoint } from "../../api";
import type { BetterAuthPlugin, User } from "../../types";
import { alphabet, generateRandomString } from "../../crypto";
@@ -49,6 +49,12 @@ export const emailOTP = (options: EmailOTPOptions) => {
otpLength: 6,
...options,
};
const ERROR_CODES = {
OTP_EXPIRED: "otp expired",
INVALID_OTP: "invalid otp",
INVALID_EMAIL: "invalid email",
USER_NOT_FOUND: "user not found",
} as const;
return {
id: "email-otp",
endpoints: {
@@ -100,7 +106,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) {
throw new APIError("BAD_REQUEST", {
message: "Invalid email",
message: ERROR_CODES.INVALID_EMAIL,
});
}
const otp = generateRandomString(opts.otpLength, alphabet("0-9"));
@@ -522,7 +528,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
throw new APIError("BAD_REQUEST", {
message: "User not found",
message: ERROR_CODES.USER_NOT_FOUND,
});
}
const verificationValue =
@@ -536,13 +542,13 @@ export const emailOTP = (options: EmailOTPOptions) => {
);
}
throw new APIError("BAD_REQUEST", {
message: "Invalid OTP",
message: ERROR_CODES.OTP_EXPIRED,
});
}
const otp = ctx.body.otp;
if (verificationValue.value !== otp) {
throw new APIError("BAD_REQUEST", {
message: "Invalid OTP",
message: ERROR_CODES.INVALID_OTP,
});
}
await ctx.context.internalAdapter.deleteVerificationValue(
@@ -597,5 +603,6 @@ export const emailOTP = (options: EmailOTPOptions) => {
},
],
},
$ERROR_CODES: ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -166,6 +166,9 @@ async function getUserInfo(
* A generic OAuth plugin that can be used to add OAuth support to any provider
*/
export const genericOAuth = (options: GenericOAuthOptions) => {
const ERROR_CODES = {
INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration",
} as const;
return {
id: "generic-oauth",
init: (ctx) => {
@@ -352,7 +355,7 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
}
if (!finalAuthUrl || !finalTokenUrl) {
throw new APIError("BAD_REQUEST", {
message: "Invalid OAuth configuration.",
message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
});
}
const { state, codeVerifier } = await generateState(ctx);
@@ -555,5 +558,6 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
},
),
},
$ERROR_CODES: ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -4,6 +4,7 @@ import type { BetterAuthPlugin } from "../../types/plugins";
import { APIError } from "better-call";
import { setSessionCookie } from "../../cookies";
import { alphabet, generateRandomString } from "../../crypto";
import { BASE_ERROR_CODES } from "../../error/codes";
interface MagicLinkOptions {
/**
@@ -95,7 +96,7 @@ export const magicLink = (options: MagicLinkOptions) => {
if (!user) {
throw new APIError("BAD_REQUEST", {
message: "User not found",
message: BASE_ERROR_CODES.USER_NOT_FOUND,
});
}
}
@@ -205,10 +206,12 @@ export const magicLink = (options: MagicLinkOptions) => {
});
userId = newUser.id;
if (!userId) {
throw ctx.redirect(`${toRedirectTo}?error=USER_NOT_CREATED`);
throw ctx.redirect(
`${toRedirectTo}?error=failed_to_create_user`,
);
}
} else {
throw ctx.redirect(`${toRedirectTo}?error=USER_NOT_FOUND`);
throw ctx.redirect(`${toRedirectTo}?error=failed_to_create_user`);
}
}
const session = await ctx.context.internalAdapter.createSession(
@@ -216,7 +219,9 @@ export const magicLink = (options: MagicLinkOptions) => {
ctx.headers,
);
if (!session) {
throw ctx.redirect(`${toRedirectTo}?error=SESSION_NOT_CREATED`);
throw ctx.redirect(
`${toRedirectTo}?error=failed_to_create_session`,
);
}
await setSessionCookie(ctx, {
session,
@@ -30,6 +30,10 @@ export const multiSession = (options?: MultiSessionConfig) => {
const isMultiSessionCookie = (key: string) => key.includes("_multi-");
const ERROR_CODES = {
INVALID_SESSION_TOKEN: "Invalid session token",
} as const;
return {
id: "multi-session",
endpoints: {
@@ -109,7 +113,7 @@ export const multiSession = (options?: MultiSessionConfig) => {
);
if (!sessionCookie) {
throw new APIError("UNAUTHORIZED", {
message: "Invalid session token",
message: ERROR_CODES.INVALID_SESSION_TOKEN,
});
}
const session =
@@ -120,7 +124,7 @@ export const multiSession = (options?: MultiSessionConfig) => {
maxAge: 0,
});
throw new APIError("UNAUTHORIZED", {
message: "Invalid session token",
message: ERROR_CODES.INVALID_SESSION_TOKEN,
});
}
await setSessionCookie(ctx, session);
@@ -170,7 +174,7 @@ export const multiSession = (options?: MultiSessionConfig) => {
);
if (!sessionCookie) {
throw new APIError("UNAUTHORIZED", {
message: "Invalid session token",
message: ERROR_CODES.INVALID_SESSION_TOKEN,
});
}
@@ -284,5 +288,6 @@ export const multiSession = (options?: MultiSessionConfig) => {
},
],
},
$ERROR_CODES: ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -0,0 +1,34 @@
export const ORGANIZATION_ERROR_CODES = {
YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION:
"You are not allowed to create a new organization",
YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS:
"You have reached the maximum number of organizations",
ORGANIZATION_ALREADY_EXISTS: "Organization already exists",
ORGANIZATION_NOT_FOUND: "Organization not found",
USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION:
"User is not a member of the organization",
YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION:
"You are not allowed to update this organization",
YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION:
"You are not allowed to delete this organization",
NO_ACTIVE_ORGANIZATION: "No active organization",
USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION:
"User is already a member of this organization",
MEMBER_NOT_FOUND: "Member not found",
ROLE_NOT_FOUND: "Role not found",
YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER:
"You cannot leave the organization as the only owner",
YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER:
"You are not allowed to delete this member",
YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION:
"You are not allowed to invite users to this organization",
USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION:
"User is already invited to this organization",
INVITATION_NOT_FOUND: "Invitation not found",
YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION:
"You are not the recipient of the invitation",
YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION:
"You are not allowed to cancel this invitation",
INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION:
"Inviter is no longer a member of the organization",
} as const;
@@ -44,6 +44,7 @@ import {
} from "./routes/crud-org";
import type { Invitation, Member, Organization } from "./schema";
import type { Prettify } from "../../types/helper";
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
export interface OrganizationOptions {
/**
@@ -308,7 +309,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
async (ctx) => {
if (!ctx.context.session.session.activeOrganizationId) {
throw new APIError("BAD_REQUEST", {
message: "No active organization",
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
});
}
const adapter = getOrgAdapter(ctx.context);
@@ -319,7 +320,8 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
});
if (!member) {
throw new APIError("UNAUTHORIZED", {
message: "You are not a member of this organization",
message:
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
});
}
const role = roles[member.role as keyof typeof roles];
@@ -481,5 +483,6 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
}
>,
},
$ERROR_CODES: ORGANIZATION_ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -6,6 +6,7 @@ import { orgMiddleware, orgSessionMiddleware } from "../call";
import { type InferRolesFromOption } from "../schema";
import { APIError } from "better-call";
import type { OrganizationOptions } from "../organization";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
export const createInvitation = <O extends OrganizationOptions | undefined>(
option: O,
@@ -99,7 +100,7 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
throw new APIError("BAD_REQUEST", {
message: "Organization not found",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
@@ -109,13 +110,13 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const role = ctx.context.roles[member.role];
if (!role) {
throw new APIError("BAD_REQUEST", {
message: "Role not found!",
message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
});
}
const canInvite = role.authorize({
@@ -123,7 +124,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
});
if (canInvite.error) {
throw new APIError("FORBIDDEN", {
message: "You are not allowed to invite members",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
});
}
const alreadyMember = await adapter.findMemberByEmail({
@@ -132,7 +134,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
});
if (alreadyMember) {
throw new APIError("BAD_REQUEST", {
message: "User is already a member of this organization",
message:
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
});
}
const alreadyInvited = await adapter.findPendingInvitation({
@@ -141,7 +144,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
});
if (alreadyInvited.length && !ctx.body.resend) {
throw new APIError("BAD_REQUEST", {
message: "User is already invited to this organization",
message:
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
});
}
const invitation = await adapter.createInvitation({
@@ -157,7 +161,7 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
if (!organization) {
throw new APIError("BAD_REQUEST", {
message: "Organization not found",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
@@ -224,12 +228,13 @@ export const acceptInvitation = createAuthEndpoint(
invitation.status !== "pending"
) {
throw new APIError("BAD_REQUEST", {
message: "Invitation not found!",
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
});
}
if (invitation.email !== session.user.email) {
throw new APIError("FORBIDDEN", {
message: "You are not the recipient of the invitation",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
});
}
const acceptedI = await adapter.updateInvitation({
@@ -250,7 +255,7 @@ export const acceptInvitation = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "Invitation not found!",
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
},
});
}
@@ -311,7 +316,8 @@ export const rejectInvitation = createAuthEndpoint(
}
if (invitation.email !== session.user.email) {
throw new APIError("FORBIDDEN", {
message: "You are not the recipient of the invitation",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
});
}
const rejectedI = await adapter.updateInvitation({
@@ -362,7 +368,7 @@ export const cancelInvitation = createAuthEndpoint(
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
if (!invitation) {
throw new APIError("BAD_REQUEST", {
message: "Invitation not found!",
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
});
}
const member = await adapter.findMemberByOrgId({
@@ -371,7 +377,7 @@ export const cancelInvitation = createAuthEndpoint(
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const canCancel = ctx.context.roles[member.role].authorize({
@@ -379,7 +385,8 @@ export const cancelInvitation = createAuthEndpoint(
});
if (canCancel.error) {
throw new APIError("FORBIDDEN", {
message: "You are not allowed to cancel this invitation",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION,
});
}
const canceledI = await adapter.updateInvitation({
@@ -483,7 +490,8 @@ export const getInvitation = createAuthEndpoint(
}
if (invitation.email !== session.user.email) {
throw new APIError("FORBIDDEN", {
message: "You are not the recipient of the invitation",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
});
}
const organization = await adapter.findOrganizationById(
@@ -491,7 +499,7 @@ export const getInvitation = createAuthEndpoint(
);
if (!organization) {
throw new APIError("BAD_REQUEST", {
message: "Organization not found",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
const member = await adapter.findMemberByOrgId({
@@ -500,7 +508,8 @@ export const getInvitation = createAuthEndpoint(
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: "Inviter is no longer a member of the organization",
message:
ORGANIZATION_ERROR_CODES.INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION,
});
}
@@ -8,6 +8,8 @@ import type { User } from "../../../db/schema";
import { generateId } from "../../../utils";
import type { OrganizationOptions } from "../organization";
import { getSessionFromCtx } from "../../../api";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { BASE_ERROR_CODES } from "../../../error/codes";
export const addMember = <O extends OrganizationOptions>() =>
createAuthEndpoint(
@@ -38,7 +40,7 @@ export const addMember = <O extends OrganizationOptions>() =>
return ctx.json(null, {
status: 400,
body: {
message: "No active organization found!",
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
@@ -51,7 +53,7 @@ export const addMember = <O extends OrganizationOptions>() =>
if (!user) {
throw new APIError("BAD_REQUEST", {
message: "User not found!",
message: BASE_ERROR_CODES.USER_NOT_FOUND,
});
}
@@ -61,7 +63,8 @@ export const addMember = <O extends OrganizationOptions>() =>
});
if (alreadyMember) {
throw new APIError("BAD_REQUEST", {
message: "User is already a member of this organization",
message:
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
});
}
@@ -143,7 +146,7 @@ export const removeMember = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "No active organization found!",
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
@@ -154,13 +157,13 @@ export const removeMember = createAuthEndpoint(
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const role = ctx.context.roles[member.role];
if (!role) {
throw new APIError("BAD_REQUEST", {
message: "Role not found!",
message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
});
}
const isLeaving =
@@ -171,7 +174,8 @@ export const removeMember = createAuthEndpoint(
member.role === (ctx.context.orgOptions?.creatorRole || "owner");
if (isOwnerLeaving) {
throw new APIError("BAD_REQUEST", {
message: "You cannot leave the organization as the owner",
message:
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
});
}
@@ -182,7 +186,8 @@ export const removeMember = createAuthEndpoint(
}).success;
if (!canDeleteMember) {
throw new APIError("UNAUTHORIZED", {
message: "You are not allowed to delete this member",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER,
});
}
let existing: Member | null = null;
@@ -196,7 +201,7 @@ export const removeMember = createAuthEndpoint(
}
if (existing?.organizationId !== organizationId) {
throw new APIError("BAD_REQUEST", {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
await adapter.deleteMember(existing.id);
@@ -273,7 +278,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
return ctx.json(null, {
status: 400,
body: {
message: "No active organization found!",
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
@@ -286,7 +291,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
return ctx.json(null, {
status: 400,
body: {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
},
});
}
@@ -295,7 +300,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
return ctx.json(null, {
status: 400,
body: {
message: "Role not found!",
message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
},
});
}
@@ -325,7 +330,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
return ctx.json(null, {
status: 400,
body: {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
},
});
}
@@ -378,7 +383,7 @@ export const getActiveMember = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "No active organization found!",
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
@@ -391,7 +396,7 @@ export const getActiveMember = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "Member not found!",
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
},
});
}
@@ -5,6 +5,7 @@ import { getOrgAdapter } from "../adapter";
import { orgMiddleware, orgSessionMiddleware } from "../call";
import { APIError } from "better-call";
import { setSessionCookie } from "../../../cookies";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { getSessionFromCtx } from "../../../api";
export const createOrganization = createAuthEndpoint(
@@ -83,7 +84,8 @@ export const createOrganization = createAuthEndpoint(
if (!canCreateOrg) {
throw new APIError("FORBIDDEN", {
message: "You are not allowed to create an organization",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION,
});
}
const adapter = getOrgAdapter(ctx.context, options);
@@ -98,7 +100,8 @@ export const createOrganization = createAuthEndpoint(
if (hasReachedOrgLimit) {
throw new APIError("FORBIDDEN", {
message: "You have reached the organization limit",
message:
ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS,
});
}
@@ -107,7 +110,7 @@ export const createOrganization = createAuthEndpoint(
);
if (existingOrganization) {
throw new APIError("BAD_REQUEST", {
message: "Organization with this slug already exists",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS,
});
}
const organization = await adapter.createOrganization({
@@ -197,7 +200,7 @@ export const updateOrganization = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "Organization id not found!",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
},
});
}
@@ -210,7 +213,8 @@ export const updateOrganization = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "User is not a member of this organization!",
message:
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
},
});
}
@@ -229,7 +233,8 @@ export const updateOrganization = createAuthEndpoint(
if (canUpdateOrg.error) {
return ctx.json(null, {
body: {
message: "You are not allowed to update this organization",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
},
status: 403,
});
@@ -284,7 +289,7 @@ export const deleteOrganization = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "Organization id not found!",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
},
});
}
@@ -297,7 +302,8 @@ export const deleteOrganization = createAuthEndpoint(
return ctx.json(null, {
status: 400,
body: {
message: "User is not a member of this organization!",
message:
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
},
});
}
@@ -315,7 +321,8 @@ export const deleteOrganization = createAuthEndpoint(
});
if (canDeleteOrg.error) {
throw new APIError("FORBIDDEN", {
message: "You are not allowed to delete this organization",
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION,
});
}
if (organizationId === session.session.activeOrganizationId) {
@@ -387,7 +394,7 @@ export const getFullOrganization = createAuthEndpoint(
});
if (!organization) {
throw new APIError("BAD_REQUEST", {
message: "Organization not found",
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
return ctx.json(organization);
@@ -470,7 +477,8 @@ export const setActiveOrganization = createAuthEndpoint(
if (!isMember) {
await adapter.setActiveOrganization(session.session.token, null);
throw new APIError("FORBIDDEN", {
message: "You are not a member of this organization",
message:
ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
});
}
const updatedSession = await adapter.setActiveOrganization(
@@ -107,6 +107,17 @@ export const passkey = (options?: PasskeyOptions) => {
const maxAgeInSeconds = Math.floor(
(expirationTime.getTime() - currentTime.getTime()) / 1000,
);
const ERROR_CODES = {
CHALLENGE_NOT_FOUND: "Challenge not found",
YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY:
"You are not allowed to register this passkey",
FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
PASSKEY_NOT_FOUND: "Passkey not found",
AUTHENTICATION_FAILED: "Authentication failed",
UNABLE_TO_CREATE_SESSION: "Unable to create session",
FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey",
} as const;
return {
id: "passkey",
endpoints: {
@@ -493,7 +504,7 @@ export const passkey = (options?: PasskeyOptions) => {
);
if (!challengeId) {
throw new APIError("BAD_REQUEST", {
message: "Challenge not found",
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
});
}
@@ -512,7 +523,7 @@ export const passkey = (options?: PasskeyOptions) => {
if (userData.id !== ctx.context.session.user.id) {
throw new APIError("UNAUTHORIZED", {
message: "You are not authorized to register this passkey",
message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
});
}
@@ -559,7 +570,7 @@ export const passkey = (options?: PasskeyOptions) => {
} catch (e) {
console.log(e);
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to verify registration",
message: ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
});
}
},
@@ -611,7 +622,7 @@ export const passkey = (options?: PasskeyOptions) => {
);
if (!challengeId) {
throw new APIError("BAD_REQUEST", {
message: "Challenge not found",
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
});
}
@@ -621,7 +632,7 @@ export const passkey = (options?: PasskeyOptions) => {
);
if (!data) {
throw new APIError("BAD_REQUEST", {
message: "Challenge not found",
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
});
}
const { expectedChallenge } = JSON.parse(
@@ -638,7 +649,7 @@ export const passkey = (options?: PasskeyOptions) => {
});
if (!passkey) {
throw new APIError("UNAUTHORIZED", {
message: "Passkey not found",
message: ERROR_CODES.PASSKEY_NOT_FOUND,
});
}
try {
@@ -662,7 +673,7 @@ export const passkey = (options?: PasskeyOptions) => {
const { verified } = verification;
if (!verified)
throw new APIError("UNAUTHORIZED", {
message: "Authentication failed",
message: ERROR_CODES.AUTHENTICATION_FAILED,
});
await ctx.context.adapter.update<Passkey>({
@@ -683,7 +694,7 @@ export const passkey = (options?: PasskeyOptions) => {
);
if (!s) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Unable to create session",
message: ERROR_CODES.UNABLE_TO_CREATE_SESSION,
});
}
const user = await ctx.context.internalAdapter.findUserById(
@@ -709,7 +720,7 @@ export const passkey = (options?: PasskeyOptions) => {
} catch (e) {
ctx.context.logger.error("Failed to verify authentication", e);
throw new APIError("BAD_REQUEST", {
message: "Failed to verify authentication",
message: ERROR_CODES.AUTHENTICATION_FAILED,
});
}
},
@@ -777,13 +788,13 @@ export const passkey = (options?: PasskeyOptions) => {
if (!passkey) {
throw new APIError("NOT_FOUND", {
message: "Passkey not found",
message: ERROR_CODES.PASSKEY_NOT_FOUND,
});
}
if (passkey.userId !== ctx.context.session.user.id) {
throw new APIError("UNAUTHORIZED", {
message: "You are not authorized to update this passkey",
message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
});
}
@@ -802,7 +813,7 @@ export const passkey = (options?: PasskeyOptions) => {
if (!updatedPasskey) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to update passkey",
message: ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
});
}
return ctx.json(
@@ -817,6 +828,7 @@ export const passkey = (options?: PasskeyOptions) => {
),
},
schema: mergeSchema(schema, options?.schema),
$ERROR_CODES: ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -11,6 +11,7 @@ import { alphabet, generateRandomString } from "../../crypto/random";
import { getSessionFromCtx } from "../../api";
import { getDate } from "../../utils/date";
import { setSessionCookie } from "../../cookies";
import { BASE_ERROR_CODES } from "../../error/codes";
export interface UserWithPhoneNumber extends User {
phoneNumber: string;
@@ -101,6 +102,13 @@ export const phoneNumber = (options?: {
code: "code",
createdAt: "createdAt",
};
const ERROR_CODES = {
INVALID_PHONE_NUMBER: "Invalid phone number",
INVALID_PHONE_NUMBER_OR_PASSWORD: "Invalid phone number or password",
UNEXPECTED_ERROR: "Unexpected error",
OTP_NOT_FOUND: "OTP not found",
} as const;
return {
id: "phone-number",
endpoints: {
@@ -160,7 +168,7 @@ export const phoneNumber = (options?: {
);
if (!isValidNumber) {
throw new APIError("BAD_REQUEST", {
message: "Invalid phone number!",
message: ERROR_CODES.INVALID_PHONE_NUMBER,
});
}
}
@@ -176,7 +184,7 @@ export const phoneNumber = (options?: {
});
if (!user) {
throw new APIError("UNAUTHORIZED", {
message: "Invalid phone number or password",
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
});
}
const accounts =
@@ -189,14 +197,14 @@ export const phoneNumber = (options?: {
phoneNumber,
});
throw new APIError("UNAUTHORIZED", {
message: "Invalid password or password",
message: 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: "Unexpected error",
message: ERROR_CODES.UNEXPECTED_ERROR,
});
}
const validPassword = await ctx.context.password.verify({
@@ -206,7 +214,7 @@ export const phoneNumber = (options?: {
if (!validPassword) {
ctx.context.logger.error("Invalid password");
throw new APIError("UNAUTHORIZED", {
message: "Invalid email or password",
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD,
});
}
const session = await ctx.context.internalAdapter.createSession(
@@ -217,7 +225,7 @@ export const phoneNumber = (options?: {
if (!session) {
ctx.context.logger.error("Failed to create session");
throw new APIError("UNAUTHORIZED", {
message: "Failed to create session",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
});
}
@@ -282,7 +290,7 @@ export const phoneNumber = (options?: {
);
if (!isValidNumber) {
throw new APIError("BAD_REQUEST", {
message: "Invalid phone number!",
message: ERROR_CODES.INVALID_PHONE_NUMBER,
});
}
}
@@ -391,7 +399,7 @@ export const phoneNumber = (options?: {
});
}
throw new APIError("BAD_REQUEST", {
message: "OTP not found",
message: ERROR_CODES.OTP_NOT_FOUND,
});
}
if (otp.value !== ctx.body.code) {
@@ -406,7 +414,7 @@ export const phoneNumber = (options?: {
const session = await getSessionFromCtx(ctx);
if (!session) {
throw new APIError("UNAUTHORIZED", {
message: "Session not found",
message: BASE_ERROR_CODES.USER_NOT_FOUND,
});
}
const user = await ctx.context.internalAdapter.updateUser(
@@ -454,7 +462,7 @@ export const phoneNumber = (options?: {
});
if (!user) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to create user",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
});
}
} else {
@@ -468,7 +476,7 @@ export const phoneNumber = (options?: {
if (!user) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to update user",
message: BASE_ERROR_CODES.FAILED_TO_UPDATE_USER,
});
}
@@ -479,7 +487,7 @@ export const phoneNumber = (options?: {
);
if (!session) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to create session",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
});
}
await setSessionCookie(ctx, {
@@ -500,6 +508,7 @@ export const phoneNumber = (options?: {
),
},
schema: mergeSchema(schema, options?.schema),
$ERROR_CODES: ERROR_CODES,
} satisfies BetterAuthPlugin;
};
@@ -11,6 +11,7 @@ import type {
} from "../types";
import { APIError } from "better-call";
import { setSessionCookie } from "../../../cookies";
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
export interface BackupCodeOptions {
/**
@@ -121,7 +122,7 @@ export const backupCode2fa = (
});
if (!twoFactor) {
throw new APIError("BAD_REQUEST", {
message: "Backup codes aren't enabled",
message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
});
}
const validate = await verifyBackupCode(
@@ -133,7 +134,7 @@ export const backupCode2fa = (
);
if (!validate.status) {
throw new APIError("UNAUTHORIZED", {
message: "Invalid backup code",
message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
});
}
const updatedBackupCodes = await symmetricEncrypt({
@@ -179,7 +180,7 @@ export const backupCode2fa = (
const user = ctx.context.session.user as UserWithTwoFactor;
if (!user.twoFactorEnabled) {
throw new APIError("BAD_REQUEST", {
message: "Two factor isn't enabled",
message: TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED,
});
}
await ctx.context.password.checkPassword(user.id, ctx);
@@ -237,7 +238,7 @@ export const backupCode2fa = (
);
if (!backupCodes) {
throw new APIError("BAD_REQUEST", {
message: "Backup codes aren't enabled",
message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
});
}
return ctx.json({
@@ -0,0 +1,8 @@
export const TWO_FACTOR_ERROR_CODES = {
OTP_NOT_ENABLED: "OTP not enabled",
OTP_HAS_EXPIRED: "OTP has expired",
TOTP_NOT_ENABLED: "TOTP not enabled",
TWO_FACTOR_NOT_ENABLED: "Two factor isn't enabled",
BACKUP_CODES_NOT_ENABLED: "Backup codes aren't enabled",
INVALID_BACKUP_CODE: "Invalid backup code",
} as const;
@@ -17,6 +17,7 @@ import { TimeSpan } from "oslo";
import { deleteSessionCookie, setSessionCookie } from "../../cookies";
import { getEndpointResponse } from "../../utils/plugin-helper";
import { schema } from "./schema";
import { BASE_ERROR_CODES } from "../../error/codes";
export const twoFactor = (options?: TwoFactorOptions) => {
const opts = {
@@ -41,6 +42,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
},
opts.twoFactorTable,
);
return {
id: "two-factor",
endpoints: {
@@ -101,7 +103,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
});
if (!isPasswordValid) {
throw new APIError("BAD_REQUEST", {
message: "Invalid password",
message: BASE_ERROR_CODES.INVALID_PASSWORD,
});
}
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
@@ -9,6 +9,7 @@ import type {
} from "../types";
import { TimeSpan } from "oslo";
import { alphabet, generateRandomString } from "../../../crypto";
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
export interface OTPOptions {
/**
@@ -111,7 +112,7 @@ export const otp2fa = (options: OTPOptions, twoFactorTable: string) => {
});
if (!twoFactor) {
throw new APIError("BAD_REQUEST", {
message: "OTP isn't enabled",
message: TWO_FACTOR_ERROR_CODES.OTP_NOT_ENABLED,
});
}
const code = generateRandomString(opts.digits, alphabet("0-9"));
@@ -177,7 +178,7 @@ export const otp2fa = (options: OTPOptions, twoFactorTable: string) => {
});
if (!twoFactor) {
throw new APIError("BAD_REQUEST", {
message: "OTP isn't enabled",
message: TWO_FACTOR_ERROR_CODES.OTP_NOT_ENABLED,
});
}
const toCheckOtp =
@@ -186,7 +187,7 @@ export const otp2fa = (options: OTPOptions, twoFactorTable: string) => {
);
if (!toCheckOtp || toCheckOtp.expiresAt < new Date()) {
throw new APIError("BAD_REQUEST", {
message: "OTP has expired",
message: TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED,
});
}
if (toCheckOtp.value === ctx.body.code) {
@@ -13,6 +13,7 @@ import type {
UserWithTwoFactor,
} from "../types";
import { setSessionCookie } from "../../../cookies";
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
export type TOTPOptions = {
/**
@@ -93,7 +94,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
});
if (!twoFactor) {
throw new APIError("BAD_REQUEST", {
message: "totp isn't enabled",
message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
});
}
const totp = new TOTPController(opts);
@@ -157,7 +158,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
});
if (!twoFactor || !user.twoFactorEnabled) {
throw new APIError("BAD_REQUEST", {
message: "totp isn't enabled",
message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
});
}
await ctx.context.password.checkPassword(user.id, ctx);
@@ -228,7 +229,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
if (!twoFactor) {
throw new APIError("BAD_REQUEST", {
message: "totp isn't enabled",
message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
});
}
const totp = new TOTPController(opts);
@@ -5,8 +5,15 @@ import { APIError } from "better-call";
import type { Account, User } from "../../db/schema";
import { setSessionCookie } from "../../cookies";
import { sendVerificationEmailFn } from "../../api";
import { BASE_ERROR_CODES } from "../../error/codes";
import { TWO_FACTOR_ERROR_CODES } from "../two-factor/error-code";
export const username = () => {
const ERROR_CODES = {
INVALID_USERNAME_OR_PASSWORD: "invalid username or password",
EMAIL_NOT_VERIFIED: "email not verified",
UNEXPECTED_ERROR: "unexpected error",
};
return {
id: "username",
endpoints: {
@@ -68,7 +75,7 @@ export const username = () => {
await ctx.context.password.hash(ctx.body.password);
ctx.context.logger.error("User not found", { username });
throw new APIError("UNAUTHORIZED", {
message: "Invalid username or password",
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
});
}
@@ -78,7 +85,7 @@ export const username = () => {
) {
await sendVerificationEmailFn(ctx, user);
throw new APIError("UNAUTHORIZED", {
message: "Email not verified",
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
});
}
@@ -97,14 +104,14 @@ export const username = () => {
});
if (!account) {
throw new APIError("UNAUTHORIZED", {
message: "Invalid username or password",
message: 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: "Unexpected error",
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
});
}
const validPassword = await ctx.context.password.verify({
@@ -114,7 +121,7 @@ export const username = () => {
if (!validPassword) {
ctx.context.logger.error("Invalid password");
throw new APIError("UNAUTHORIZED", {
message: "Invalid username or password",
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
});
}
const session = await ctx.context.internalAdapter.createSession(
@@ -126,7 +133,7 @@ export const username = () => {
return ctx.json(null, {
status: 500,
body: {
message: "Failed to create session",
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
status: 500,
},
});
@@ -155,5 +162,6 @@ export const username = () => {
},
},
},
$ERROR_CODES: TWO_FACTOR_ERROR_CODES,
} satisfies BetterAuthPlugin;
};
+16 -1
View File
@@ -3,7 +3,7 @@ import type { Migration } from "kysely";
import type { AuthEndpoint } from "../api/call";
import type { FieldAttribute } from "../db/field";
import type { HookEndpointContext } from "./context";
import type { DeepPartial, LiteralString } from "./helper";
import type { DeepPartial, LiteralString, UnionToIntersection } from "./helper";
import type { AuthContext, BetterAuthOptions } from ".";
export type PluginSchema = {
@@ -137,6 +137,10 @@ export type BetterAuthPlugin = {
max: number;
pathMatcher: (path: string) => boolean;
}[];
/**
* The error codes returned by the plugin
*/
$ERROR_CODES?: Record<string, string>;
};
export type InferOptionSchema<S extends PluginSchema> = S extends Record<
@@ -152,3 +156,14 @@ export type InferOptionSchema<S extends PluginSchema> = S extends Record<
};
}
: never;
export type InferPluginErrorCodes<O extends BetterAuthOptions> =
O["plugins"] extends Array<infer P>
? UnionToIntersection<
P extends BetterAuthPlugin
? P["$ERROR_CODES"] extends Record<string, any>
? P["$ERROR_CODES"]
: {}
: {}
>
: {};