@zpg6-test-pkgs/better-auth
Version:
The most comprehensive authentication library for TypeScript.
931 lines (924 loc) • 32 kB
JavaScript
'use strict';
const z = require('zod/v4');
const session = require('../../shared/better-auth.DmBU2Klq.cjs');
const betterCall = require('better-call');
const schema$1 = require('../../shared/better-auth.BIMq4RPW.cjs');
const random = require('../../shared/better-auth.CYeOI8C-.cjs');
require('../../shared/better-auth.afydZyFs.cjs');
const cookies_index = require('../../shared/better-auth.l2-e84v_.cjs');
require('../../shared/better-auth.B6fIklBU.cjs');
require('../../shared/better-auth.B3274wGK.cjs');
const date = require('../../shared/better-auth.C1hdVENX.cjs');
require('../../shared/better-auth.vPQBmXQL.cjs');
require('@better-auth/utils/hmac');
require('@better-auth/utils/base64');
require('@better-auth/utils/binary');
require('@better-auth/utils/random');
require('@better-auth/utils/hash');
require('../../crypto/index.cjs');
require('@noble/ciphers/chacha');
require('@noble/ciphers/utils');
require('@noble/ciphers/webcrypto');
require('jose');
require('@noble/hashes/scrypt');
require('@better-auth/utils');
require('@better-auth/utils/hex');
require('@noble/hashes/utils');
require('@better-fetch/fetch');
require('../../shared/better-auth.DRmln2Nr.cjs');
require('../../shared/better-auth.ANpbi45u.cjs');
require('jose/errors');
require('../../shared/better-auth.Bg6iw3ig.cjs');
require('defu');
function _interopNamespaceCompat(e) {
if (e && typeof e === 'object' && 'default' in e) return e;
const n = Object.create(null);
if (e) {
for (const k in e) {
n[k] = e[k];
}
}
n.default = e;
return n;
}
const z__namespace = /*#__PURE__*/_interopNamespaceCompat(z);
const ERROR_CODES = {
INVALID_PHONE_NUMBER: "Invalid phone number",
PHONE_NUMBER_EXIST: "Phone number already exists",
INVALID_PHONE_NUMBER_OR_PASSWORD: "Invalid phone number or password",
UNEXPECTED_ERROR: "Unexpected error",
OTP_NOT_FOUND: "OTP not found",
OTP_EXPIRED: "OTP expired",
INVALID_OTP: "Invalid OTP",
PHONE_NUMBER_NOT_VERIFIED: "Phone number not verified"
};
function generateOTP(size) {
return random.generateRandomString(size, "0-9");
}
const phoneNumber = (options) => {
const opts = {
expiresIn: options?.expiresIn || 300,
otpLength: options?.otpLength || 6,
...options,
phoneNumber: "phoneNumber",
phoneNumberVerified: "phoneNumberVerified",
code: "code",
createdAt: "createdAt"
};
return {
id: "phone-number",
endpoints: {
/**
* ### Endpoint
*
* POST `/sign-in/phone-number`
*
* ### API Methods
*
* **server:**
* `auth.api.signInPhoneNumber`
*
* **client:**
* `authClient.signIn.phoneNumber`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/phone-number#api-method-sign-in-phone-number)
*/
signInPhoneNumber: session.createAuthEndpoint(
"/sign-in/phone-number",
{
method: "POST",
body: z__namespace.object({
phoneNumber: z__namespace.string().meta({
description: 'Phone number to sign in. Eg: "+1234567890"'
}),
password: z__namespace.string().meta({
description: "Password to use for sign in."
}),
rememberMe: z__namespace.boolean().meta({
description: "Remember the session. Eg: true"
}).optional()
}),
metadata: {
openapi: {
summary: "Sign in with phone number",
description: "Use this endpoint to sign in with phone number",
responses: {
200: {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
$ref: "#/components/schemas/User"
},
session: {
$ref: "#/components/schemas/Session"
}
}
}
}
}
},
400: {
description: "Invalid phone number or password"
}
}
}
}
},
async (ctx) => {
const { password, phoneNumber: phoneNumber2 } = ctx.body;
if (opts.phoneNumberValidator) {
const isValidNumber = await opts.phoneNumberValidator(
ctx.body.phoneNumber
);
if (!isValidNumber) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.INVALID_PHONE_NUMBER
});
}
}
const user = await ctx.context.adapter.findOne({
model: "user",
where: [
{
field: "phoneNumber",
value: phoneNumber2
}
]
});
if (!user) {
throw new betterCall.APIError("UNAUTHORIZED", {
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD
});
}
if (opts.requireVerification) {
if (!user.phoneNumberVerified) {
const otp = generateOTP(opts.otpLength);
await ctx.context.internalAdapter.createVerificationValue(
{
value: otp,
identifier: phoneNumber2,
expiresAt: date.getDate(opts.expiresIn, "sec")
},
ctx
);
await opts.sendOTP?.(
{
phoneNumber: phoneNumber2,
code: otp
},
ctx.request
);
throw new betterCall.APIError("UNAUTHORIZED", {
message: ERROR_CODES.PHONE_NUMBER_NOT_VERIFIED
});
}
}
const accounts = await ctx.context.internalAdapter.findAccountByUserId(user.id);
const credentialAccount = accounts.find(
(a) => a.providerId === "credential"
);
if (!credentialAccount) {
ctx.context.logger.error("Credential account not found", {
phoneNumber: phoneNumber2
});
throw new betterCall.APIError("UNAUTHORIZED", {
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD
});
}
const currentPassword = credentialAccount?.password;
if (!currentPassword) {
ctx.context.logger.error("Password not found", { phoneNumber: phoneNumber2 });
throw new betterCall.APIError("UNAUTHORIZED", {
message: ERROR_CODES.UNEXPECTED_ERROR
});
}
const validPassword = await ctx.context.password.verify({
hash: currentPassword,
password
});
if (!validPassword) {
ctx.context.logger.error("Invalid password");
throw new betterCall.APIError("UNAUTHORIZED", {
message: ERROR_CODES.INVALID_PHONE_NUMBER_OR_PASSWORD
});
}
const session$1 = await ctx.context.internalAdapter.createSession(
user.id,
ctx,
ctx.body.rememberMe === false
);
if (!session$1) {
ctx.context.logger.error("Failed to create session");
throw new betterCall.APIError("UNAUTHORIZED", {
message: session.BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION
});
}
await cookies_index.setSessionCookie(
ctx,
{
session: session$1,
user
},
ctx.body.rememberMe === false
);
return ctx.json({
token: session$1.token,
user: {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
name: user.name,
image: user.image,
phoneNumber: user.phoneNumber,
phoneNumberVerified: user.phoneNumberVerified,
createdAt: user.createdAt,
updatedAt: user.updatedAt
}
});
}
),
/**
* ### Endpoint
*
* POST `/phone-number/send-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.sendPhoneNumberOTP`
*
* **client:**
* `authClient.phoneNumber.sendOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/phone-number#api-method-phone-number-send-otp)
*/
sendPhoneNumberOTP: session.createAuthEndpoint(
"/phone-number/send-otp",
{
method: "POST",
body: z__namespace.object({
phoneNumber: z__namespace.string().meta({
description: 'Phone number to send OTP. Eg: "+1234567890"'
})
}),
metadata: {
openapi: {
summary: "Send OTP to phone number",
description: "Use this endpoint to send OTP to phone number",
responses: {
200: {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string"
}
}
}
}
}
}
}
}
}
},
async (ctx) => {
if (!options?.sendOTP) {
ctx.context.logger.warn("sendOTP not implemented");
throw new betterCall.APIError("NOT_IMPLEMENTED", {
message: "sendOTP not implemented"
});
}
if (opts.phoneNumberValidator) {
const isValidNumber = await opts.phoneNumberValidator(
ctx.body.phoneNumber
);
if (!isValidNumber) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.INVALID_PHONE_NUMBER
});
}
}
const code = generateOTP(opts.otpLength);
await ctx.context.internalAdapter.createVerificationValue(
{
value: `${code}:0`,
identifier: ctx.body.phoneNumber,
expiresAt: date.getDate(opts.expiresIn, "sec")
},
ctx
);
await options.sendOTP(
{
phoneNumber: ctx.body.phoneNumber,
code
},
ctx.request
);
return ctx.json({ message: "code sent" });
}
),
/**
* ### Endpoint
*
* POST `/phone-number/verify`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyPhoneNumber`
*
* **client:**
* `authClient.phoneNumber.verify`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/phone-number#api-method-phone-number-verify)
*/
verifyPhoneNumber: session.createAuthEndpoint(
"/phone-number/verify",
{
method: "POST",
body: z__namespace.object({
/**
* Phone number
*/
phoneNumber: z__namespace.string().meta({
description: 'Phone number to verify. Eg: "+1234567890"'
}),
/**
* OTP code
*/
code: z__namespace.string().meta({
description: 'OTP code. Eg: "123456"'
}),
/**
* Disable session creation after verification
* @default false
*/
disableSession: z__namespace.boolean().meta({
description: "Disable session creation after verification. Eg: false"
}).optional(),
/**
* This checks if there is a session already
* and updates the phone number with the provided
* phone number
*/
updatePhoneNumber: z__namespace.boolean().meta({
description: "Check if there is a session and update the phone number. Eg: true"
}).optional()
}),
metadata: {
openapi: {
summary: "Verify phone number",
description: "Use this endpoint to verify phone number",
responses: {
"200": {
description: "Phone number verified successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
description: "Indicates if the verification was successful",
enum: [true]
},
token: {
type: "string",
nullable: true,
description: "Session token if session is created, null if disableSession is true or no session is created"
},
user: {
type: "object",
nullable: true,
properties: {
id: {
type: "string",
description: "Unique identifier of the user"
},
email: {
type: "string",
format: "email",
nullable: true,
description: "User's email address"
},
emailVerified: {
type: "boolean",
nullable: true,
description: "Whether the email is verified"
},
name: {
type: "string",
nullable: true,
description: "User's name"
},
image: {
type: "string",
format: "uri",
nullable: true,
description: "User's profile image URL"
},
phoneNumber: {
type: "string",
description: "User's phone number"
},
phoneNumberVerified: {
type: "boolean",
description: "Whether the phone number is verified"
},
createdAt: {
type: "string",
format: "date-time",
description: "Timestamp when the user was created"
},
updatedAt: {
type: "string",
format: "date-time",
description: "Timestamp when the user was last updated"
}
},
required: [
"id",
"phoneNumber",
"phoneNumberVerified",
"createdAt",
"updatedAt"
],
description: "User object with phone number details, null if no user is created or found"
}
},
required: ["status"]
}
}
}
},
400: {
description: "Invalid OTP"
}
}
}
}
},
async (ctx) => {
const otp = await ctx.context.internalAdapter.findVerificationValue(
ctx.body.phoneNumber
);
if (!otp || otp.expiresAt < /* @__PURE__ */ new Date()) {
if (otp && otp.expiresAt < /* @__PURE__ */ new Date()) {
throw new betterCall.APIError("BAD_REQUEST", {
message: "OTP expired"
});
}
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.OTP_NOT_FOUND
});
}
const [otpValue, attempts] = otp.value.split(":");
const allowedAttempts = options?.allowedAttempts || 3;
if (attempts && parseInt(attempts) >= allowedAttempts) {
await ctx.context.internalAdapter.deleteVerificationValue(otp.id);
throw new betterCall.APIError("FORBIDDEN", {
message: "Too many attempts"
});
}
if (otpValue !== ctx.body.code) {
await ctx.context.internalAdapter.updateVerificationValue(otp.id, {
value: `${otpValue}:${parseInt(attempts || "0") + 1}`
});
throw new betterCall.APIError("BAD_REQUEST", {
message: "Invalid OTP"
});
}
await ctx.context.internalAdapter.deleteVerificationValue(otp.id);
if (ctx.body.updatePhoneNumber) {
const session$1 = await session.getSessionFromCtx(ctx);
if (!session$1) {
throw new betterCall.APIError("UNAUTHORIZED", {
message: session.BASE_ERROR_CODES.USER_NOT_FOUND
});
}
const existingUser = await ctx.context.adapter.findMany({
model: "user",
where: [
{
field: "phoneNumber",
value: ctx.body.phoneNumber
}
]
});
if (existingUser.length) {
throw ctx.error("BAD_REQUEST", {
message: ERROR_CODES.PHONE_NUMBER_EXIST
});
}
let user2 = await ctx.context.internalAdapter.updateUser(
session$1.user.id,
{
[opts.phoneNumber]: ctx.body.phoneNumber,
[opts.phoneNumberVerified]: true
},
ctx
);
return ctx.json({
status: true,
token: session$1.session.token,
user: {
id: user2.id,
email: user2.email,
emailVerified: user2.emailVerified,
name: user2.name,
image: user2.image,
phoneNumber: user2.phoneNumber,
phoneNumberVerified: user2.phoneNumberVerified,
createdAt: user2.createdAt,
updatedAt: user2.updatedAt
}
});
}
let user = await ctx.context.adapter.findOne({
model: "user",
where: [
{
value: ctx.body.phoneNumber,
field: opts.phoneNumber
}
]
});
if (!user) {
if (options?.signUpOnVerification) {
user = await ctx.context.internalAdapter.createUser(
{
email: options.signUpOnVerification.getTempEmail(
ctx.body.phoneNumber
),
name: options.signUpOnVerification.getTempName ? options.signUpOnVerification.getTempName(
ctx.body.phoneNumber
) : ctx.body.phoneNumber,
[opts.phoneNumber]: ctx.body.phoneNumber,
[opts.phoneNumberVerified]: true
},
ctx
);
if (!user) {
throw new betterCall.APIError("INTERNAL_SERVER_ERROR", {
message: session.BASE_ERROR_CODES.FAILED_TO_CREATE_USER
});
}
}
} else {
user = await ctx.context.internalAdapter.updateUser(
user.id,
{
[opts.phoneNumberVerified]: true
},
ctx
);
}
if (!user) {
throw new betterCall.APIError("INTERNAL_SERVER_ERROR", {
message: session.BASE_ERROR_CODES.FAILED_TO_UPDATE_USER
});
}
await options?.callbackOnVerification?.(
{
phoneNumber: ctx.body.phoneNumber,
user
},
ctx.request
);
if (!ctx.body.disableSession) {
const session$1 = await ctx.context.internalAdapter.createSession(
user.id,
ctx
);
if (!session$1) {
throw new betterCall.APIError("INTERNAL_SERVER_ERROR", {
message: session.BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION
});
}
await cookies_index.setSessionCookie(ctx, {
session: session$1,
user
});
return ctx.json({
status: true,
token: session$1.token,
user: {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
name: user.name,
image: user.image,
phoneNumber: user.phoneNumber,
phoneNumberVerified: user.phoneNumberVerified,
createdAt: user.createdAt,
updatedAt: user.updatedAt
}
});
}
return ctx.json({
status: true,
token: null,
user: {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
name: user.name,
image: user.image,
phoneNumber: user.phoneNumber,
phoneNumberVerified: user.phoneNumberVerified,
createdAt: user.createdAt,
updatedAt: user.updatedAt
}
});
}
),
/**
* @deprecated Use requestPasswordResetPhoneNumber instead. This endpoint will be removed in the next major version.
*/
forgetPasswordPhoneNumber: session.createAuthEndpoint(
"/phone-number/forget-password",
{
method: "POST",
body: z__namespace.object({
phoneNumber: z__namespace.string().meta({
description: `The phone number which is associated with the user. Eg: "+1234567890"`
})
}),
metadata: {
openapi: {
description: "Request OTP for password reset via phone number",
responses: {
"200": {
description: "OTP sent successfully for password reset",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
description: "Indicates if the OTP was sent successfully",
enum: [true]
}
},
required: ["status"]
}
}
}
}
}
}
}
},
async (ctx) => {
const user = await ctx.context.adapter.findOne({
model: "user",
where: [
{
value: ctx.body.phoneNumber,
field: opts.phoneNumber
}
]
});
if (!user) {
throw new betterCall.APIError("BAD_REQUEST", {
message: "phone number isn't registered"
});
}
const code = generateOTP(opts.otpLength);
await ctx.context.internalAdapter.createVerificationValue(
{
value: `${code}:0`,
identifier: `${ctx.body.phoneNumber}-request-password-reset`,
expiresAt: date.getDate(opts.expiresIn, "sec")
},
ctx
);
await options?.sendForgetPasswordOTP?.(
{
phoneNumber: ctx.body.phoneNumber,
code
},
ctx.request
);
return ctx.json({
status: true
});
}
),
requestPasswordResetPhoneNumber: session.createAuthEndpoint(
"/phone-number/request-password-reset",
{
method: "POST",
body: z__namespace.object({
phoneNumber: z__namespace.string()
}),
metadata: {
openapi: {
description: "Request OTP for password reset via phone number",
responses: {
"200": {
description: "OTP sent successfully for password reset",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
description: "Indicates if the OTP was sent successfully",
enum: [true]
}
},
required: ["status"]
}
}
}
}
}
}
}
},
async (ctx) => {
const user = await ctx.context.adapter.findOne({
model: "user",
where: [
{
value: ctx.body.phoneNumber,
field: opts.phoneNumber
}
]
});
if (!user) {
throw new betterCall.APIError("BAD_REQUEST", {
message: "phone number isn't registered"
});
}
const code = generateOTP(opts.otpLength);
await ctx.context.internalAdapter.createVerificationValue(
{
value: `${code}:0`,
identifier: `${ctx.body.phoneNumber}-request-password-reset`,
expiresAt: date.getDate(opts.expiresIn, "sec")
},
ctx
);
await options?.sendPasswordResetOTP?.(
{
phoneNumber: ctx.body.phoneNumber,
code
},
ctx.request
);
return ctx.json({
status: true
});
}
),
resetPasswordPhoneNumber: session.createAuthEndpoint(
"/phone-number/reset-password",
{
method: "POST",
body: z__namespace.object({
otp: z__namespace.string().meta({
description: 'The one time password to reset the password. Eg: "123456"'
}),
phoneNumber: z__namespace.string().meta({
description: 'The phone number to the account which intends to reset the password for. Eg: "+1234567890"'
}),
newPassword: z__namespace.string().meta({
description: `The new password. Eg: "new-and-secure-password"`
})
}),
metadata: {
openapi: {
description: "Reset password using phone number OTP",
responses: {
"200": {
description: "Password reset successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
description: "Indicates if the password was reset successfully",
enum: [true]
}
},
required: ["status"]
}
}
}
}
}
}
}
},
async (ctx) => {
const verification = await ctx.context.internalAdapter.findVerificationValue(
`${ctx.body.phoneNumber}-request-password-reset`
);
if (!verification) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.OTP_NOT_FOUND
});
}
if (verification.expiresAt < /* @__PURE__ */ new Date()) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.OTP_EXPIRED
});
}
const [otpValue, attempts] = verification.value.split(":");
const allowedAttempts = options?.allowedAttempts || 3;
if (attempts && parseInt(attempts) >= allowedAttempts) {
await ctx.context.internalAdapter.deleteVerificationValue(
verification.id
);
throw new betterCall.APIError("FORBIDDEN", {
message: "Too many attempts"
});
}
if (ctx.body.otp !== otpValue) {
await ctx.context.internalAdapter.updateVerificationValue(
verification.id,
{
value: `${otpValue}:${parseInt(attempts || "0") + 1}`
}
);
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.INVALID_OTP
});
}
const user = await ctx.context.adapter.findOne({
model: "user",
where: [
{
field: "phoneNumber",
value: ctx.body.phoneNumber
}
]
});
if (!user) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ERROR_CODES.UNEXPECTED_ERROR
});
}
const hashedPassword = await ctx.context.password.hash(
ctx.body.newPassword
);
await ctx.context.internalAdapter.updatePassword(
user.id,
hashedPassword
);
await ctx.context.internalAdapter.deleteVerificationValue(
verification.id
);
return ctx.json({
status: true
});
}
)
},
schema: schema$1.mergeSchema(schema, options?.schema),
rateLimit: [
{
pathMatcher(path) {
return path.startsWith("/phone-number");
},
window: 60 * 1e3,
max: 10
}
],
$ERROR_CODES: ERROR_CODES
};
};
const schema = {
user: {
fields: {
phoneNumber: {
type: "string",
required: false,
unique: true,
sortable: true,
returned: true
},
phoneNumberVerified: {
type: "boolean",
required: false,
returned: true,
input: false
}
}
}
};
exports.phoneNumber = phoneNumber;