import { env, isTest, isDevelopment, logger, createLogger, isProduction, shouldPublishLog } from "@better-auth/core/env"; import { BetterAuthError, BASE_ERROR_CODES } from "@better-auth/core/error"; import { isValidIP, normalizeIP, deprecate, normalizePathname, createRateLimitKey, safeJSONParse, generateId, filterOutputFields, defineErrorCodes } from "@better-auth/core/utils"; import { defineRequestState, getCurrentAuthContext, getCurrentAdapter, runWithTransaction, hasRequestState, runWithRequestState, runWithEndpointContext, runWithAdapter, getBetterAuthVersion } from "@better-auth/core/context"; import { APIError, toResponse, createRouter } from "better-call"; import { createAuthMiddleware, createAuthEndpoint } from "@better-auth/core/api"; import * as import___better_auth_core_db from "@better-auth/core/db"; import { getAuthTables } from "@better-auth/core/db"; import * as z from "zod"; import { Kysely, SqliteDialect, MysqlDialect, PostgresDialect, MssqlDialect, sql } from "kysely"; import { initGetModelName, initGetFieldName, createAdapterFactory } from "@better-auth/core/db/adapter"; import { hkdf } from "@noble/hashes/hkdf.js"; import { sha256 } from "@noble/hashes/sha2.js"; import { jwtDecrypt, calculateJwkThumbprint, base64url, jwtVerify, EncryptJWT, SignJWT, decodeJwt } from "jose"; import { hex } from "@better-auth/utils/hex"; import { createRandomStringGenerator } from "@better-auth/utils/random"; import "@better-auth/utils"; import { createHash } from "@better-auth/utils/hash"; import { base64Url } from "@better-auth/utils/base64"; import { binary } from "@better-auth/utils/binary"; import { createHMAC } from "@better-auth/utils/hmac"; import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"; import { hexToBytes as hexToBytes$1, managedNonce, utf8ToBytes, bytesToHex } from "@noble/ciphers/utils.js"; import { SocialProviderListEnum, socialProviders } from "@better-auth/core/social-providers"; import { JWTExpired } from "jose/errors"; import defu$1, { createDefu, defu } from "defu"; import { scryptAsync } from "@noble/hashes/scrypt.js"; import { hexToBytes } from "@noble/hashes/utils.js"; import { createTelemetry } from "@better-auth/telemetry"; import { count, desc, asc, inArray, notInArray, like, lt, lte, ne, gt, gte, eq, and, or, sql as sql$1 } from "drizzle-orm"; import { b as private_env } from "./shared-server.js"; import "./url.js"; import { getRequestEvent } from "@sveltejs/kit/internal/server"; import "./root.js"; import "./utils.js"; import "@sveltejs/kit"; import "@sveltejs/kit/internal"; import "./query.js"; import { d as db, c as team, t as teamMember } from "./index2.js"; import { betterFetch } from "@better-fetch/fetch"; import { createAuthorizationURL, validateAuthorizationCode, refreshAccessToken } from "@better-auth/core/oauth2"; import "@better-auth/utils/otp"; function checkHasPath(url) { try { return (new URL(url).pathname.replace(/\/+$/, "") || "/") !== "/"; } catch { throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`); } } function assertHasProtocol(url) { try { const parsedUrl = new URL(url); if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new BetterAuthError(`Invalid base URL: ${url}. URL must include 'http://' or 'https://'`); } catch (error2) { if (error2 instanceof BetterAuthError) throw error2; throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`, { cause: error2 }); } } function withPath(url, path = "/api/auth") { assertHasProtocol(url); if (checkHasPath(url)) return url; const trimmedUrl = url.replace(/\/+$/, ""); if (!path || path === "/") return trimmedUrl; path = path.startsWith("/") ? path : `/${path}`; return `${trimmedUrl}${path}`; } function validateProxyHeader(header, type) { if (!header || header.trim() === "") return false; if (type === "proto") return header === "http" || header === "https"; if (type === "host") { if ([ /\.\./, /\0/, /[\s]/, /^[.]/, /[<>'"]/, /javascript:/i, /file:/i, /data:/i ].some((pattern) => pattern.test(header))) return false; return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?$/.test(header) || /^(\d{1,3}\.){3}\d{1,3}(:[0-9]{1,5})?$/.test(header) || /^\[[0-9a-fA-F:]+\](:[0-9]{1,5})?$/.test(header) || /^localhost(:[0-9]{1,5})?$/i.test(header); } return false; } function getBaseURL(url, path, request, loadEnv, trustedProxyHeaders) { if (url) return withPath(url, path); { const fromEnv = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_BETTER_AUTH_URL || env.PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_AUTH_URL || (env.BASE_URL !== "/" ? env.BASE_URL : void 0); if (fromEnv) return withPath(fromEnv, path); } const fromRequest = request?.headers.get("x-forwarded-host"); const fromRequestProto = request?.headers.get("x-forwarded-proto"); if (fromRequest && fromRequestProto && trustedProxyHeaders) { if (validateProxyHeader(fromRequestProto, "proto") && validateProxyHeader(fromRequest, "host")) try { return withPath(`${fromRequestProto}://${fromRequest}`, path); } catch (_error) { } } if (request) { const url$1 = getOrigin(request.url); if (!url$1) throw new BetterAuthError("Could not get origin from request. Please provide a valid base URL."); return withPath(url$1, path); } if (typeof window !== "undefined" && window.location) return withPath(window.location.origin, path); } function getOrigin(url) { try { const parsedUrl = new URL(url); return parsedUrl.origin === "null" ? null : parsedUrl.origin; } catch { return null; } } function getProtocol(url) { try { return new URL(url).protocol; } catch { return null; } } function getHost(url) { try { return new URL(url).host; } catch { return null; } } const LOCALHOST_IP = "127.0.0.1"; function getIp(req, options) { if (options.advanced?.ipAddress?.disableIpTracking) return null; const headers = "headers" in req ? req.headers : req; const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || ["x-forwarded-for"]; for (const key of ipHeaders) { const value = "get" in headers ? headers.get(key) : headers[key]; if (typeof value === "string") { const ip = value.split(",")[0].trim(); if (isValidIP(ip)) return normalizeIP(ip, { ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet }); } } if (isTest() || isDevelopment()) return LOCALHOST_IP; return null; } const { get: getOAuthState, set: setOAuthState } = defineRequestState(() => null); function escapeRegExpChar(char) { if (char === "-" || char === "^" || char === "$" || char === "+" || char === "." || char === "(" || char === ")" || char === "|" || char === "[" || char === "]" || char === "{" || char === "}" || char === "*" || char === "?" || char === "\\") return `\\${char}`; else return char; } function escapeRegExpString(str) { let result = ""; for (let i = 0; i < str.length; i++) result += escapeRegExpChar(str[i]); return result; } function transform(pattern, separator = true) { if (Array.isArray(pattern)) return `(?:${pattern.map((p) => `^${transform(p, separator)}$`).join("|")})`; let separatorSplitter = ""; let separatorMatcher = ""; let wildcard = "."; if (separator === true) { separatorSplitter = "/"; separatorMatcher = "[/\\\\]"; wildcard = "[^/\\\\]"; } else if (separator) { separatorSplitter = separator; separatorMatcher = escapeRegExpString(separatorSplitter); if (separatorMatcher.length > 1) { separatorMatcher = `(?:${separatorMatcher})`; wildcard = `((?!${separatorMatcher}).)`; } else wildcard = `[^${separatorMatcher}]`; } const requiredSeparator = separator ? `${separatorMatcher}+?` : ""; const optionalSeparator = separator ? `${separatorMatcher}*?` : ""; const segments = separator ? pattern.split(separatorSplitter) : [pattern]; let result = ""; for (let s = 0; s < segments.length; s++) { const segment = segments[s]; const nextSegment = segments[s + 1]; let currentSeparator = ""; if (!segment && s > 0) continue; if (separator) if (s === segments.length - 1) currentSeparator = optionalSeparator; else if (nextSegment !== "**") currentSeparator = requiredSeparator; else currentSeparator = ""; if (separator && segment === "**") { if (currentSeparator) { result += s === 0 ? "" : currentSeparator; result += `(?:${wildcard}*?${currentSeparator})*?`; } continue; } for (let c = 0; c < segment.length; c++) { const char = segment[c]; if (char === "\\") { if (c < segment.length - 1) { result += escapeRegExpChar(segment[c + 1]); c++; } } else if (char === "?") result += wildcard; else if (char === "*") result += `${wildcard}*?`; else result += escapeRegExpChar(char); } result += currentSeparator; } return result; } function isMatch(regexp, sample) { if (typeof sample !== "string") throw new TypeError(`Sample must be a string, but ${typeof sample} given`); return regexp.test(sample); } function wildcardMatch(pattern, options) { if (typeof pattern !== "string" && !Array.isArray(pattern)) throw new TypeError(`The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`); if (typeof options === "string" || typeof options === "boolean") options = { separator: options }; if (arguments.length === 2 && !(typeof options === "undefined" || typeof options === "object" && options !== null && !Array.isArray(options))) throw new TypeError(`The second argument must be an options object or a string/boolean separator, but ${typeof options} given`); options = options || {}; if (options.separator === "\\") throw new Error("\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead"); const regexpPattern = transform(pattern, options.separator); const regexp = new RegExp(`^${regexpPattern}$`, options.flags); const fn = isMatch.bind(null, regexp); fn.options = options; fn.pattern = pattern; fn.regexp = regexp; return fn; } const matchesOriginPattern = (url, pattern, settings) => { if (url.startsWith("/")) { if (settings?.allowRelativePaths) return url.startsWith("/") && /^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(url); return false; } if (pattern.includes("*") || pattern.includes("?")) { if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url); const host = getHost(url); if (!host) return false; return wildcardMatch(pattern)(host); } const protocol = getProtocol(url); return protocol === "http:" || protocol === "https:" || !protocol ? pattern === getOrigin(url) : url.startsWith(pattern); }; function shouldSkipCSRFForBackwardCompat(ctx) { return ctx.context.skipOriginCheck === true && ctx.context.options.advanced?.disableCSRFCheck === void 0; } const logBackwardCompatWarning = deprecate(function logBackwardCompatWarning$1() { }, "disableOriginCheck: true currently also disables CSRF checks. In a future version, disableOriginCheck will ONLY disable URL validation. To keep CSRF disabled, add disableCSRFCheck: true to your config."); const originCheckMiddleware = createAuthMiddleware(async (ctx) => { if (ctx.request?.method === "GET" || ctx.request?.method === "OPTIONS" || ctx.request?.method === "HEAD" || !ctx.request) return; await validateOrigin(ctx); if (ctx.context.skipOriginCheck) return; const { body, query } = ctx; const callbackURL = body?.callbackURL || query?.callbackURL; const redirectURL = body?.redirectTo; const errorCallbackURL = body?.errorCallbackURL; const newUserCallbackURL = body?.newUserCallbackURL; const validateURL = (url, label) => { if (!url) return; if (!ctx.context.isTrustedOrigin(url, { allowRelativePaths: label !== "origin" })) { ctx.context.logger.error(`Invalid ${label}: ${url}`); ctx.context.logger.info(`If it's a valid URL, please add ${url} to trustedOrigins in your auth config `, `Current list of trustedOrigins: ${ctx.context.trustedOrigins}`); throw new APIError("FORBIDDEN", { message: `Invalid ${label}` }); } }; callbackURL && validateURL(callbackURL, "callbackURL"); redirectURL && validateURL(redirectURL, "redirectURL"); errorCallbackURL && validateURL(errorCallbackURL, "errorCallbackURL"); newUserCallbackURL && validateURL(newUserCallbackURL, "newUserCallbackURL"); }); const originCheck = (getValue) => createAuthMiddleware(async (ctx) => { if (!ctx.request) return; if (ctx.context.skipOriginCheck) return; const callbackURL = getValue(ctx); const validateURL = (url, label) => { if (!url) return; if (!ctx.context.isTrustedOrigin(url, { allowRelativePaths: label !== "origin" })) { ctx.context.logger.error(`Invalid ${label}: ${url}`); ctx.context.logger.info(`If it's a valid URL, please add ${url} to trustedOrigins in your auth config `, `Current list of trustedOrigins: ${ctx.context.trustedOrigins}`); throw new APIError("FORBIDDEN", { message: `Invalid ${label}` }); } }; const callbacks = Array.isArray(callbackURL) ? callbackURL : [callbackURL]; for (const url of callbacks) validateURL(url, "callbackURL"); }); async function validateOrigin(ctx, forceValidate = false) { const headers = ctx.request?.headers; if (!headers || !ctx.request) return; const originHeader = headers.get("origin") || headers.get("referer") || ""; const useCookies = headers.has("cookie"); if (ctx.context.skipCSRFCheck) return; if (shouldSkipCSRFForBackwardCompat(ctx)) { ctx.context.options.advanced?.disableOriginCheck === true && logBackwardCompatWarning(); return; } const skipOriginCheck = ctx.context.skipOriginCheck; if (Array.isArray(skipOriginCheck)) try { const basePath = new URL(ctx.context.baseURL).pathname; const currentPath = normalizePathname(ctx.request.url, basePath); if (skipOriginCheck.some((skipPath) => currentPath.startsWith(skipPath))) return; } catch { } if (!(forceValidate || useCookies)) return; if (!originHeader || originHeader === "null") throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.MISSING_OR_NULL_ORIGIN }); const trustedOrigins = Array.isArray(ctx.context.options.trustedOrigins) ? ctx.context.trustedOrigins : [...ctx.context.trustedOrigins, ...(await ctx.context.options.trustedOrigins?.(ctx.request))?.filter((v) => Boolean(v)) || []]; if (!trustedOrigins.some((origin) => matchesOriginPattern(originHeader, origin))) { ctx.context.logger.error(`Invalid origin: ${originHeader}`); ctx.context.logger.info(`If it's a valid URL, please add ${originHeader} to trustedOrigins in your auth config `, `Current list of trustedOrigins: ${trustedOrigins}`); throw new APIError("FORBIDDEN", { message: "Invalid origin" }); } } const formCsrfMiddleware = createAuthMiddleware(async (ctx) => { if (!ctx.request) return; await validateFormCsrf(ctx); }); async function validateFormCsrf(ctx) { const req = ctx.request; if (!req) return; if (ctx.context.skipCSRFCheck) return; if (shouldSkipCSRFForBackwardCompat(ctx)) return; const headers = req.headers; if (headers.has("cookie")) return await validateOrigin(ctx); const site = headers.get("Sec-Fetch-Site"); const mode = headers.get("Sec-Fetch-Mode"); const dest = headers.get("Sec-Fetch-Dest"); if (Boolean(site && site.trim() || mode && mode.trim() || dest && dest.trim())) { if (site === "cross-site" && mode === "navigate") { ctx.context.logger.error("Blocked cross-site navigation login attempt (CSRF protection)", { secFetchSite: site, secFetchMode: mode, secFetchDest: dest }); throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.CROSS_SITE_NAVIGATION_LOGIN_BLOCKED }); } return await validateOrigin(ctx, true); } } const memory = /* @__PURE__ */ new Map(); function shouldRateLimit(max, window2, rateLimitData) { const now2 = Date.now(); const windowInMs = window2 * 1e3; return now2 - rateLimitData.lastRequest < windowInMs && rateLimitData.count >= max; } function rateLimitResponse(retryAfter) { return new Response(JSON.stringify({ message: "Too many requests. Please try again later." }), { status: 429, statusText: "Too Many Requests", headers: { "X-Retry-After": retryAfter.toString() } }); } function getRetryAfter(lastRequest, window2) { const now2 = Date.now(); const windowInMs = window2 * 1e3; return Math.ceil((lastRequest + windowInMs - now2) / 1e3); } function createDatabaseStorageWrapper(ctx) { const model = "rateLimit"; const db2 = ctx.adapter; return { get: async (key) => { const data = (await db2.findMany({ model, where: [{ field: "key", value: key }] }))[0]; if (typeof data?.lastRequest === "bigint") data.lastRequest = Number(data.lastRequest); return data; }, set: async (key, value, _update) => { try { if (_update) await db2.updateMany({ model, where: [{ field: "key", value: key }], update: { count: value.count, lastRequest: value.lastRequest } }); else await db2.create({ model, data: { key, count: value.count, lastRequest: value.lastRequest } }); } catch (e) { ctx.logger.error("Error setting rate limit", e); } } }; } function getRateLimitStorage(ctx, rateLimitSettings) { if (ctx.options.rateLimit?.customStorage) return ctx.options.rateLimit.customStorage; const storage = ctx.rateLimit.storage; if (storage === "secondary-storage") return { get: async (key) => { const data = await ctx.options.secondaryStorage?.get(key); return data ? safeJSONParse(data) : null; }, set: async (key, value, _update) => { const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10; await ctx.options.secondaryStorage?.set?.(key, JSON.stringify(value), ttl); } }; else if (storage === "memory") return { async get(key) { const entry = memory.get(key); if (!entry) return null; if (Date.now() >= entry.expiresAt) { memory.delete(key); return null; } return entry.data; }, async set(key, value, _update) { const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10; const expiresAt = Date.now() + ttl * 1e3; memory.set(key, { data: value, expiresAt }); } }; return createDatabaseStorageWrapper(ctx); } async function onRequestRateLimit(req, ctx) { if (!ctx.rateLimit.enabled) return; const basePath = new URL(ctx.baseURL).pathname; const path = normalizePathname(req.url, basePath); let currentWindow = ctx.rateLimit.window; let currentMax = ctx.rateLimit.max; const ip = getIp(req, ctx.options); if (!ip) return; const key = createRateLimitKey(ip, path); const specialRule = getDefaultSpecialRules().find((rule) => rule.pathMatcher(path)); if (specialRule) { currentWindow = specialRule.window; currentMax = specialRule.max; } for (const plugin of ctx.options.plugins || []) if (plugin.rateLimit) { const matchedRule = plugin.rateLimit.find((rule) => rule.pathMatcher(path)); if (matchedRule) { currentWindow = matchedRule.window; currentMax = matchedRule.max; break; } } if (ctx.rateLimit.customRules) { const _path = Object.keys(ctx.rateLimit.customRules).find((p) => { if (p.includes("*")) return wildcardMatch(p)(path); return p === path; }); if (_path) { const customRule = ctx.rateLimit.customRules[_path]; const resolved = typeof customRule === "function" ? await customRule(req, { window: currentWindow, max: currentMax }) : customRule; if (resolved) { currentWindow = resolved.window; currentMax = resolved.max; } if (resolved === false) return; } } const storage = getRateLimitStorage(ctx, { window: currentWindow }); const data = await storage.get(key); const now2 = Date.now(); if (!data) await storage.set(key, { key, count: 1, lastRequest: now2 }); else { const timeSinceLastRequest = now2 - data.lastRequest; if (shouldRateLimit(currentMax, currentWindow, data)) return rateLimitResponse(getRetryAfter(data.lastRequest, currentWindow)); else if (timeSinceLastRequest > currentWindow * 1e3) await storage.set(key, { ...data, count: 1, lastRequest: now2 }, true); else await storage.set(key, { ...data, count: data.count + 1, lastRequest: now2 }, true); } } function getDefaultSpecialRules() { return [{ pathMatcher(path) { return path.startsWith("/sign-in") || path.startsWith("/sign-up") || path.startsWith("/change-password") || path.startsWith("/change-email"); }, window: 10, max: 3 }]; } const getDate = (span, unit = "ms") => { return new Date(Date.now() + (unit === "sec" ? span * 1e3 : span)); }; const cache = /* @__PURE__ */ new WeakMap(); function parseOutputData(data, schema2) { const fields = schema2.fields; const parsedData = {}; for (const key in data) { const field = fields[key]; if (!field) { parsedData[key] = data[key]; continue; } if (field.returned === false && key !== "id") continue; parsedData[key] = data[key]; } return parsedData; } function getFields(options, table, mode) { const cacheKey = `${table}:${mode}`; if (!cache.has(options)) cache.set(options, /* @__PURE__ */ new Map()); const tableCache = cache.get(options); if (tableCache.has(cacheKey)) return tableCache.get(cacheKey); const coreSchema = mode === "output" ? getAuthTables(options)[table]?.fields ?? {} : {}; const additionalFields = table === "user" || table === "session" || table === "account" ? options[table]?.additionalFields : void 0; let schema2 = { ...coreSchema, ...additionalFields ?? {} }; for (const plugin of options.plugins || []) if (plugin.schema && plugin.schema[table]) schema2 = { ...schema2, ...plugin.schema[table].fields }; tableCache.set(cacheKey, schema2); return schema2; } function parseUserOutput(options, user) { return parseOutputData(user, { fields: getFields(options, "user", "output") }); } function parseSessionOutput(options, session) { return parseOutputData(session, { fields: getFields(options, "session", "output") }); } function parseAccountOutput(options, account) { const { accessToken: _accessToken, refreshToken: _refreshToken, idToken: _idToken, accessTokenExpiresAt: _accessTokenExpiresAt, refreshTokenExpiresAt: _refreshTokenExpiresAt, password: _password, ...rest } = parseOutputData(account, { fields: getFields(options, "account", "output") }); return rest; } function parseInputData(data, schema2) { const action = schema2.action || "create"; const fields = schema2.fields; const parsedData = Object.assign(/* @__PURE__ */ Object.create(null), null); for (const key in fields) { if (key in data) { if (fields[key].input === false) { if (fields[key].defaultValue !== void 0) { if (action !== "update") { parsedData[key] = fields[key].defaultValue; continue; } } if (data[key]) throw new APIError("BAD_REQUEST", { message: `${key} is not allowed to be set` }); continue; } if (fields[key].validator?.input && data[key] !== void 0) { const result = fields[key].validator.input["~standard"].validate(data[key]); if (result instanceof Promise) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Async validation is not supported for additional fields" }); if ("issues" in result && result.issues) throw new APIError("BAD_REQUEST", { message: result.issues[0]?.message || "Validation Error" }); parsedData[key] = result.value; continue; } if (fields[key].transform?.input && data[key] !== void 0) { parsedData[key] = fields[key].transform?.input(data[key]); continue; } parsedData[key] = data[key]; continue; } if (fields[key].defaultValue !== void 0 && action === "create") { if (typeof fields[key].defaultValue === "function") { parsedData[key] = fields[key].defaultValue(); continue; } parsedData[key] = fields[key].defaultValue; continue; } if (fields[key].required && action === "create") throw new APIError("BAD_REQUEST", { message: `${key} is required` }); } return parsedData; } function parseUserInput(options, user = {}, action) { return parseInputData(user, { fields: getFields(options, "user", "input"), action }); } function parseAdditionalUserInput(options, user) { const schema2 = getFields(options, "user", "input"); return parseInputData(user || {}, { fields: schema2 }); } function parseAccountInput(options, account) { return parseInputData(account, { fields: getFields(options, "account", "input") }); } function parseSessionInput(options, session) { return parseInputData(session, { fields: getFields(options, "session", "input") }); } function mergeSchema(schema2, newSchema) { if (!newSchema) return schema2; for (const table in newSchema) { const newModelName = newSchema[table]?.modelName; if (newModelName) schema2[table].modelName = newModelName; for (const field in schema2[table].fields) { const newField = newSchema[table]?.fields?.[field]; if (!newField) continue; schema2[table].fields[field].fieldName = newField; } } return schema2; } var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (all, symbols) => { let target = {}; for (var name in all) { __defProp(target, name, { get: all[name], enumerable: true }); } return target; }; var __copyProps = (to, from, except, desc2) => { if (from && typeof from === "object" || typeof from === "function") { for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) { __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable }); } } } return to; }; var __reExport = (target, mod, secondTarget, symbols) => { __copyProps(target, mod, "default"); }; async function getBaseAdapter(options, handleDirectDatabase) { let adapter; if (!options.database) { const tables = getAuthTables(options); const memoryDB = Object.keys(tables).reduce((acc, key) => { acc[key] = []; return acc; }, {}); const { memoryAdapter } = await import("./index5.js"); adapter = memoryAdapter(memoryDB)(options); } else if (typeof options.database === "function") adapter = options.database(options); else adapter = await handleDirectDatabase(options); if (!adapter.transaction) { logger.warn("Adapter does not correctly implement transaction function, patching it automatically. Please update your adapter implementation."); adapter.transaction = async (cb) => { return cb(adapter); }; } return adapter; } async function getAdapter(options) { return getBaseAdapter(options, async (opts) => { const { createKyselyAdapter: createKyselyAdapter2 } = await import("./index6.js"); const { kysely, databaseType, transaction } = await createKyselyAdapter2(opts); if (!kysely) throw new BetterAuthError("Failed to initialize database adapter"); const { kyselyAdapter } = await import("./index6.js"); return kyselyAdapter(kysely, { type: databaseType || "sqlite", debugLogs: opts.database && "debugLogs" in opts.database ? opts.database.debugLogs : false, transaction })(opts); }); } const createFieldAttribute = (type, config2) => { return { type, ...config2 }; }; function convertToDB(fields, values) { const result = values.id ? { id: values.id } : {}; for (const key in fields) { const field = fields[key]; const value = values[key]; if (value === void 0) continue; result[field.fieldName || key] = value; } return result; } function convertFromDB(fields, values) { if (!values) return null; const result = { id: values.id }; for (const [key, value] of Object.entries(fields)) result[key] = values[value.fieldName || key]; return result; } function getWithHooks(adapter, ctx) { const hooks = ctx.hooks; async function createWithHooks(data, model, customCreateFn) { const context = await getCurrentAuthContext().catch(() => null); let actualData = data; for (const hook of hooks || []) { const toRun = hook[model]?.create?.before; if (toRun) { const result = await toRun(actualData, context); if (result === false) return null; if (typeof result === "object" && "data" in result) actualData = { ...actualData, ...result.data }; } } const customCreated = customCreateFn ? await customCreateFn.fn(actualData) : null; const created = !customCreateFn || customCreateFn.executeMainFn ? await (await getCurrentAdapter(adapter)).create({ model, data: actualData, forceAllowId: true }) : customCreated; for (const hook of hooks || []) { const toRun = hook[model]?.create?.after; if (toRun) await toRun(created, context); } return created; } async function updateWithHooks(data, where, model, customUpdateFn) { const context = await getCurrentAuthContext().catch(() => null); let actualData = data; for (const hook of hooks || []) { const toRun = hook[model]?.update?.before; if (toRun) { const result = await toRun(data, context); if (result === false) return null; if (typeof result === "object" && "data" in result) actualData = { ...actualData, ...result.data }; } } const customUpdated = customUpdateFn ? await customUpdateFn.fn(actualData) : null; const updated = !customUpdateFn || customUpdateFn.executeMainFn ? await (await getCurrentAdapter(adapter)).update({ model, update: actualData, where }) : customUpdated; for (const hook of hooks || []) { const toRun = hook[model]?.update?.after; if (toRun) await toRun(updated, context); } return updated; } async function updateManyWithHooks(data, where, model, customUpdateFn) { const context = await getCurrentAuthContext().catch(() => null); let actualData = data; for (const hook of hooks || []) { const toRun = hook[model]?.update?.before; if (toRun) { const result = await toRun(data, context); if (result === false) return null; if (typeof result === "object" && "data" in result) actualData = { ...actualData, ...result.data }; } } const customUpdated = customUpdateFn ? await customUpdateFn.fn(actualData) : null; const updated = !customUpdateFn || customUpdateFn.executeMainFn ? await (await getCurrentAdapter(adapter)).updateMany({ model, update: actualData, where }) : customUpdated; for (const hook of hooks || []) { const toRun = hook[model]?.update?.after; if (toRun) await toRun(updated, context); } return updated; } async function deleteWithHooks(where, model, customDeleteFn) { const context = await getCurrentAuthContext().catch(() => null); let entityToDelete = null; try { entityToDelete = (await (await getCurrentAdapter(adapter)).findMany({ model, where, limit: 1 }))[0] || null; } catch { } if (entityToDelete) for (const hook of hooks || []) { const toRun = hook[model]?.delete?.before; if (toRun) { if (await toRun(entityToDelete, context) === false) return null; } } const customDeleted = customDeleteFn ? await customDeleteFn.fn(where) : null; const deleted = !customDeleteFn || customDeleteFn.executeMainFn ? await (await getCurrentAdapter(adapter)).delete({ model, where }) : customDeleted; if (entityToDelete) for (const hook of hooks || []) { const toRun = hook[model]?.delete?.after; if (toRun) await toRun(entityToDelete, context); } return deleted; } async function deleteManyWithHooks(where, model, customDeleteFn) { const context = await getCurrentAuthContext().catch(() => null); let entitiesToDelete = []; try { entitiesToDelete = await (await getCurrentAdapter(adapter)).findMany({ model, where }); } catch { } for (const entity of entitiesToDelete) for (const hook of hooks || []) { const toRun = hook[model]?.delete?.before; if (toRun) { if (await toRun(entity, context) === false) return null; } } const customDeleted = customDeleteFn ? await customDeleteFn.fn(where) : null; const deleted = !customDeleteFn || customDeleteFn.executeMainFn ? await (await getCurrentAdapter(adapter)).deleteMany({ model, where }) : customDeleted; for (const entity of entitiesToDelete) for (const hook of hooks || []) { const toRun = hook[model]?.delete?.after; if (toRun) await toRun(entity, context); } return deleted; } return { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks }; } const createInternalAdapter = (adapter, ctx) => { const logger2 = ctx.logger; const options = ctx.options; const secondaryStorage = options.secondaryStorage; const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7; const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks } = getWithHooks(adapter, ctx); async function refreshUserSessions(user) { if (!secondaryStorage) return; const listRaw = await secondaryStorage.get(`active-sessions-${user.id}`); if (!listRaw) return; const now2 = Date.now(); const validSessions = (safeJSONParse(listRaw) || []).filter((s) => s.expiresAt > now2); await Promise.all(validSessions.map(async ({ token }) => { const cached = await secondaryStorage.get(token); if (!cached) return; const parsed = safeJSONParse(cached); if (!parsed) return; const sessionTTL = Math.max(Math.floor(new Date(parsed.session.expiresAt).getTime() - now2) / 1e3, 0); await secondaryStorage.set(token, JSON.stringify({ session: parsed.session, user }), Math.floor(sessionTTL)); })); } return { createOAuthUser: async (user, account) => { return runWithTransaction(adapter, async () => { const createdUser = await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...user }, "user", void 0); return { user: createdUser, account: await createWithHooks({ ...account, userId: createdUser.id, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }, "account", void 0) }; }); }, createUser: async (user) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...user, email: user.email?.toLowerCase() }, "user", void 0); }, createAccount: async (account) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...account }, "account", void 0); }, listSessions: async (userId) => { if (secondaryStorage) { const currentList = await secondaryStorage.get(`active-sessions-${userId}`); if (!currentList) return []; const list = safeJSONParse(currentList) || []; const now2 = Date.now(); const seenTokens = /* @__PURE__ */ new Set(); const sessions = []; for (const { token, expiresAt } of list) { if (expiresAt <= now2 || seenTokens.has(token)) continue; seenTokens.add(token); const data = await secondaryStorage.get(token); if (!data) continue; try { const parsed = typeof data === "string" ? JSON.parse(data) : data; if (!parsed?.session) continue; sessions.push(parseSessionOutput(ctx.options, { ...parsed.session, expiresAt: new Date(parsed.session.expiresAt) })); } catch { continue; } } return sessions; } return await (await getCurrentAdapter(adapter)).findMany({ model: "session", where: [{ field: "userId", value: userId }] }); }, listUsers: async (limit, offset, sortBy, where) => { return await (await getCurrentAdapter(adapter)).findMany({ model: "user", limit, offset, sortBy, where }); }, countTotalUsers: async (where) => { const total = await (await getCurrentAdapter(adapter)).count({ model: "user", where }); if (typeof total === "string") return parseInt(total); return total; }, deleteUser: async (userId) => { if (!secondaryStorage || options.session?.storeSessionInDatabase) await deleteManyWithHooks([{ field: "userId", value: userId }], "session", void 0); await deleteManyWithHooks([{ field: "userId", value: userId }], "account", void 0); await deleteWithHooks([{ field: "id", value: userId }], "user", void 0); }, createSession: async (userId, dontRememberMe, override, overrideAll) => { const ctx$1 = await getCurrentAuthContext().catch(() => null); const headers = ctx$1?.headers || ctx$1?.request?.headers; const { id: _, ...rest } = override || {}; const defaultAdditionalFields = parseSessionInput(ctx$1?.context.options ?? options, {}); const data = { ipAddress: ctx$1?.request || ctx$1?.headers ? getIp(ctx$1?.request || ctx$1?.headers, ctx$1?.context.options) || "" : "", userAgent: headers?.get("user-agent") || "", ...rest, expiresAt: dontRememberMe ? getDate(3600 * 24, "sec") : getDate(sessionExpiration, "sec"), userId, token: generateId(32), createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...defaultAdditionalFields, ...overrideAll ? rest : {} }; return await createWithHooks(data, "session", secondaryStorage ? { fn: async (sessionData) => { const currentList = await secondaryStorage.get(`active-sessions-${userId}`); let list = []; const now2 = Date.now(); if (currentList) { list = safeJSONParse(currentList) || []; list = list.filter((session) => session.expiresAt > now2 && session.token !== data.token); } const sorted = [...list, { token: data.token, expiresAt: data.expiresAt.getTime() }].sort((a, b) => a.expiresAt - b.expiresAt); const furthestSessionExp = sorted.at(-1)?.expiresAt ?? data.expiresAt.getTime(); const furthestSessionTTL = Math.max(Math.floor((furthestSessionExp - now2) / 1e3), 0); if (furthestSessionTTL > 0) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), furthestSessionTTL); const user = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ field: "id", value: userId }] }); const sessionTTL = Math.max(Math.floor((data.expiresAt.getTime() - now2) / 1e3), 0); if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({ session: sessionData, user }), sessionTTL); return sessionData; }, executeMainFn: options.session?.storeSessionInDatabase } : void 0); }, findSession: async (token) => { if (secondaryStorage) { const sessionStringified = await secondaryStorage.get(token); if (!sessionStringified && !options.session?.storeSessionInDatabase) return null; if (sessionStringified) { const s = safeJSONParse(sessionStringified); if (!s) return null; return { session: parseSessionOutput(ctx.options, { ...s.session, expiresAt: new Date(s.session.expiresAt), createdAt: new Date(s.session.createdAt), updatedAt: new Date(s.session.updatedAt) }), user: parseUserOutput(ctx.options, { ...s.user, createdAt: new Date(s.user.createdAt), updatedAt: new Date(s.user.updatedAt) }) }; } } const result = await (await getCurrentAdapter(adapter)).findOne({ model: "session", where: [{ value: token, field: "token" }], join: { user: true } }); if (!result) return null; const { user, ...session } = result; if (!user) return null; return { session: parseSessionOutput(ctx.options, session), user: parseUserOutput(ctx.options, user) }; }, findSessions: async (sessionTokens) => { if (secondaryStorage) { const sessions$1 = []; for (const sessionToken of sessionTokens) { const sessionStringified = await secondaryStorage.get(sessionToken); if (sessionStringified) try { const s = typeof sessionStringified === "string" ? JSON.parse(sessionStringified) : sessionStringified; if (!s?.session) continue; const session = { session: { ...s.session, expiresAt: new Date(s.session.expiresAt) }, user: { ...s.user, createdAt: new Date(s.user.createdAt), updatedAt: new Date(s.user.updatedAt) } }; sessions$1.push(session); } catch { continue; } } return sessions$1; } const sessions = await (await getCurrentAdapter(adapter)).findMany({ model: "session", where: [{ field: "token", value: sessionTokens, operator: "in" }], join: { user: true } }); if (!sessions.length) return []; if (sessions.some((session) => !session.user)) return []; return sessions.map((_session) => { const { user, ...session } = _session; return { session, user }; }); }, updateSession: async (sessionToken, session) => { return await updateWithHooks(session, [{ field: "token", value: sessionToken }], "session", secondaryStorage ? { async fn(data) { const currentSession = await secondaryStorage.get(sessionToken); if (!currentSession) return null; const parsedSession = safeJSONParse(currentSession); if (!parsedSession) return null; const mergedSession = { ...parsedSession.session, ...data, expiresAt: new Date(data.expiresAt ?? parsedSession.session.expiresAt), createdAt: new Date(parsedSession.session.createdAt), updatedAt: new Date(data.updatedAt ?? parsedSession.session.updatedAt) }; const updatedSession = parseSessionOutput(ctx.options, mergedSession); const now2 = Date.now(); const expiresMs = new Date(updatedSession.expiresAt).getTime(); const sessionTTL = Math.max(Math.floor((expiresMs - now2) / 1e3), 0); if (sessionTTL > 0) { await secondaryStorage.set(sessionToken, JSON.stringify({ session: updatedSession, user: parsedSession.user }), sessionTTL); const listKey = `active-sessions-${updatedSession.userId}`; const listRaw = await secondaryStorage.get(listKey); const sorted = (listRaw ? safeJSONParse(listRaw) || [] : []).filter((s) => s.token !== sessionToken && s.expiresAt > now2).concat([{ token: sessionToken, expiresAt: expiresMs }]).sort((a, b) => a.expiresAt - b.expiresAt); const furthestSessionExp = sorted.at(-1)?.expiresAt; if (furthestSessionExp && furthestSessionExp > now2) await secondaryStorage.set(listKey, JSON.stringify(sorted), Math.floor((furthestSessionExp - now2) / 1e3)); else await secondaryStorage.delete(listKey); } return updatedSession; }, executeMainFn: options.session?.storeSessionInDatabase } : void 0); }, deleteSession: async (token) => { if (secondaryStorage) { const data = await secondaryStorage.get(token); if (data) { const { session } = safeJSONParse(data) ?? {}; if (!session) { logger2.error("Session not found in secondary storage"); return; } const userId = session.userId; const currentList = await secondaryStorage.get(`active-sessions-${userId}`); if (currentList) { const list = safeJSONParse(currentList) || []; const now2 = Date.now(); const filtered = list.filter((session$1) => session$1.expiresAt > now2 && session$1.token !== token); const furthestSessionExp = filtered.sort((a, b) => a.expiresAt - b.expiresAt).at(-1)?.expiresAt; if (filtered.length > 0 && furthestSessionExp && furthestSessionExp > Date.now()) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(filtered), Math.floor((furthestSessionExp - now2) / 1e3)); else await secondaryStorage.delete(`active-sessions-${userId}`); } else logger2.error("Active sessions list not found in secondary storage"); } await secondaryStorage.delete(token); if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return; } await deleteWithHooks([{ field: "token", value: token }], "session", void 0); }, deleteAccounts: async (userId) => { await deleteManyWithHooks([{ field: "userId", value: userId }], "account", void 0); }, deleteAccount: async (accountId) => { await deleteWithHooks([{ field: "id", value: accountId }], "account", void 0); }, deleteSessions: async (userIdOrSessionTokens) => { if (secondaryStorage) { if (typeof userIdOrSessionTokens === "string") { const activeSession = await secondaryStorage.get(`active-sessions-${userIdOrSessionTokens}`); const sessions = activeSession ? safeJSONParse(activeSession) : []; if (!sessions) return; for (const session of sessions) await secondaryStorage.delete(session.token); await secondaryStorage.delete(`active-sessions-${userIdOrSessionTokens}`); } else for (const sessionToken of userIdOrSessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken); if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return; } await deleteManyWithHooks([{ field: Array.isArray(userIdOrSessionTokens) ? "token" : "userId", value: userIdOrSessionTokens, operator: Array.isArray(userIdOrSessionTokens) ? "in" : void 0 }], "session", void 0); }, findOAuthUser: async (email, accountId, providerId) => { const account = await (await getCurrentAdapter(adapter)).findOne({ model: "account", where: [{ value: accountId, field: "accountId" }, { value: providerId, field: "providerId" }], join: { user: true } }); if (account) if (account.user) return { user: account.user, linkedAccount: account, accounts: [account] }; else { const user = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }] }); if (user) return { user, linkedAccount: account, accounts: [account] }; return null; } else { const user = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }] }); if (user) return { user, linkedAccount: null, accounts: await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ value: user.id, field: "userId" }] }) || [] }; else return null; } }, findUserByEmail: async (email, options$1) => { const result = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }], join: { ...options$1?.includeAccounts ? { account: true } : {} } }); if (!result) return null; const { account: accounts, ...user } = result; return { user, accounts: accounts ?? [] }; }, findUserById: async (userId) => { if (!userId) return null; return await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ field: "id", value: userId }] }); }, linkAccount: async (account) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...account }, "account", void 0); }, updateUser: async (userId, data) => { const user = await updateWithHooks(data, [{ field: "id", value: userId }], "user", void 0); await refreshUserSessions(user); return user; }, updateUserByEmail: async (email, data) => { const user = await updateWithHooks(data, [{ field: "email", value: email.toLowerCase() }], "user", void 0); await refreshUserSessions(user); return user; }, updatePassword: async (userId, password) => { await updateManyWithHooks({ password }, [{ field: "userId", value: userId }, { field: "providerId", value: "credential" }], "account", void 0); }, findAccounts: async (userId) => { return await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ field: "userId", value: userId }] }); }, findAccount: async (accountId) => { return await (await getCurrentAdapter(adapter)).findOne({ model: "account", where: [{ field: "accountId", value: accountId }] }); }, findAccountByProviderId: async (accountId, providerId) => { return await (await getCurrentAdapter(adapter)).findOne({ model: "account", where: [{ field: "accountId", value: accountId }, { field: "providerId", value: providerId }] }); }, findAccountByUserId: async (userId) => { return await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ field: "userId", value: userId }] }); }, updateAccount: async (id, data) => { return await updateWithHooks(data, [{ field: "id", value: id }], "account", void 0); }, createVerificationValue: async (data) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...data }, "verification", void 0); }, findVerificationValue: async (identifier) => { const verification = await (await getCurrentAdapter(adapter)).findMany({ model: "verification", where: [{ field: "identifier", value: identifier }], sortBy: { field: "createdAt", direction: "desc" }, limit: 1 }); if (!options.verification?.disableCleanup) await deleteManyWithHooks([{ field: "expiresAt", value: /* @__PURE__ */ new Date(), operator: "lt" }], "verification", void 0); return verification[0]; }, deleteVerificationValue: async (id) => { await deleteWithHooks([{ field: "id", value: id }], "verification", void 0); }, deleteVerificationByIdentifier: async (identifier) => { await deleteWithHooks([{ field: "identifier", value: identifier }], "verification", void 0); }, updateVerificationValue: async (id, data) => { return await updateWithHooks(data, [{ field: "id", value: id }], "verification", void 0); } }; }; function toZodSchema({ fields, isClientSide }) { const zodFields = Object.keys(fields).reduce((acc, key) => { const field = fields[key]; if (!field) return acc; if (isClientSide && field.input === false) return acc; let schema2; if (field.type === "json") schema2 = z.json ? z.json() : z.any(); else if (field.type === "string[]" || field.type === "number[]") schema2 = z.array(field.type === "string[]" ? z.string() : z.number()); else if (Array.isArray(field.type)) schema2 = z.any(); else schema2 = z[field.type](); if (field?.required === false) schema2 = schema2.optional(); if (!isClientSide && field?.returned === false) return acc; return { ...acc, [key]: schema2 }; }, {}); return z.object(zodFields); } function getSchema(config2) { const tables = (0, db_exports.getAuthTables)(config2); const schema2 = {}; for (const key in tables) { const table = tables[key]; const fields = table.fields; const actualFields = {}; Object.entries(fields).forEach(([key$1, field]) => { actualFields[field.fieldName || key$1] = field; if (field.references) { const refTable = tables[field.references.model]; if (refTable) actualFields[field.fieldName || key$1].references = { ...field.references, model: refTable.modelName, field: field.references.field }; } }); if (schema2[table.modelName]) { schema2[table.modelName].fields = { ...schema2[table.modelName].fields, ...actualFields }; continue; } schema2[table.modelName] = { fields: actualFields, order: table.order || Infinity }; } return schema2; } function getKyselyDatabaseType(db2) { if (!db2) return null; if ("dialect" in db2) return getKyselyDatabaseType(db2.dialect); if ("createDriver" in db2) { if (db2 instanceof SqliteDialect) return "sqlite"; if (db2 instanceof MysqlDialect) return "mysql"; if (db2 instanceof PostgresDialect) return "postgres"; if (db2 instanceof MssqlDialect) return "mssql"; } if ("aggregate" in db2) return "sqlite"; if ("getConnection" in db2) return "mysql"; if ("connect" in db2) return "postgres"; if ("fileControl" in db2) return "sqlite"; if ("open" in db2 && "close" in db2 && "prepare" in db2) return "sqlite"; return null; } const createKyselyAdapter = async (config2) => { const db2 = config2.database; if (!db2) return { kysely: null, databaseType: null, transaction: void 0 }; if ("db" in db2) return { kysely: db2.db, databaseType: db2.type, transaction: db2.transaction }; if ("dialect" in db2) return { kysely: new Kysely({ dialect: db2.dialect }), databaseType: db2.type, transaction: db2.transaction }; let dialect = void 0; const databaseType = getKyselyDatabaseType(db2); if ("createDriver" in db2) dialect = db2; if ("aggregate" in db2 && !("createSession" in db2)) dialect = new SqliteDialect({ database: db2 }); if ("getConnection" in db2) dialect = new MysqlDialect(db2); if ("connect" in db2) dialect = new PostgresDialect({ pool: db2 }); if ("fileControl" in db2) { const { BunSqliteDialect } = await import("./bun-sqlite-dialect.js"); dialect = new BunSqliteDialect({ database: db2 }); } if ("createSession" in db2) { let DatabaseSync = void 0; try { const nodeSqlite = "node:sqlite"; ({ DatabaseSync } = await import( /* @vite-ignore */ /* webpackIgnore: true */ nodeSqlite )); } catch (error2) { if (error2 !== null && typeof error2 === "object" && "code" in error2 && error2.code !== "ERR_UNKNOWN_BUILTIN_MODULE") throw error2; } if (DatabaseSync && db2 instanceof DatabaseSync) { const { NodeSqliteDialect } = await import("./node-sqlite-dialect.js"); dialect = new NodeSqliteDialect({ database: db2 }); } } return { kysely: dialect ? new Kysely({ dialect }) : null, databaseType, transaction: void 0 }; }; const map = { postgres: { string: [ "character varying", "varchar", "text", "uuid" ], number: [ "int4", "integer", "bigint", "smallint", "numeric", "real", "double precision" ], boolean: ["bool", "boolean"], date: [ "timestamptz", "timestamp", "date" ], json: ["json", "jsonb"] }, mysql: { string: [ "varchar", "text", "uuid" ], number: [ "integer", "int", "bigint", "smallint", "decimal", "float", "double" ], boolean: ["boolean", "tinyint"], date: [ "timestamp", "datetime", "date" ], json: ["json"] }, sqlite: { string: ["TEXT"], number: ["INTEGER", "REAL"], boolean: ["INTEGER", "BOOLEAN"], date: ["DATE", "INTEGER"], json: ["TEXT"] }, mssql: { string: [ "varchar", "nvarchar", "uniqueidentifier" ], number: [ "int", "bigint", "smallint", "decimal", "float", "double" ], boolean: ["bit", "smallint"], date: [ "datetime2", "date", "datetime" ], json: ["varchar", "nvarchar"] } }; function matchType(columnDataType, fieldType, dbType) { function normalize(type) { return type.toLowerCase().split("(")[0].trim(); } if (fieldType === "string[]" || fieldType === "number[]") return columnDataType.toLowerCase().includes("json"); const types2 = map[dbType]; return (Array.isArray(fieldType) ? types2["string"].map((t) => t.toLowerCase()) : types2[fieldType].map((t) => t.toLowerCase())).includes(normalize(columnDataType)); } async function getPostgresSchema(db2) { try { const result = await sql`SHOW search_path`.execute(db2); if (result.rows[0]?.search_path) return result.rows[0].search_path.split(",").map((s) => s.trim()).map((s) => s.replace(/^["']|["']$/g, "")).filter((s) => !s.startsWith("$"))[0] || "public"; } catch { } return "public"; } async function getMigrations(config2) { const betterAuthSchema = getSchema(config2); const logger$1 = createLogger(config2.logger); let { kysely: db2, databaseType: dbType } = await createKyselyAdapter(config2); if (!dbType) { logger$1.warn("Could not determine database type, defaulting to sqlite. Please provide a type in the database options to avoid this."); dbType = "sqlite"; } if (!db2) { logger$1.error("Only kysely adapter is supported for migrations. You can use `generate` command to generate the schema, if you're using a different adapter."); process.exit(1); } let currentSchema = "public"; if (dbType === "postgres") { currentSchema = await getPostgresSchema(db2); logger$1.debug(`PostgreSQL migration: Using schema '${currentSchema}' (from search_path)`); try { if (!(await sql` SELECT schema_name FROM information_schema.schemata WHERE schema_name = ${currentSchema} `.execute(db2)).rows[0]) logger$1.warn(`Schema '${currentSchema}' does not exist. Tables will be inspected from available schemas. Consider creating the schema first or checking your database configuration.`); } catch (error2) { logger$1.debug(`Could not verify schema existence: ${error2 instanceof Error ? error2.message : String(error2)}`); } } const allTableMetadata = await db2.introspection.getTables(); let tableMetadata = allTableMetadata; if (dbType === "postgres") try { const tablesInSchema = await sql` SELECT table_name FROM information_schema.tables WHERE table_schema = ${currentSchema} AND table_type = 'BASE TABLE' `.execute(db2); const tableNamesInSchema = new Set(tablesInSchema.rows.map((row) => row.table_name)); tableMetadata = allTableMetadata.filter((table) => table.schema === currentSchema && tableNamesInSchema.has(table.name)); logger$1.debug(`Found ${tableMetadata.length} table(s) in schema '${currentSchema}': ${tableMetadata.map((t) => t.name).join(", ") || "(none)"}`); } catch (error2) { logger$1.warn(`Could not filter tables by schema. Using all discovered tables. Error: ${error2 instanceof Error ? error2.message : String(error2)}`); } const toBeCreated = []; const toBeAdded = []; for (const [key, value] of Object.entries(betterAuthSchema)) { const table = tableMetadata.find((t) => t.name === key); if (!table) { const tIndex = toBeCreated.findIndex((t) => t.table === key); const tableData = { table: key, fields: value.fields, order: value.order || Infinity }; const insertIndex = toBeCreated.findIndex((t) => (t.order || Infinity) > tableData.order); if (insertIndex === -1) if (tIndex === -1) toBeCreated.push(tableData); else toBeCreated[tIndex].fields = { ...toBeCreated[tIndex].fields, ...value.fields }; else toBeCreated.splice(insertIndex, 0, tableData); continue; } const toBeAddedFields = {}; for (const [fieldName, field] of Object.entries(value.fields)) { const column = table.columns.find((c) => c.name === fieldName); if (!column) { toBeAddedFields[fieldName] = field; continue; } if (matchType(column.dataType, field.type, dbType)) continue; else logger$1.warn(`Field ${fieldName} in table ${key} has a different type in the database. Expected ${field.type} but got ${column.dataType}.`); } if (Object.keys(toBeAddedFields).length > 0) toBeAdded.push({ table: key, fields: toBeAddedFields, order: value.order || Infinity }); } const migrations = []; const useUUIDs = config2.advanced?.database?.generateId === "uuid"; const useNumberId = config2.advanced?.database?.useNumberId || config2.advanced?.database?.generateId === "serial"; function getType(field, fieldName) { const type = field.type; const provider = dbType || "sqlite"; const typeMap = { string: { sqlite: "text", postgres: "text", mysql: field.unique ? "varchar(255)" : field.references ? "varchar(36)" : field.sortable ? "varchar(255)" : field.index ? "varchar(255)" : "text", mssql: field.unique || field.sortable ? "varchar(255)" : field.references ? "varchar(36)" : "varchar(8000)" }, boolean: { sqlite: "integer", postgres: "boolean", mysql: "boolean", mssql: "smallint" }, number: { sqlite: field.bigint ? "bigint" : "integer", postgres: field.bigint ? "bigint" : "integer", mysql: field.bigint ? "bigint" : "integer", mssql: field.bigint ? "bigint" : "integer" }, date: { sqlite: "date", postgres: "timestamptz", mysql: "timestamp(3)", mssql: sql`datetime2(3)` }, json: { sqlite: "text", postgres: "jsonb", mysql: "json", mssql: "varchar(8000)" }, id: { postgres: useNumberId ? sql`integer GENERATED BY DEFAULT AS IDENTITY` : useUUIDs ? "uuid" : "text", mysql: useNumberId ? "integer" : useUUIDs ? "varchar(36)" : "varchar(36)", mssql: useNumberId ? "integer" : useUUIDs ? "varchar(36)" : "varchar(36)", sqlite: useNumberId ? "integer" : "text" }, foreignKeyId: { postgres: useNumberId ? "integer" : useUUIDs ? "uuid" : "text", mysql: useNumberId ? "integer" : useUUIDs ? "varchar(36)" : "varchar(36)", mssql: useNumberId ? "integer" : useUUIDs ? "varchar(36)" : "varchar(36)", sqlite: useNumberId ? "integer" : "text" }, "string[]": { sqlite: "text", postgres: "jsonb", mysql: "json", mssql: "varchar(8000)" }, "number[]": { sqlite: "text", postgres: "jsonb", mysql: "json", mssql: "varchar(8000)" } }; if (fieldName === "id" || field.references?.field === "id") { if (fieldName === "id") return typeMap.id[provider]; return typeMap.foreignKeyId[provider]; } if (Array.isArray(type)) return "text"; if (!(type in typeMap)) throw new Error(`Unsupported field type '${String(type)}' for field '${fieldName}'. Allowed types are: string, number, boolean, date, string[], number[]. If you need to store structured data, store it as a JSON string (type: "string") or split it into primitive fields. See https://better-auth.com/docs/advanced/schema#additional-fields`); return typeMap[type][provider]; } const getModelName = initGetModelName({ schema: getAuthTables(config2), usePlural: false }); const getFieldName = initGetFieldName({ schema: getAuthTables(config2), usePlural: false }); function getReferencePath(model, field) { try { return `${getModelName(model)}.${getFieldName({ model, field })}`; } catch { return `${model}.${field}`; } } if (toBeAdded.length) for (const table of toBeAdded) for (const [fieldName, field] of Object.entries(table.fields)) { const type = getType(field, fieldName); const builder = db2.schema.alterTable(table.table); if (field.index) { const index = db2.schema.alterTable(table.table).addIndex(`${table.table}_${fieldName}_idx`); migrations.push(index); } const built = builder.addColumn(fieldName, type, (col) => { col = field.required !== false ? col.notNull() : col; if (field.references) col = col.references(getReferencePath(field.references.model, field.references.field)).onDelete(field.references.onDelete || "cascade"); if (field.unique) col = col.unique(); if (field.type === "date" && typeof field.defaultValue === "function" && (dbType === "postgres" || dbType === "mysql" || dbType === "mssql")) if (dbType === "mysql") col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`); else col = col.defaultTo(sql`CURRENT_TIMESTAMP`); return col; }); migrations.push(built); } const toBeIndexed = []; if (config2.advanced?.database?.useNumberId) logger$1.warn("`useNumberId` is deprecated. Please use `generateId` with `serial` instead."); if (toBeCreated.length) for (const table of toBeCreated) { const idType = getType({ type: useNumberId ? "number" : "string" }, "id"); let dbT = db2.schema.createTable(table.table).addColumn("id", idType, (col) => { if (useNumberId) { if (dbType === "postgres") return col.primaryKey().notNull(); else if (dbType === "sqlite") return col.primaryKey().notNull(); else if (dbType === "mssql") return col.identity().primaryKey().notNull(); return col.autoIncrement().primaryKey().notNull(); } if (useUUIDs) { if (dbType === "postgres") return col.primaryKey().defaultTo(sql`pg_catalog.gen_random_uuid()`).notNull(); return col.primaryKey().notNull(); } return col.primaryKey().notNull(); }); for (const [fieldName, field] of Object.entries(table.fields)) { const type = getType(field, fieldName); dbT = dbT.addColumn(fieldName, type, (col) => { col = field.required !== false ? col.notNull() : col; if (field.references) col = col.references(getReferencePath(field.references.model, field.references.field)).onDelete(field.references.onDelete || "cascade"); if (field.unique) col = col.unique(); if (field.type === "date" && typeof field.defaultValue === "function" && (dbType === "postgres" || dbType === "mysql" || dbType === "mssql")) if (dbType === "mysql") col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`); else col = col.defaultTo(sql`CURRENT_TIMESTAMP`); return col; }); if (field.index) { const builder = db2.schema.createIndex(`${table.table}_${fieldName}_${field.unique ? "uidx" : "idx"}`).on(table.table).columns([fieldName]); toBeIndexed.push(field.unique ? builder.unique() : builder); } } migrations.push(dbT); } if (toBeIndexed.length) for (const index of toBeIndexed) migrations.push(index); async function runMigrations() { for (const migration of migrations) await migration.execute(); } async function compileMigrations() { return migrations.map((m) => m.compile().sql).join(";\n\n") + ";"; } return { toBeCreated, toBeAdded, runMigrations, compileMigrations }; } var db_exports = /* @__PURE__ */ __export({ convertFromDB: () => convertFromDB, convertToDB: () => convertToDB, createFieldAttribute: () => createFieldAttribute, createInternalAdapter: () => createInternalAdapter, getAdapter: () => getAdapter, getBaseAdapter: () => getBaseAdapter, getMigrations: () => getMigrations, getSchema: () => getSchema, getWithHooks: () => getWithHooks, matchType: () => matchType, mergeSchema: () => mergeSchema, parseAccountInput: () => parseAccountInput, parseAccountOutput: () => parseAccountOutput, parseAdditionalUserInput: () => parseAdditionalUserInput, parseInputData: () => parseInputData, parseSessionInput: () => parseSessionInput, parseSessionOutput: () => parseSessionOutput, parseUserInput: () => parseUserInput, parseUserOutput: () => parseUserOutput, toZodSchema: () => toZodSchema }); __reExport(db_exports, import___better_auth_core_db); async function signJWT(payload, secret, expiresIn = 3600) { return await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn).sign(new TextEncoder().encode(secret)); } async function verifyJWT(token, secret) { try { return (await jwtVerify(token, new TextEncoder().encode(secret))).payload; } catch { return null; } } const info = new Uint8Array([ 66, 101, 116, 116, 101, 114, 65, 117, 116, 104, 46, 106, 115, 32, 71, 101, 110, 101, 114, 97, 116, 101, 100, 32, 69, 110, 99, 114, 121, 112, 116, 105, 111, 110, 32, 75, 101, 121 ]); const now = () => Date.now() / 1e3 | 0; const alg = "dir"; const enc = "A256CBC-HS512"; async function symmetricEncodeJWT(payload, secret, salt, expiresIn = 3600) { const encryptionSecret = hkdf(sha256, new TextEncoder().encode(secret), new TextEncoder().encode(salt), info, 64); const thumbprint = await calculateJwkThumbprint({ kty: "oct", k: base64url.encode(encryptionSecret) }, "sha256"); return await new EncryptJWT(payload).setProtectedHeader({ alg, enc, kid: thumbprint }).setIssuedAt().setExpirationTime(now() + expiresIn).setJti(crypto.randomUUID()).encrypt(encryptionSecret); } async function symmetricDecodeJWT(token, secret, salt) { if (!token) return null; try { const { payload } = await jwtDecrypt(token, async ({ kid }) => { const encryptionSecret = hkdf(sha256, new TextEncoder().encode(secret), new TextEncoder().encode(salt), info, 64); if (kid === void 0) return encryptionSecret; if (kid === await calculateJwkThumbprint({ kty: "oct", k: base64url.encode(encryptionSecret) }, "sha256")) return encryptionSecret; throw new Error("no matching decryption secret"); }, { clockTolerance: 15, keyManagementAlgorithms: [alg], contentEncryptionAlgorithms: [enc, "A256GCM"] }); return payload; } catch { return null; } } function constantTimeEqual(a, b) { if (typeof a === "string") a = new TextEncoder().encode(a); if (typeof b === "string") b = new TextEncoder().encode(b); const aBuffer = new Uint8Array(a); const bBuffer = new Uint8Array(b); let c = aBuffer.length ^ bBuffer.length; const length = Math.max(aBuffer.length, bBuffer.length); for (let i = 0; i < length; i++) c |= (i < aBuffer.length ? aBuffer[i] : 0) ^ (i < bBuffer.length ? bBuffer[i] : 0); return c === 0; } const config = { N: 16384, r: 16, p: 1, dkLen: 64 }; async function generateKey(password, salt) { return await scryptAsync(password.normalize("NFKC"), salt, { N: config.N, p: config.p, r: config.r, dkLen: config.dkLen, maxmem: 128 * config.N * config.r * 2 }); } const hashPassword = async (password) => { const salt = hex.encode(crypto.getRandomValues(new Uint8Array(16))); const key = await generateKey(password, salt); return `${salt}:${hex.encode(key)}`; }; const verifyPassword$1 = async ({ hash, password }) => { const [salt, key] = hash.split(":"); if (!salt || !key) throw new BetterAuthError("Invalid password hash"); return constantTimeEqual(await generateKey(password, salt), hexToBytes(key)); }; const generateRandomString = createRandomStringGenerator("a-z", "0-9", "A-Z", "-_"); const symmetricEncrypt = async ({ key, data }) => { const keyAsBytes = await createHash("SHA-256").digest(key); const dataAsBytes = utf8ToBytes(data); return bytesToHex(managedNonce(xchacha20poly1305)(new Uint8Array(keyAsBytes)).encrypt(dataAsBytes)); }; const symmetricDecrypt = async ({ key, data }) => { const keyAsBytes = await createHash("SHA-256").digest(key); const dataAsBytes = hexToBytes$1(data); const chacha = managedNonce(xchacha20poly1305)(new Uint8Array(keyAsBytes)); return new TextDecoder().decode(chacha.decrypt(dataAsBytes)); }; const ALLOWED_COOKIE_SIZE = 4096; const ESTIMATED_EMPTY_COOKIE_SIZE = 200; const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE; function parseCookiesFromContext(ctx) { const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return {}; const cookies = {}; const pairs = cookieHeader.split("; "); for (const pair of pairs) { const [name, ...valueParts] = pair.split("="); if (name && valueParts.length > 0) cookies[name] = valueParts.join("="); } return cookies; } function getChunkIndex(cookieName) { const parts = cookieName.split("."); const lastPart = parts[parts.length - 1]; const index = parseInt(lastPart || "0", 10); return isNaN(index) ? 0 : index; } function readExistingChunks(cookieName, ctx) { const chunks = {}; const cookies = parseCookiesFromContext(ctx); for (const [name, value] of Object.entries(cookies)) if (name.startsWith(cookieName)) chunks[name] = value; return chunks; } function joinChunks(chunks) { return Object.keys(chunks).sort((a, b) => { return getChunkIndex(a) - getChunkIndex(b); }).map((key) => chunks[key]).join(""); } function chunkCookie(storeName, cookie, chunks, logger2) { const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE); if (chunkCount === 1) { chunks[cookie.name] = cookie.value; return [cookie]; } const cookies = []; for (let i = 0; i < chunkCount; i++) { const name = `${cookie.name}.${i}`; const start = i * CHUNK_SIZE; const value = cookie.value.substring(start, start + CHUNK_SIZE); cookies.push({ ...cookie, name, value }); chunks[name] = value; } logger2.debug(`CHUNKING_${storeName.toUpperCase()}_COOKIE`, { message: `${storeName} cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`, emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE, valueSize: cookie.value.length, chunkCount, chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE) }); return cookies; } function getCleanCookies(chunks, cookieOptions) { const cleanedChunks = {}; for (const name in chunks) cleanedChunks[name] = { name, value: "", attributes: { ...cookieOptions, maxAge: 0 } }; return cleanedChunks; } const storeFactory = (storeName) => (cookieName, cookieOptions, ctx) => { const chunks = readExistingChunks(cookieName, ctx); const logger2 = ctx.context.logger; return { getValue() { return joinChunks(chunks); }, hasChunks() { return Object.keys(chunks).length > 0; }, chunk(value, options) { const cleanedChunks = getCleanCookies(chunks, cookieOptions); for (const name in chunks) delete chunks[name]; const cookies = cleanedChunks; const chunked = chunkCookie(storeName, { name: cookieName, value, attributes: { ...cookieOptions, ...options } }, chunks, logger2); for (const chunk of chunked) cookies[chunk.name] = chunk; return Object.values(cookies); }, clean() { const cleanedChunks = getCleanCookies(chunks, cookieOptions); for (const name in chunks) delete chunks[name]; return Object.values(cleanedChunks); }, setCookies(cookies) { for (const cookie of cookies) ctx.setCookie(cookie.name, cookie.value, cookie.attributes); } }; }; const createSessionStore = storeFactory("Session"); const createAccountStore = storeFactory("Account"); function getChunkedCookie(ctx, cookieName) { const value = ctx.getCookie(cookieName); if (value) return value; const chunks = []; const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return null; const cookies = {}; const pairs = cookieHeader.split("; "); for (const pair of pairs) { const [name, ...valueParts] = pair.split("="); if (name && valueParts.length > 0) cookies[name] = valueParts.join("="); } for (const [name, val] of Object.entries(cookies)) if (name.startsWith(cookieName + ".")) { const indexStr = name.split(".").at(-1); const index = parseInt(indexStr || "0", 10); if (!isNaN(index)) chunks.push({ index, value: val }); } if (chunks.length > 0) { chunks.sort((a, b) => a.index - b.index); return chunks.map((c) => c.value).join(""); } return null; } async function setAccountCookie(c, accountData) { const accountDataCookie = c.context.authCookies.accountData; const options = { maxAge: 300, ...accountDataCookie.attributes }; const data = await symmetricEncodeJWT(accountData, c.context.secret, "better-auth-account", options.maxAge); if (data.length > ALLOWED_COOKIE_SIZE) { const accountStore = createAccountStore(accountDataCookie.name, options, c); const cookies = accountStore.chunk(data, options); accountStore.setCookies(cookies); } else { const accountStore = createAccountStore(accountDataCookie.name, options, c); if (accountStore.hasChunks()) { const cleanCookies = accountStore.clean(); accountStore.setCookies(cleanCookies); } c.setCookie(accountDataCookie.name, data, options); } } async function getAccountCookie(c) { const accountCookie = getChunkedCookie(c, c.context.authCookies.accountData.name); if (accountCookie) { const accountData = safeJSONParse(await symmetricDecodeJWT(accountCookie, c.context.secret, "better-auth-account")); if (accountData) return accountData; } return null; } const getSessionQuerySchema = z.optional(z.object({ disableCookieCache: z.coerce.boolean().meta({ description: "Disable cookie cache and fetch session from database" }).optional(), disableRefresh: z.coerce.boolean().meta({ description: "Disable session refresh. Useful for checking session status, without updating the session" }).optional() })); function isPromise(obj) { return !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function"; } const SEC = 1e3; const MIN = SEC * 60; const HOUR = MIN * 60; const DAY = HOUR * 24; const WEEK = DAY * 7; const MONTH = DAY * 30; const YEAR = DAY * 365.25; const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mo|years?|yrs?|y)(?: (ago|from now))?$/i; function parse(value) { const match = REGEX.exec(value); if (!match || match[4] && match[1]) throw new TypeError(`Invalid time string format: "${value}". Use formats like "7d", "30m", "1 hour", etc.`); const n = parseFloat(match[2]); const unit = match[3].toLowerCase(); let result; switch (unit) { case "years": case "year": case "yrs": case "yr": case "y": result = n * YEAR; break; case "months": case "month": case "mo": result = n * MONTH; break; case "weeks": case "week": case "w": result = n * WEEK; break; case "days": case "day": case "d": result = n * DAY; break; case "hours": case "hour": case "hrs": case "hr": case "h": result = n * HOUR; break; case "minutes": case "minute": case "mins": case "min": case "m": result = n * MIN; break; case "seconds": case "second": case "secs": case "sec": case "s": result = n * SEC; break; default: throw new TypeError(`Unknown time unit: "${unit}"`); } if (match[1] === "-" || match[4] === "ago") return -result; return result; } function ms(value) { return parse(value); } function sec(value) { return Math.round(parse(value) / 1e3); } const SECURE_COOKIE_PREFIX = "__Secure-"; function splitSetCookieHeader(setCookie) { if (!setCookie) return []; const result = []; let current = ""; let i = 0; while (i < setCookie.length) { const c = setCookie[i]; if (c === ",") { const lower = current.toLowerCase(); if (lower.includes("expires=") && !lower.includes("gmt")) { current += c; i++; } else { const trimmed$1 = current.trim(); if (trimmed$1) result.push(trimmed$1); current = ""; i++; if (i < setCookie.length && setCookie[i] === " ") i++; } continue; } current += c; i++; } const trimmed = current.trim(); if (trimmed) result.push(trimmed); return result; } function parseSetCookieHeader(setCookie) { const cookies = /* @__PURE__ */ new Map(); splitSetCookieHeader(setCookie).forEach((cookieString) => { const [nameValue, ...attributes] = cookieString.split(";").map((part) => part.trim()); const [name, ...valueParts] = (nameValue || "").split("="); const value = valueParts.join("="); if (!name || value === void 0) return; const attrObj = { value }; attributes.forEach((attribute) => { const [attrName, ...attrValueParts] = attribute.split("="); const attrValue = attrValueParts.join("="); const normalizedAttrName = attrName.trim().toLowerCase(); switch (normalizedAttrName) { case "max-age": attrObj["max-age"] = attrValue ? parseInt(attrValue.trim(), 10) : void 0; break; case "expires": attrObj.expires = attrValue ? new Date(attrValue.trim()) : void 0; break; case "domain": attrObj.domain = attrValue ? attrValue.trim() : void 0; break; case "path": attrObj.path = attrValue ? attrValue.trim() : void 0; break; case "secure": attrObj.secure = true; break; case "httponly": attrObj.httponly = true; break; case "samesite": attrObj.samesite = attrValue ? attrValue.trim().toLowerCase() : void 0; break; default: attrObj[normalizedAttrName] = attrValue ? attrValue.trim() : true; break; } }); cookies.set(name, attrObj); }); return cookies; } function createCookieGetter(options) { const secureCookiePrefix = (options.advanced?.useSecureCookies !== void 0 ? options.advanced?.useSecureCookies : options.baseURL ? options.baseURL.startsWith("https://") ? true : false : isProduction) ? SECURE_COOKIE_PREFIX : ""; const crossSubdomainEnabled = !!options.advanced?.crossSubDomainCookies?.enabled; const domain = crossSubdomainEnabled ? options.advanced?.crossSubDomainCookies?.domain || (options.baseURL ? new URL(options.baseURL).hostname : void 0) : void 0; if (crossSubdomainEnabled && !domain) throw new BetterAuthError("baseURL is required when crossSubdomainCookies are enabled"); function createCookie(cookieName, overrideAttributes = {}) { const prefix = options.advanced?.cookiePrefix || "better-auth"; const name = options.advanced?.cookies?.[cookieName]?.name || `${prefix}.${cookieName}`; const attributes = options.advanced?.cookies?.[cookieName]?.attributes; return { name: `${secureCookiePrefix}${name}`, attributes: { secure: !!secureCookiePrefix, sameSite: "lax", path: "/", httpOnly: true, ...crossSubdomainEnabled ? { domain } : {}, ...options.advanced?.defaultCookieAttributes, ...overrideAttributes, ...attributes } }; } return createCookie; } function getCookies(options) { const createCookie = createCookieGetter(options); const sessionToken = createCookie("session_token", { maxAge: options.session?.expiresIn || sec("7d") }); const sessionData = createCookie("session_data", { maxAge: options.session?.cookieCache?.maxAge || 300 }); const accountData = createCookie("account_data", { maxAge: options.session?.cookieCache?.maxAge || 300 }); const dontRememberToken = createCookie("dont_remember"); return { sessionToken: { name: sessionToken.name, attributes: sessionToken.attributes }, sessionData: { name: sessionData.name, attributes: sessionData.attributes }, dontRememberToken: { name: dontRememberToken.name, attributes: dontRememberToken.attributes }, accountData: { name: accountData.name, attributes: accountData.attributes } }; } async function setCookieCache(ctx, session, dontRememberMe) { if (!ctx.context.options.session?.cookieCache?.enabled) return; const filteredSession = filterOutputFields(session.session, ctx.context.options.session?.additionalFields); const filteredUser = parseUserOutput(ctx.context.options, session.user); const versionConfig = ctx.context.options.session?.cookieCache?.version; let version = "1"; if (versionConfig) { if (typeof versionConfig === "string") version = versionConfig; else if (typeof versionConfig === "function") { const result = versionConfig(session.session, session.user); version = isPromise(result) ? await result : result; } } const sessionData = { session: filteredSession, user: filteredUser, updatedAt: Date.now(), version }; const options = { ...ctx.context.authCookies.sessionData.attributes, maxAge: dontRememberMe ? void 0 : ctx.context.authCookies.sessionData.attributes.maxAge }; const expiresAtDate = getDate(options.maxAge || 60, "sec").getTime(); const strategy = ctx.context.options.session?.cookieCache?.strategy || "compact"; let data; if (strategy === "jwe") data = await symmetricEncodeJWT(sessionData, ctx.context.secret, "better-auth-session", options.maxAge || 300); else if (strategy === "jwt") data = await signJWT(sessionData, ctx.context.secret, options.maxAge || 300); else data = base64Url.encode(JSON.stringify({ session: sessionData, expiresAt: expiresAtDate, signature: await createHMAC("SHA-256", "base64urlnopad").sign(ctx.context.secret, JSON.stringify({ ...sessionData, expiresAt: expiresAtDate })) }), { padding: false }); if (data.length > 4093) { const sessionStore = createSessionStore(ctx.context.authCookies.sessionData.name, options, ctx); const cookies = sessionStore.chunk(data, options); sessionStore.setCookies(cookies); } else { const sessionStore = createSessionStore(ctx.context.authCookies.sessionData.name, options, ctx); if (sessionStore.hasChunks()) { const cleanCookies = sessionStore.clean(); sessionStore.setCookies(cleanCookies); } ctx.setCookie(ctx.context.authCookies.sessionData.name, data, options); } if (ctx.context.options.account?.storeAccountCookie) { const accountData = await getAccountCookie(ctx); if (accountData) await setAccountCookie(ctx, accountData); } } async function setSessionCookie(ctx, session, dontRememberMe, overrides) { const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret); dontRememberMe = dontRememberMe !== void 0 ? dontRememberMe : !!dontRememberMeCookie; const options = ctx.context.authCookies.sessionToken.attributes; const maxAge = dontRememberMe ? void 0 : ctx.context.sessionConfig.expiresIn; await ctx.setSignedCookie(ctx.context.authCookies.sessionToken.name, session.session.token, ctx.context.secret, { ...options, maxAge, ...overrides }); if (dontRememberMe) await ctx.setSignedCookie(ctx.context.authCookies.dontRememberToken.name, "true", ctx.context.secret, ctx.context.authCookies.dontRememberToken.attributes); await setCookieCache(ctx, session, dontRememberMe); ctx.context.setNewSession(session); } function expireCookie(ctx, cookie) { ctx.setCookie(cookie.name, "", { ...cookie.attributes, maxAge: 0 }); } function deleteSessionCookie(ctx, skipDontRememberMe) { expireCookie(ctx, ctx.context.authCookies.sessionToken); expireCookie(ctx, ctx.context.authCookies.sessionData); if (ctx.context.options.account?.storeAccountCookie) { expireCookie(ctx, ctx.context.authCookies.accountData); const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.attributes, ctx); const cleanCookies$1 = accountStore.clean(); accountStore.setCookies(cleanCookies$1); } if (ctx.context.oauthConfig.storeStateStrategy === "cookie") expireCookie(ctx, ctx.context.createAuthCookie("oauth_state")); const sessionStore = createSessionStore(ctx.context.authCookies.sessionData.name, ctx.context.authCookies.sessionData.attributes, ctx); const cleanCookies = sessionStore.clean(); sessionStore.setCookies(cleanCookies); expireCookie(ctx, ctx.context.authCookies.dontRememberToken); } const getSession = () => createAuthEndpoint("/get-session", { method: "GET", operationId: "getSession", query: getSessionQuerySchema, requireHeaders: true, metadata: { openapi: { operationId: "getSession", description: "Get the current session", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", nullable: true, properties: { session: { $ref: "#/components/schemas/Session" }, user: { $ref: "#/components/schemas/User" } }, required: ["session", "user"] } } } } } } } }, async (ctx) => { try { const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret); if (!sessionCookieToken) return null; const sessionDataCookie = getChunkedCookie(ctx, ctx.context.authCookies.sessionData.name); let sessionDataPayload = null; if (sessionDataCookie) { const strategy = ctx.context.options.session?.cookieCache?.strategy || "compact"; if (strategy === "jwe") { const payload = await symmetricDecodeJWT(sessionDataCookie, ctx.context.secret, "better-auth-session"); if (payload && payload.session && payload.user) sessionDataPayload = { session: { session: payload.session, user: payload.user, updatedAt: payload.updatedAt, version: payload.version }, expiresAt: payload.exp ? payload.exp * 1e3 : Date.now() }; else { expireCookie(ctx, ctx.context.authCookies.sessionData); return ctx.json(null); } } else if (strategy === "jwt") { const payload = await verifyJWT(sessionDataCookie, ctx.context.secret); if (payload && payload.session && payload.user) sessionDataPayload = { session: { session: payload.session, user: payload.user, updatedAt: payload.updatedAt, version: payload.version }, expiresAt: payload.exp ? payload.exp * 1e3 : Date.now() }; else { expireCookie(ctx, ctx.context.authCookies.sessionData); return ctx.json(null); } } else { const parsed = safeJSONParse(binary.decode(base64Url.decode(sessionDataCookie))); if (parsed) if (await createHMAC("SHA-256", "base64urlnopad").verify(ctx.context.secret, JSON.stringify({ ...parsed.session, expiresAt: parsed.expiresAt }), parsed.signature)) sessionDataPayload = parsed; else { expireCookie(ctx, ctx.context.authCookies.sessionData); return ctx.json(null); } } } const dontRememberMe = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret); if (sessionDataPayload?.session && ctx.context.options.session?.cookieCache?.enabled && !ctx.query?.disableCookieCache) { const session$1 = sessionDataPayload.session; const versionConfig = ctx.context.options.session?.cookieCache?.version; let expectedVersion = "1"; if (versionConfig) { if (typeof versionConfig === "string") expectedVersion = versionConfig; else if (typeof versionConfig === "function") { const result = versionConfig(session$1.session, session$1.user); expectedVersion = result instanceof Promise ? await result : result; } } if ((session$1.version || "1") !== expectedVersion) expireCookie(ctx, ctx.context.authCookies.sessionData); else { const cachedSessionExpiresAt = new Date(session$1.session.expiresAt); if (sessionDataPayload.expiresAt < Date.now() || cachedSessionExpiresAt < /* @__PURE__ */ new Date()) expireCookie(ctx, ctx.context.authCookies.sessionData); else { const cookieRefreshCache = ctx.context.sessionConfig.cookieRefreshCache; if (cookieRefreshCache === false) { ctx.context.session = session$1; const parsedSession$2 = parseSessionOutput(ctx.context.options, { ...session$1.session, expiresAt: new Date(session$1.session.expiresAt), createdAt: new Date(session$1.session.createdAt), updatedAt: new Date(session$1.session.updatedAt) }); const parsedUser$2 = parseUserOutput(ctx.context.options, { ...session$1.user, createdAt: new Date(session$1.user.createdAt), updatedAt: new Date(session$1.user.updatedAt) }); return ctx.json({ session: parsedSession$2, user: parsedUser$2 }); } if (sessionDataPayload.expiresAt - Date.now() < cookieRefreshCache.updateAge * 1e3) { const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec"); const refreshedSession = { session: { ...session$1.session, expiresAt: newExpiresAt }, user: session$1.user, updatedAt: Date.now() }; await setCookieCache(ctx, refreshedSession, false); const parsedRefreshedSession = parseSessionOutput(ctx.context.options, { ...refreshedSession.session, expiresAt: new Date(refreshedSession.session.expiresAt), createdAt: new Date(refreshedSession.session.createdAt), updatedAt: new Date(refreshedSession.session.updatedAt) }); const parsedRefreshedUser = parseUserOutput(ctx.context.options, { ...refreshedSession.user, createdAt: new Date(refreshedSession.user.createdAt), updatedAt: new Date(refreshedSession.user.updatedAt) }); ctx.context.session = { session: parsedRefreshedSession, user: parsedRefreshedUser }; return ctx.json({ session: parsedRefreshedSession, user: parsedRefreshedUser }); } const parsedSession$1 = parseSessionOutput(ctx.context.options, { ...session$1.session, expiresAt: new Date(session$1.session.expiresAt), createdAt: new Date(session$1.session.createdAt), updatedAt: new Date(session$1.session.updatedAt) }); const parsedUser$1 = parseUserOutput(ctx.context.options, { ...session$1.user, createdAt: new Date(session$1.user.createdAt), updatedAt: new Date(session$1.user.updatedAt) }); ctx.context.session = { session: parsedSession$1, user: parsedUser$1 }; return ctx.json({ session: parsedSession$1, user: parsedUser$1 }); } } } const session = await ctx.context.internalAdapter.findSession(sessionCookieToken); ctx.context.session = session; if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) { deleteSessionCookie(ctx); if (session) await ctx.context.internalAdapter.deleteSession(session.session.token); return ctx.json(null); } if (dontRememberMe || ctx.query?.disableRefresh) { const parsedSession$1 = parseSessionOutput(ctx.context.options, session.session); const parsedUser$1 = parseUserOutput(ctx.context.options, session.user); return ctx.json({ session: parsedSession$1, user: parsedUser$1 }); } const expiresIn = ctx.context.sessionConfig.expiresIn; const updateAge = ctx.context.sessionConfig.updateAge; if (session.session.expiresAt.valueOf() - expiresIn * 1e3 + updateAge * 1e3 <= Date.now() && (!ctx.query?.disableRefresh || !ctx.context.options.session?.disableSessionRefresh)) { const updatedSession = await ctx.context.internalAdapter.updateSession(session.session.token, { expiresAt: getDate(ctx.context.sessionConfig.expiresIn, "sec"), updatedAt: /* @__PURE__ */ new Date() }); if (!updatedSession) { deleteSessionCookie(ctx); return ctx.json(null, { status: 401 }); } const maxAge = (updatedSession.expiresAt.valueOf() - Date.now()) / 1e3; await setSessionCookie(ctx, { session: updatedSession, user: session.user }, false, { maxAge }); const parsedUpdatedSession = parseSessionOutput(ctx.context.options, updatedSession); const parsedUser$1 = parseUserOutput(ctx.context.options, session.user); return ctx.json({ session: parsedUpdatedSession, user: parsedUser$1 }); } await setCookieCache(ctx, session, !!dontRememberMe); const parsedSession = parseSessionOutput(ctx.context.options, session.session); const parsedUser = parseUserOutput(ctx.context.options, session.user); return ctx.json({ session: parsedSession, user: parsedUser }); } catch (error2) { ctx.context.logger.error("INTERNAL_SERVER_ERROR", error2); throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION }); } }); const getSessionFromCtx = async (ctx, config2) => { if (ctx.context.session) return ctx.context.session; const session = await getSession()({ ...ctx, asResponse: false, headers: ctx.headers, returnHeaders: false, returnStatus: false, query: { ...config2, ...ctx.query } }).catch((e) => { return null; }); ctx.context.session = session; return session; }; const sessionMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session?.session) throw new APIError("UNAUTHORIZED"); return { session }; }); const sensitiveSessionMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx, { disableCookieCache: true }); if (!session?.session) throw new APIError("UNAUTHORIZED"); return { session }; }); createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session?.session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED"); return { session }; }); const freshSessionMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session?.session) throw new APIError("UNAUTHORIZED"); if (ctx.context.sessionConfig.freshAge === 0) return { session }; const freshAge = ctx.context.sessionConfig.freshAge; const lastUpdated = new Date(session.session.updatedAt || session.session.createdAt).getTime(); if (!(Date.now() - lastUpdated < freshAge * 1e3)) throw new APIError("FORBIDDEN", { message: "Session is not fresh" }); return { session }; }); const listSessions = () => createAuthEndpoint("/list-sessions", { method: "GET", operationId: "listUserSessions", use: [sessionMiddleware], requireHeaders: true, metadata: { openapi: { operationId: "listUserSessions", description: "List all active sessions for the user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } }, async (ctx) => { try { const activeSessions = (await ctx.context.internalAdapter.listSessions(ctx.context.session.user.id)).filter((session) => { return session.expiresAt > /* @__PURE__ */ new Date(); }); return ctx.json(activeSessions.map((session) => parseSessionOutput(ctx.context.options, session))); } catch (e) { ctx.context.logger.error(e); throw ctx.error("INTERNAL_SERVER_ERROR"); } }); const revokeSession = createAuthEndpoint("/revoke-session", { method: "POST", body: z.object({ token: z.string().meta({ description: "The token to revoke" }) }), use: [sensitiveSessionMiddleware], requireHeaders: true, metadata: { openapi: { description: "Revoke a single session", requestBody: { content: { "application/json": { schema: { type: "object", properties: { token: { type: "string", description: "The token to revoke" } }, required: ["token"] } } } }, responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if the session was revoked successfully" } }, required: ["status"] } } } } } } } }, async (ctx) => { const token = ctx.body.token; if ((await ctx.context.internalAdapter.findSession(token))?.session.userId === ctx.context.session.user.id) try { await ctx.context.internalAdapter.deleteSession(token); } catch (error2) { ctx.context.logger.error(error2 && typeof error2 === "object" && "name" in error2 ? error2.name : "", error2); throw new APIError("INTERNAL_SERVER_ERROR"); } return ctx.json({ status: true }); }); const revokeSessions = createAuthEndpoint("/revoke-sessions", { method: "POST", use: [sensitiveSessionMiddleware], requireHeaders: true, metadata: { openapi: { description: "Revoke all sessions for the user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if all sessions were revoked successfully" } }, required: ["status"] } } } } } } } }, async (ctx) => { try { await ctx.context.internalAdapter.deleteSessions(ctx.context.session.user.id); } catch (error2) { ctx.context.logger.error(error2 && typeof error2 === "object" && "name" in error2 ? error2.name : "", error2); throw new APIError("INTERNAL_SERVER_ERROR"); } return ctx.json({ status: true }); }); const revokeOtherSessions = createAuthEndpoint("/revoke-other-sessions", { method: "POST", requireHeaders: true, use: [sensitiveSessionMiddleware], metadata: { openapi: { description: "Revoke all other sessions for the user except the current one", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if all other sessions were revoked successfully" } }, required: ["status"] } } } } } } } }, async (ctx) => { const session = ctx.context.session; if (!session.user) throw new APIError("UNAUTHORIZED"); const otherSessions = (await ctx.context.internalAdapter.listSessions(session.user.id)).filter((session$1) => { return session$1.expiresAt > /* @__PURE__ */ new Date(); }).filter((session$1) => session$1.token !== ctx.context.session.session.token); await Promise.all(otherSessions.map((session$1) => ctx.context.internalAdapter.deleteSession(session$1.token))); return ctx.json({ status: true }); }); const stateDataSchema = z.looseObject({ callbackURL: z.string(), codeVerifier: z.string(), errorURL: z.string().optional(), newUserURL: z.string().optional(), expiresAt: z.number(), link: z.object({ email: z.string(), userId: z.coerce.string() }).optional(), requestSignUp: z.boolean().optional() }); var StateError = class extends BetterAuthError { code; details; constructor(message, options) { super(message, options); this.code = options.code; this.details = options.details; } }; async function generateGenericState(c, stateData, settings) { const state = generateRandomString(32); if (c.context.oauthConfig.storeStateStrategy === "cookie") { const encryptedData = await symmetricEncrypt({ key: c.context.secret, data: JSON.stringify(stateData) }); const stateCookie$1 = c.context.createAuthCookie("oauth_state", { maxAge: 600 }); c.setCookie(stateCookie$1.name, encryptedData, stateCookie$1.attributes); return { state, codeVerifier: stateData.codeVerifier }; } const stateCookie = c.context.createAuthCookie("state", { maxAge: 300 }); await c.setSignedCookie(stateCookie.name, state, c.context.secret, stateCookie.attributes); const expiresAt = /* @__PURE__ */ new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 10); const verification = await c.context.internalAdapter.createVerificationValue({ value: JSON.stringify(stateData), identifier: state, expiresAt }); if (!verification) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" }); return { state: verification.identifier, codeVerifier: stateData.codeVerifier }; } async function parseGenericState(c, state, settings) { const storeStateStrategy = c.context.oauthConfig.storeStateStrategy; let parsedData; if (storeStateStrategy === "cookie") { const stateCookie = c.context.createAuthCookie("oauth_state"); const encryptedData = c.getCookie(stateCookie.name); if (!encryptedData) throw new StateError("State mismatch: auth state cookie not found", { code: "state_mismatch", details: { state } }); try { const decryptedData = await symmetricDecrypt({ key: c.context.secret, data: encryptedData }); parsedData = stateDataSchema.parse(JSON.parse(decryptedData)); } catch (error2) { throw new StateError("State invalid: Failed to decrypt or parse auth state", { code: "state_invalid", details: { state }, cause: error2 }); } expireCookie(c, stateCookie); } else { const data = await c.context.internalAdapter.findVerificationValue(state); if (!data) throw new StateError("State mismatch: verification not found", { code: "state_mismatch", details: { state } }); parsedData = stateDataSchema.parse(JSON.parse(data.value)); const stateCookie = c.context.createAuthCookie("state"); const stateCookieValue = await c.getSignedCookie(stateCookie.name, c.context.secret); if (!c.context.oauthConfig.skipStateCookieCheck && (!stateCookieValue || stateCookieValue !== state)) throw new StateError("State mismatch: State not persisted correctly", { code: "state_security_mismatch", details: { state } }); expireCookie(c, stateCookie); await c.context.internalAdapter.deleteVerificationValue(data.id); } if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", { code: "state_mismatch", details: { expiresAt: parsedData.expiresAt } }); return parsedData; } async function generateState(c, link, additionalData) { const callbackURL = c.body?.callbackURL || c.context.options.baseURL; if (!callbackURL) throw new APIError("BAD_REQUEST", { message: "callbackURL is required" }); const codeVerifier = generateRandomString(128); const stateData = { ...additionalData ? additionalData : {}, callbackURL, codeVerifier, errorURL: c.body?.errorCallbackURL, newUserURL: c.body?.newUserCallbackURL, link, expiresAt: Date.now() + 600 * 1e3, requestSignUp: c.body?.requestSignUp }; await setOAuthState(stateData); try { return generateGenericState(c, stateData); } catch (error2) { c.context.logger.error("Failed to create verification", error2); throw new APIError("INTERNAL_SERVER_ERROR", { message: "Unable to create verification", cause: error2 }); } } async function parseState(c) { const state = c.query.state || c.body.state; const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; let parsedData; try { parsedData = await parseGenericState(c, state); } catch (error2) { c.context.logger.error("Failed to parse state", error2); if (error2 instanceof StateError && error2.code === "state_security_mismatch") throw c.redirect(`${errorURL}?error=state_mismatch`); throw c.redirect(`${errorURL}?error=please_restart_the_process`); } if (!parsedData.errorURL) parsedData.errorURL = errorURL; if (parsedData) await setOAuthState(parsedData); return parsedData; } function decryptOAuthToken(token, ctx) { if (!token) return token; if (ctx.options.account?.encryptOAuthTokens) return symmetricDecrypt({ key: ctx.secret, data: token }); return token; } function setTokenUtil(token, ctx) { if (ctx.options.account?.encryptOAuthTokens && token) return symmetricEncrypt({ key: ctx.secret, data: token }); return token; } const listUserAccounts = createAuthEndpoint("/list-accounts", { method: "GET", use: [sessionMiddleware], metadata: { openapi: { operationId: "listUserAccounts", description: "List all accounts linked to the user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { type: "object", properties: { id: { type: "string" }, providerId: { type: "string" }, createdAt: { type: "string", format: "date-time" }, updatedAt: { type: "string", format: "date-time" }, accountId: { type: "string" }, userId: { type: "string" }, scopes: { type: "array", items: { type: "string" } } }, required: [ "id", "providerId", "createdAt", "updatedAt", "accountId", "userId", "scopes" ] } } } } } } } } }, async (c) => { const session = c.context.session; const accounts = await c.context.internalAdapter.findAccounts(session.user.id); return c.json(accounts.map((a) => { const { scope, ...parsed } = parseAccountOutput(c.context.options, a); return { ...parsed, scopes: scope?.split(",") || [] }; })); }); const linkSocialAccount = createAuthEndpoint("/link-social", { method: "POST", requireHeaders: true, body: z.object({ callbackURL: z.string().meta({ description: "The URL to redirect to after the user has signed in" }).optional(), provider: SocialProviderListEnum, idToken: z.object({ token: z.string(), nonce: z.string().optional(), accessToken: z.string().optional(), refreshToken: z.string().optional(), scopes: z.array(z.string()).optional() }).optional(), requestSignUp: z.boolean().optional(), scopes: z.array(z.string()).meta({ description: "Additional scopes to request from the provider" }).optional(), errorCallbackURL: z.string().meta({ description: "The URL to redirect to if there is an error during the link process" }).optional(), disableRedirect: z.boolean().meta({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself" }).optional(), additionalData: z.record(z.string(), z.any()).optional() }), use: [sessionMiddleware], metadata: { openapi: { description: "Link a social account to the user", operationId: "linkSocialAccount", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { url: { type: "string", description: "The authorization URL to redirect the user to" }, redirect: { type: "boolean", description: "Indicates if the user should be redirected to the authorization URL" }, status: { type: "boolean" } }, required: ["redirect"] } } } } } } } }, async (c) => { const session = c.context.session; const provider = c.context.socialProviders.find((p) => p.id === c.body.provider); if (!provider) { c.context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: c.body.provider }); throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND }); } if (c.body.idToken) { if (!provider.verifyIdToken) { c.context.logger.error("Provider does not support id token verification", { provider: c.body.provider }); throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED }); } const { token, nonce } = c.body.idToken; if (!await provider.verifyIdToken(token, nonce)) { c.context.logger.error("Invalid id token", { provider: c.body.provider }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_TOKEN }); } const linkingUserInfo = await provider.getUserInfo({ idToken: token, accessToken: c.body.idToken.accessToken, refreshToken: c.body.idToken.refreshToken }); if (!linkingUserInfo || !linkingUserInfo?.user) { 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 }); } const linkingUserId = String(linkingUserInfo.user.id); if (!linkingUserInfo.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 }); } if ((await c.context.internalAdapter.findAccounts(session.user.id)).find((a) => a.providerId === provider.id && a.accountId === linkingUserId)) return c.json({ url: "", status: true, redirect: false }); if (!c.context.options.account?.accountLinking?.trustedProviders?.includes(provider.id) && !linkingUserInfo.user.emailVerified || c.context.options.account?.accountLinking?.enabled === false) throw new APIError("UNAUTHORIZED", { message: "Account not linked - linking not allowed" }); if (linkingUserInfo.user.email?.toLowerCase() !== session.user.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) throw new APIError("UNAUTHORIZED", { message: "Account not linked - different emails not allowed" }); try { await c.context.internalAdapter.createAccount({ userId: session.user.id, providerId: provider.id, accountId: linkingUserId, accessToken: c.body.idToken.accessToken, idToken: token, refreshToken: c.body.idToken.refreshToken, scope: c.body.idToken.scopes?.join(",") }); } catch { throw new APIError("EXPECTATION_FAILED", { message: "Account not linked - unable to create account" }); } if (c.context.options.account?.accountLinking?.updateUserInfoOnLink === true) try { await c.context.internalAdapter.updateUser(session.user.id, { name: linkingUserInfo.user?.name, image: linkingUserInfo.user?.image }); } catch (e) { console.warn("Could not update user - " + e.toString()); } return c.json({ url: "", status: true, redirect: false }); } const state = await generateState(c, { userId: session.user.id, email: session.user.email }, c.body.additionalData); const url = await provider.createAuthorizationURL({ state: state.state, codeVerifier: state.codeVerifier, redirectURI: `${c.context.baseURL}/callback/${provider.id}`, scopes: c.body.scopes }); if (!c.body.disableRedirect) c.setHeader("Location", url.toString()); return c.json({ url: url.toString(), redirect: !c.body.disableRedirect }); }); const unlinkAccount = createAuthEndpoint("/unlink-account", { method: "POST", body: z.object({ providerId: z.string(), accountId: z.string().optional() }), use: [freshSessionMiddleware], metadata: { openapi: { description: "Unlink an account", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" } } } } } } } } } }, async (ctx) => { const { providerId, accountId } = ctx.body; const accounts = await ctx.context.internalAdapter.findAccounts(ctx.context.session.user.id); if (accounts.length === 1 && !ctx.context.options.account?.accountLinking?.allowUnlinkingAll) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT }); const accountExist = accounts.find((account) => accountId ? account.accountId === accountId && account.providerId === providerId : account.providerId === providerId); if (!accountExist) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.ACCOUNT_NOT_FOUND }); await ctx.context.internalAdapter.deleteAccount(accountExist.id); return ctx.json({ status: true }); }); const getAccessToken = createAuthEndpoint("/get-access-token", { method: "POST", body: z.object({ providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }), accountId: z.string().meta({ description: "The account ID associated with the refresh token" }).optional(), userId: z.string().meta({ description: "The user ID associated with the account" }).optional() }), metadata: { openapi: { description: "Get a valid access token, doing a refresh if needed", responses: { 200: { description: "A Valid access token", content: { "application/json": { schema: { type: "object", properties: { tokenType: { type: "string" }, idToken: { type: "string" }, accessToken: { type: "string" }, accessTokenExpiresAt: { type: "string", format: "date-time" } } } } } }, 400: { description: "Invalid refresh token or provider configuration" } } } } }, async (ctx) => { const { providerId, accountId, userId } = ctx.body || {}; const req = ctx.request; const session = await getSessionFromCtx(ctx); if (req && !session) throw ctx.error("UNAUTHORIZED"); const resolvedUserId = session?.user?.id || userId; if (!resolvedUserId) throw ctx.error("UNAUTHORIZED"); if (!ctx.context.socialProviders.find((p) => p.id === providerId)) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} is not supported.` }); const accountData = await getAccountCookie(ctx); let account = void 0; if (accountData && providerId === accountData.providerId && (!accountId || accountData.id === accountId)) account = accountData; else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.id === accountId && acc.providerId === providerId : acc.providerId === providerId); if (!account) throw new APIError("BAD_REQUEST", { message: "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.` }); try { let newTokens = null; const accessTokenExpired = account.accessTokenExpiresAt && new Date(account.accessTokenExpiresAt).getTime() - Date.now() < 5e3; if (account.refreshToken && accessTokenExpired && provider.refreshAccessToken) { const refreshToken$1 = await decryptOAuthToken(account.refreshToken, ctx.context); newTokens = await provider.refreshAccessToken(refreshToken$1); const updatedData = { accessToken: await setTokenUtil(newTokens.accessToken, ctx.context), accessTokenExpiresAt: newTokens.accessTokenExpiresAt, refreshToken: await setTokenUtil(newTokens.refreshToken, ctx.context), refreshTokenExpiresAt: newTokens.refreshTokenExpiresAt }; let updatedAccount = null; if (account.id) updatedAccount = await ctx.context.internalAdapter.updateAccount(account.id, updatedData); if (ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, { ...account, ...updatedAccount ?? updatedData }); } const accessTokenExpiresAt = (() => { if (newTokens?.accessTokenExpiresAt) { if (typeof newTokens.accessTokenExpiresAt === "string") return new Date(newTokens.accessTokenExpiresAt); return newTokens.accessTokenExpiresAt; } if (account.accessTokenExpiresAt) { if (typeof account.accessTokenExpiresAt === "string") return new Date(account.accessTokenExpiresAt); return account.accessTokenExpiresAt; } })(); const tokens = { accessToken: newTokens?.accessToken ?? await decryptOAuthToken(account.accessToken ?? "", ctx.context), accessTokenExpiresAt, scopes: account.scope?.split(",") ?? [], idToken: newTokens?.idToken ?? account.idToken ?? void 0 }; return ctx.json(tokens); } catch (error2) { throw new APIError("BAD_REQUEST", { message: "Failed to get a valid access token", cause: error2 }); } }); const refreshToken = createAuthEndpoint("/refresh-token", { method: "POST", body: z.object({ providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }), accountId: z.string().meta({ description: "The account ID associated with the refresh token" }).optional(), userId: z.string().meta({ description: "The user ID associated with the account" }).optional() }), metadata: { openapi: { description: "Refresh the access token using a refresh token", responses: { 200: { description: "Access token refreshed successfully", content: { "application/json": { schema: { type: "object", properties: { tokenType: { type: "string" }, idToken: { type: "string" }, accessToken: { type: "string" }, refreshToken: { type: "string" }, accessTokenExpiresAt: { type: "string", format: "date-time" }, refreshTokenExpiresAt: { type: "string", format: "date-time" } } } } } }, 400: { description: "Invalid refresh token or provider configuration" } } } } }, async (ctx) => { const { providerId, accountId, userId } = ctx.body; const req = ctx.request; const session = await getSessionFromCtx(ctx); if (req && !session) throw ctx.error("UNAUTHORIZED"); const resolvedUserId = session?.user?.id || userId; if (!resolvedUserId) throw new APIError("BAD_REQUEST", { message: `Either userId or session is required` }); const provider = ctx.context.socialProviders.find((p) => p.id === providerId); if (!provider) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} not found.` }); if (!provider.refreshAccessToken) throw new APIError("BAD_REQUEST", { message: `Provider ${providerId} does not support token refreshing.` }); let account = void 0; const accountData = await getAccountCookie(ctx); if (accountData && (!providerId || providerId === accountData?.providerId)) account = accountData; else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.id === accountId && acc.providerId === providerId : acc.providerId === providerId); if (!account) throw new APIError("BAD_REQUEST", { message: "Account not found" }); let refreshToken$1 = void 0; if (accountData && providerId === accountData.providerId) refreshToken$1 = accountData.refreshToken ?? void 0; else refreshToken$1 = account.refreshToken ?? void 0; if (!refreshToken$1) throw new APIError("BAD_REQUEST", { message: "Refresh token not found" }); try { const decryptedRefreshToken = await decryptOAuthToken(refreshToken$1, ctx.context); const tokens = await provider.refreshAccessToken(decryptedRefreshToken); if (account.id) { const updateData = { ...account || {}, accessToken: await setTokenUtil(tokens.accessToken, ctx.context), refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context), accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(",") || account.scope, idToken: tokens.idToken || account.idToken }; await ctx.context.internalAdapter.updateAccount(account.id, updateData); } if (accountData && providerId === accountData.providerId && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, { ...accountData, accessToken: await setTokenUtil(tokens.accessToken, ctx.context), refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context), accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(",") || accountData.scope, idToken: tokens.idToken || accountData.idToken }); return ctx.json({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(",") || account.scope, idToken: tokens.idToken || account.idToken, providerId: account.providerId, accountId: account.accountId }); } catch (error2) { throw new APIError("BAD_REQUEST", { message: "Failed to refresh access token", cause: error2 }); } }); const accountInfoQuerySchema = z.optional(z.object({ accountId: z.string().meta({ description: "The provider given account id for which to get the account info" }).optional() })); const accountInfo = createAuthEndpoint("/account-info", { method: "GET", use: [sessionMiddleware], metadata: { openapi: { description: "Get the account info provided by the provider", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { user: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, email: { type: "string" }, image: { type: "string" }, emailVerified: { type: "boolean" } }, required: ["id", "emailVerified"] }, data: { type: "object", properties: {}, additionalProperties: true } }, required: ["user", "data"], additionalProperties: false } } } } } } }, query: accountInfoQuerySchema }, async (ctx) => { const providedAccountId = ctx.query?.accountId; let account = void 0; if (!providedAccountId) { if (ctx.context.options.account?.storeAccountCookie) { const accountData = await getAccountCookie(ctx); if (accountData) account = accountData; } } else { const accountData = await ctx.context.internalAdapter.findAccount(providedAccountId); if (accountData) account = accountData; } if (!account || account.userId !== ctx.context.session.user.id) throw new APIError("BAD_REQUEST", { message: "Account not found" }); const provider = ctx.context.socialProviders.find((p) => p.id === account.providerId); if (!provider) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Provider account provider is ${account.providerId} but it is not configured` }); const tokens = await getAccessToken({ ...ctx, method: "POST", body: { accountId: account.id, providerId: account.providerId }, returnHeaders: false, returnStatus: false }); if (!tokens.accessToken) throw new APIError("BAD_REQUEST", { message: "Access token not found" }); const info2 = await provider.getUserInfo({ ...tokens, accessToken: tokens.accessToken }); return ctx.json(info2); }); async function createEmailVerificationToken(secret, email, updateTo, expiresIn = 3600, extraPayload) { return await signJWT({ email: email.toLowerCase(), updateTo, ...extraPayload }, secret, expiresIn); } async function sendVerificationEmailFn(ctx, user) { 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" }); } const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn); const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/"); const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user, url, token }, ctx.request)); } const sendVerificationEmail = createAuthEndpoint("/send-verification-email", { method: "POST", operationId: "sendVerificationEmail", body: z.object({ email: z.email().meta({ description: "The email to send the verification email to" }), callbackURL: z.string().meta({ description: "The URL to use for email verification callback" }).optional() }), metadata: { openapi: { operationId: "sendVerificationEmail", description: "Send a verification email to the user", requestBody: { content: { "application/json": { schema: { type: "object", properties: { email: { type: "string", description: "The email to send the verification email to", example: "user@example.com" }, callbackURL: { type: "string", description: "The URL to use for email verification callback", example: "https://example.com/callback", nullable: true } }, required: ["email"] } } } }, responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if the email was sent successfully", example: true } } } } } }, "400": { description: "Bad Request", content: { "application/json": { schema: { type: "object", properties: { message: { type: "string", description: "Error message", example: "Verification email isn't enabled" } } } } } } } } } }, 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" }); } const { email } = ctx.body; const session = await getSessionFromCtx(ctx); if (!session) { const user = await ctx.context.internalAdapter.findUserByEmail(email); if (!user || user.user.emailVerified) { await createEmailVerificationToken(ctx.context.secret, email, void 0, ctx.context.options.emailVerification?.expiresIn); return ctx.json({ status: true }); } await sendVerificationEmailFn(ctx, user.user); return ctx.json({ status: true }); } if (session?.user.email !== email) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.EMAIL_MISMATCH }); if (session?.user.emailVerified) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.EMAIL_ALREADY_VERIFIED }); await sendVerificationEmailFn(ctx, session.user); return ctx.json({ status: true }); }); const verifyEmail = createAuthEndpoint("/verify-email", { method: "GET", operationId: "verifyEmail", query: z.object({ token: z.string().meta({ description: "The token to verify the email" }), callbackURL: z.string().meta({ description: "The URL to redirect to after email verification" }).optional() }), use: [originCheck((ctx) => ctx.query.callbackURL)], metadata: { openapi: { description: "Verify the email of the user", parameters: [{ name: "token", in: "query", description: "The token to verify the email", required: true, schema: { type: "string" } }, { name: "callbackURL", in: "query", description: "The URL to redirect to after email verification", required: false, schema: { type: "string" } }], responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { user: { type: "object", $ref: "#/components/schemas/User" }, status: { type: "boolean", description: "Indicates if the email was verified successfully" } }, required: ["user", "status"] } } } } } } } }, async (ctx) => { function redirectOnError(error2) { if (ctx.query.callbackURL) { if (ctx.query.callbackURL.includes("?")) throw ctx.redirect(`${ctx.query.callbackURL}&error=${error2}`); throw ctx.redirect(`${ctx.query.callbackURL}?error=${error2}`); } throw new APIError("UNAUTHORIZED", { message: error2 }); } const { token } = ctx.query; let jwt; try { jwt = await jwtVerify(token, new TextEncoder().encode(ctx.context.secret), { algorithms: ["HS256"] }); } catch (e) { if (e instanceof JWTExpired) return redirectOnError("token_expired"); return redirectOnError("invalid_token"); } const parsed = z.object({ email: z.email(), updateTo: z.string().optional(), requestType: z.string().optional() }).parse(jwt.payload); const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email); if (!user) return redirectOnError("user_not_found"); if (parsed.updateTo) { const session = await getSessionFromCtx(ctx); if (session && session.user.email !== parsed.email) return redirectOnError("unauthorized"); switch (parsed.requestType) { case "change-email-confirmation": { const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-verification" }); const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/"); const url = `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`; if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user: { ...user.user, email: parsed.updateTo }, url, token: newToken }, ctx.request)); if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL); return ctx.json({ status: true }); } case "change-email-verification": { let activeSession = session; if (!activeSession) { const newSession = await ctx.context.internalAdapter.createSession(user.user.id); if (!newSession) throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION }); activeSession = { session: newSession, user: user.user }; } if (ctx.context.options.emailVerification?.onEmailVerification) await ctx.context.options.emailVerification.onEmailVerification(user.user, ctx.request); const updatedUser$1 = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, { email: parsed.updateTo, emailVerified: true }); if (ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser$1, ctx.request); await setSessionCookie(ctx, { session: activeSession.session, user: { ...activeSession.user, email: parsed.updateTo, emailVerified: true } }); if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL); return ctx.json({ status: true, user: parseUserOutput(ctx.context.options, updatedUser$1) }); } default: { let activeSession = session; if (!activeSession) { const newSession = await ctx.context.internalAdapter.createSession(user.user.id); if (!newSession) throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION }); activeSession = { session: newSession, user: user.user }; } const updatedUser$1 = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, { email: parsed.updateTo, emailVerified: false }); const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo); const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/"); if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user: updatedUser$1, url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`, token: newToken }, ctx.request)); await setSessionCookie(ctx, { session: activeSession.session, user: { ...activeSession.user, email: parsed.updateTo, emailVerified: false } }); if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL); return ctx.json({ status: true, user: parseUserOutput(ctx.context.options, updatedUser$1) }); } } } if (user.user.emailVerified) { if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL); return ctx.json({ status: true, user: null }); } if (ctx.context.options.emailVerification?.beforeEmailVerification) await ctx.context.options.emailVerification.beforeEmailVerification(user.user, ctx.request); if (ctx.context.options.emailVerification?.onEmailVerification) await ctx.context.options.emailVerification.onEmailVerification(user.user, ctx.request); const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, { emailVerified: true }); if (ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request); if (ctx.context.options.emailVerification?.autoSignInAfterVerification) { const currentSession = await getSessionFromCtx(ctx); if (!currentSession || currentSession.user.email !== parsed.email) { const session = await ctx.context.internalAdapter.createSession(user.user.id); if (!session) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to create session" }); await setSessionCookie(ctx, { session, user: { ...user.user, emailVerified: true } }); } else await setSessionCookie(ctx, { session: currentSession.session, user: { ...currentSession.user, emailVerified: true } }); } if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL); return ctx.json({ status: true, user: null }); }); async function handleOAuthUserInfo(c, opts) { const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts; const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => { logger.error("Better auth was unable to query your database.\nError: ", e); const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; throw c.redirect(`${errorURL}?error=internal_server_error`); }); let user = dbUser?.user; const isRegister = !user; if (dbUser) { const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId); if (!linkedAccount) { const accountLinking = c.context.options.account?.accountLinking; const trustedProviders = c.context.options.account?.accountLinking?.trustedProviders; if (!(opts.isTrustedProvider || trustedProviders?.includes(account.providerId)) && !userInfo.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) { if (isDevelopment()) logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`); return { error: "account not linked", data: null }; } try { await c.context.internalAdapter.linkAccount({ providerId: account.providerId, accountId: userInfo.id.toString(), userId: dbUser.user.id, accessToken: await setTokenUtil(account.accessToken, c.context), refreshToken: await setTokenUtil(account.refreshToken, c.context), idToken: account.idToken, accessTokenExpiresAt: account.accessTokenExpiresAt, refreshTokenExpiresAt: account.refreshTokenExpiresAt, scope: account.scope }); } catch (e) { logger.error("Unable to link account", e); return { error: "unable to link account", data: null }; } if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true }); } else { const freshTokens = c.context.options.account?.updateAccountOnSignIn !== false ? Object.fromEntries(Object.entries({ idToken: account.idToken, accessToken: await setTokenUtil(account.accessToken, c.context), refreshToken: await setTokenUtil(account.refreshToken, c.context), accessTokenExpiresAt: account.accessTokenExpiresAt, refreshTokenExpiresAt: account.refreshTokenExpiresAt, scope: account.scope }).filter(([_, value]) => value !== void 0)) : {}; if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, { ...linkedAccount, ...freshTokens }); if (Object.keys(freshTokens).length > 0) await c.context.internalAdapter.updateAccount(linkedAccount.id, freshTokens); if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true }); } if (overrideUserInfo) { const { id: _, ...restUserInfo } = userInfo; user = await c.context.internalAdapter.updateUser(dbUser.user.id, { ...restUserInfo, email: userInfo.email.toLowerCase(), emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified }); } } else { if (disableSignUp) return { error: "signup disabled", data: null, isRegister: false }; try { const { id: _, ...restUserInfo } = userInfo; const accountData = { accessToken: await setTokenUtil(account.accessToken, c.context), refreshToken: await setTokenUtil(account.refreshToken, c.context), idToken: account.idToken, accessTokenExpiresAt: account.accessTokenExpiresAt, refreshTokenExpiresAt: account.refreshTokenExpiresAt, scope: account.scope, providerId: account.providerId, accountId: userInfo.id.toString() }; const { user: createdUser, account: createdAccount } = await c.context.internalAdapter.createOAuthUser({ ...restUserInfo, email: userInfo.email.toLowerCase() }, accountData); user = createdUser; if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount); if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp && c.context.options.emailVerification?.sendVerificationEmail) { const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn); const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; await c.context.runInBackgroundOrAwait(c.context.options.emailVerification.sendVerificationEmail({ user, url, token }, c.request)); } } catch (e) { logger.error(e); if (e instanceof APIError) return { error: e.message, data: null, isRegister: false }; return { error: "unable to create user", data: null, isRegister: false }; } } if (!user) return { error: "unable to create user", data: null, isRegister: false }; const session = await c.context.internalAdapter.createSession(user.id); if (!session) return { error: "unable to create session", data: null, isRegister: false }; return { data: { session, user }, error: null, isRegister }; } const HIDE_METADATA = { scope: "server" }; const schema = z.object({ code: z.string().optional(), error: z.string().optional(), device_id: z.string().optional(), error_description: z.string().optional(), state: z.string().optional(), user: z.string().optional() }); const callbackOAuth = createAuthEndpoint("/callback/:id", { method: ["GET", "POST"], operationId: "handleOAuthCallback", body: schema.optional(), query: schema.optional(), metadata: { ...HIDE_METADATA, allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"] } }, async (c) => { let queryOrBody; const defaultErrorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; if (c.method === "POST") { const postData = c.body ? schema.parse(c.body) : {}; const queryData = c.query ? schema.parse(c.query) : {}; const mergedData = schema.parse({ ...postData, ...queryData }); const params = new URLSearchParams(); for (const [key, value] of Object.entries(mergedData)) if (value !== void 0 && value !== null) params.set(key, String(value)); const redirectURL = `${c.context.baseURL}/callback/${c.params.id}?${params.toString()}`; throw c.redirect(redirectURL); } try { if (c.method === "GET") queryOrBody = schema.parse(c.query); else if (c.method === "POST") queryOrBody = schema.parse(c.body); else throw new Error("Unsupported method"); } catch (e) { c.context.logger.error("INVALID_CALLBACK_REQUEST", e); throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`); } const { code, error: error2, state, error_description, device_id, user: userData } = queryOrBody; if (!state) { c.context.logger.error("State not found", error2); const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}state=state_not_found`; throw c.redirect(url); } const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp } = await parseState(c); function redirectOnError(error$1, description) { const baseURL = errorURL ?? defaultErrorURL; const params = new URLSearchParams({ error: error$1 }); if (description) params.set("error_description", description); const url = `${baseURL}${baseURL.includes("?") ? "&" : "?"}${params.toString()}`; throw c.redirect(url); } if (error2) redirectOnError(error2, error_description); if (!code) { c.context.logger.error("Code not found"); throw redirectOnError("no_code"); } const provider = c.context.socialProviders.find((p) => p.id === c.params.id); if (!provider) { c.context.logger.error("Oauth provider with id", c.params.id, "not found"); throw redirectOnError("oauth_provider_not_found"); } let tokens; try { tokens = await provider.validateAuthorizationCode({ code, codeVerifier, deviceId: device_id, redirectURI: `${c.context.baseURL}/callback/${provider.id}` }); } catch (e) { c.context.logger.error("", e); throw redirectOnError("invalid_code"); } if (!tokens) throw redirectOnError("invalid_code"); const parsedUserData = userData ? safeJSONParse(userData) : null; const userInfo = await provider.getUserInfo({ ...tokens, user: parsedUserData ?? void 0 }).then((res) => res?.user); if (!userInfo) { c.context.logger.error("Unable to get user info"); return redirectOnError("unable_to_get_user_info"); } if (!callbackURL) { c.context.logger.error("No callback URL found"); throw redirectOnError("no_callback_url"); } if (link) { if (!c.context.options.account?.accountLinking?.trustedProviders?.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) { c.context.logger.error("Unable to link account - untrusted provider"); return redirectOnError("unable_to_link_account"); } if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match"); const existingAccount = await c.context.internalAdapter.findAccount(String(userInfo.id)); if (existingAccount) { if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user"); const updateData = Object.fromEntries(Object.entries({ accessToken: await setTokenUtil(tokens.accessToken, c.context), refreshToken: await setTokenUtil(tokens.refreshToken, c.context), idToken: tokens.idToken, accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(",") }).filter(([_, value]) => value !== void 0)); await c.context.internalAdapter.updateAccount(existingAccount.id, updateData); } else if (!await c.context.internalAdapter.createAccount({ userId: link.userId, providerId: provider.id, accountId: String(userInfo.id), ...tokens, accessToken: await setTokenUtil(tokens.accessToken, c.context), refreshToken: await setTokenUtil(tokens.refreshToken, c.context), scope: tokens.scopes?.join(",") })) return redirectOnError("unable_to_link_account"); let toRedirectTo$1; try { toRedirectTo$1 = callbackURL.toString(); } catch { toRedirectTo$1 = callbackURL; } throw c.redirect(toRedirectTo$1); } if (!userInfo.email) { c.context.logger.error("Provider did not return email. This could be due to misconfiguration in the provider settings."); return redirectOnError("email_not_found"); } const accountData = { providerId: provider.id, accountId: String(userInfo.id), ...tokens, scope: tokens.scopes?.join(",") }; const result = await handleOAuthUserInfo(c, { userInfo: { ...userInfo, id: String(userInfo.id), email: userInfo.email, name: userInfo.name || userInfo.email }, account: accountData, callbackURL, disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.options?.disableSignUp, overrideUserInfo: provider.options?.overrideUserInfoOnSignIn }); if (result.error) { c.context.logger.error(result.error.split(" ").join("_")); return redirectOnError(result.error.split(" ").join("_")); } const { session, user } = result.data; await setSessionCookie(c, { session, user }); let toRedirectTo; try { toRedirectTo = (result.isRegister ? newUserURL || callbackURL : callbackURL).toString(); } catch { toRedirectTo = result.isRegister ? newUserURL || callbackURL : callbackURL; } throw c.redirect(toRedirectTo); }); function sanitize(input) { return input.replace(//g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/&(?!amp;|lt;|gt;|quot;|#39;|#x[0-9a-fA-F]+;|#[0-9]+;)/g, "&"); } const html = (options, code = "Unknown", description = null) => { const custom = options.onAPIError?.customizeDefaultErrorPage; return ` Error
${custom?.disableBackgroundGrid ? "" : `
`}
${custom?.disableCornerDecorations ? "" : `
`}

ERROR

Something went wrong

CODE: ${sanitize(code)}

${!description ? `We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find more information about the error here.` : description}

Go Home
Ask AI
`; }; const error = createAuthEndpoint("/error", { method: "GET", metadata: { ...HIDE_METADATA, openapi: { description: "Displays an error page", responses: { "200": { description: "Success", content: { "text/html": { schema: { type: "string", description: "The HTML content of the error page" } } } } } } } }, async (c) => { const url = new URL(c.request?.url || ""); const unsanitizedCode = url.searchParams.get("error") || "UNKNOWN"; const unsanitizedDescription = url.searchParams.get("error_description") || null; const safeCode = /^[\'A-Za-z0-9_-]+$/.test(unsanitizedCode) ? unsanitizedCode : "UNKNOWN"; const safeDescription = unsanitizedDescription ? sanitize(unsanitizedDescription) : null; const queryParams = new URLSearchParams(); queryParams.set("error", safeCode); if (unsanitizedDescription) queryParams.set("error_description", unsanitizedDescription); const options = c.context.options; const errorURL = options.onAPIError?.errorURL; if (errorURL) return new Response(null, { status: 302, headers: { Location: `${errorURL}${errorURL.includes("?") ? "&" : "?"}${queryParams.toString()}` } }); if (isProduction && !options.onAPIError?.customizeDefaultErrorPage) return new Response(null, { status: 302, headers: { Location: `/?${queryParams.toString()}` } }); return new Response(html(c.context.options, safeCode, safeDescription), { headers: { "Content-Type": "text/html" } }); }); const ok = createAuthEndpoint("/ok", { method: "GET", metadata: { ...HIDE_METADATA, openapi: { description: "Check if the API is working", responses: { "200": { description: "API is working", content: { "application/json": { schema: { type: "object", properties: { ok: { type: "boolean", description: "Indicates if the API is working" } }, required: ["ok"] } } } } } } } }, async (ctx) => { return ctx.json({ ok: true }); }); async function validatePassword(ctx, data) { const credentialAccount = (await ctx.context.internalAdapter.findAccounts(data.userId))?.find((account) => account.providerId === "credential"); const currentPassword = credentialAccount?.password; if (!credentialAccount || !currentPassword) return false; return await ctx.context.password.verify({ hash: currentPassword, password: data.password }); } async function checkPassword(userId, c) { const credentialAccount = (await c.context.internalAdapter.findAccounts(userId))?.find((account) => account.providerId === "credential"); const currentPassword = credentialAccount?.password; if (!credentialAccount || !currentPassword || !c.body.password) throw new APIError("BAD_REQUEST", { message: "No password credential found" }); if (!await c.context.password.verify({ hash: currentPassword, password: c.body.password })) throw new APIError("BAD_REQUEST", { message: "Invalid password" }); return true; } function redirectError(ctx, callbackURL, query) { const url = callbackURL ? new URL(callbackURL, ctx.baseURL) : new URL(`${ctx.baseURL}/error`); if (query) Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v)); return url.href; } function redirectCallback(ctx, callbackURL, query) { const url = new URL(callbackURL, ctx.baseURL); if (query) Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v)); return url.href; } const requestPasswordReset = createAuthEndpoint("/request-password-reset", { method: "POST", body: z.object({ email: z.email().meta({ description: "The email address of the user to send a password reset email to" }), redirectTo: z.string().meta({ description: "The URL to redirect the user to reset their password. If the token isn't valid or expired, it'll be redirected with a query parameter `?error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?token=VALID_TOKEN" }).optional() }), metadata: { openapi: { operationId: "requestPasswordReset", description: "Send a password reset email to the user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" }, message: { type: "string" } } } } } } } } } }, async (ctx) => { if (!ctx.context.options.emailAndPassword?.sendResetPassword) { ctx.context.logger.error("Reset password isn't enabled.Please pass an emailAndPassword.sendResetPassword function in your auth config!"); throw new APIError("BAD_REQUEST", { message: "Reset password isn't enabled" }); } const { email, redirectTo } = ctx.body; const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true }); if (!user) { generateId(24); await ctx.context.internalAdapter.findVerificationValue("dummy-verification-token"); ctx.context.logger.error("Reset Password: User not found", { email }); return ctx.json({ status: true, message: "If this email exists in our system, check your email for the reset link" }); } const expiresAt = getDate(ctx.context.options.emailAndPassword.resetPasswordTokenExpiresIn || 3600 * 1, "sec"); const verificationToken = generateId(24); await ctx.context.internalAdapter.createVerificationValue({ value: user.user.id, identifier: `reset-password:${verificationToken}`, expiresAt }); const callbackURL = redirectTo ? encodeURIComponent(redirectTo) : ""; const url = `${ctx.context.baseURL}/reset-password/${verificationToken}?callbackURL=${callbackURL}`; await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailAndPassword.sendResetPassword({ user: user.user, url, token: verificationToken }, ctx.request)); return ctx.json({ status: true, message: "If this email exists in our system, check your email for the reset link" }); }); const requestPasswordResetCallback = createAuthEndpoint("/reset-password/:token", { method: "GET", operationId: "forgetPasswordCallback", query: z.object({ callbackURL: z.string().meta({ description: "The URL to redirect the user to reset their password" }) }), use: [originCheck((ctx) => ctx.query.callbackURL)], metadata: { openapi: { operationId: "resetPasswordCallback", description: "Redirects the user to the callback URL with the token", parameters: [{ name: "token", in: "path", required: true, description: "The token to reset the password", schema: { type: "string" } }, { name: "callbackURL", in: "query", required: true, description: "The URL to redirect the user to reset their password", schema: { type: "string" } }], responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { token: { type: "string" } } } } } } } } } }, async (ctx) => { const { token } = ctx.params; const { callbackURL } = ctx.query; if (!token || !callbackURL) throw ctx.redirect(redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" })); const verification = await ctx.context.internalAdapter.findVerificationValue(`reset-password:${token}`); if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) throw ctx.redirect(redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" })); throw ctx.redirect(redirectCallback(ctx.context, callbackURL, { token })); }); const resetPassword = createAuthEndpoint("/reset-password", { method: "POST", operationId: "resetPassword", query: z.object({ token: z.string().optional() }).optional(), body: z.object({ newPassword: z.string().meta({ description: "The new password to set" }), token: z.string().meta({ description: "The token to reset the password" }).optional() }), metadata: { openapi: { operationId: "resetPassword", description: "Reset the password for a user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" } } } } } } } } } }, async (ctx) => { const token = ctx.body.token || ctx.query?.token; if (!token) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_TOKEN }); const { newPassword } = ctx.body; 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 }); if (newPassword.length > maxLength) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG }); const id = `reset-password:${token}`; const verification = await ctx.context.internalAdapter.findVerificationValue(id); if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_TOKEN }); const userId = verification.value; const hashedPassword = await ctx.context.password.hash(newPassword); if (!(await ctx.context.internalAdapter.findAccounts(userId)).find((ac) => ac.providerId === "credential")) await ctx.context.internalAdapter.createAccount({ userId, providerId: "credential", password: hashedPassword, accountId: userId }); else await ctx.context.internalAdapter.updatePassword(userId, hashedPassword); await ctx.context.internalAdapter.deleteVerificationValue(verification.id); if (ctx.context.options.emailAndPassword?.onPasswordReset) { const user = await ctx.context.internalAdapter.findUserById(userId); if (user) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request); } if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteSessions(userId); return ctx.json({ status: true }); }); const verifyPassword = createAuthEndpoint("/verify-password", { method: "POST", body: z.object({ password: z.string().meta({ description: "The password to verify" }) }), metadata: { scope: "server", openapi: { operationId: "verifyPassword", description: "Verify the current user's password", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" } } } } } } } } }, use: [sensitiveSessionMiddleware] }, async (ctx) => { const { password } = ctx.body; const session = ctx.context.session; if (!await validatePassword(ctx, { password, userId: session.user.id })) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD }); return ctx.json({ status: true }); }); const socialSignInBodySchema = z.object({ callbackURL: z.string().meta({ description: "Callback URL to redirect to after the user has signed in" }).optional(), newUserCallbackURL: z.string().optional(), errorCallbackURL: z.string().meta({ description: "Callback URL to redirect to if an error happens" }).optional(), provider: SocialProviderListEnum, disableRedirect: z.boolean().meta({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself" }).optional(), idToken: z.optional(z.object({ token: z.string().meta({ description: "ID token from the provider" }), nonce: z.string().meta({ description: "Nonce used to generate the token" }).optional(), accessToken: z.string().meta({ description: "Access token from the provider" }).optional(), refreshToken: z.string().meta({ description: "Refresh token from the provider" }).optional(), expiresAt: z.number().meta({ description: "Expiry date of the token" }).optional() })), scopes: z.array(z.string()).meta({ description: "Array of scopes to request from the provider. This will override the default scopes passed." }).optional(), requestSignUp: z.boolean().meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }).optional(), loginHint: z.string().meta({ description: "The login hint to use for the authorization code request" }).optional(), additionalData: z.record(z.string(), z.any()).optional().meta({ description: "Additional data to be passed through the OAuth flow" }) }); const signInSocial = () => createAuthEndpoint("/sign-in/social", { method: "POST", operationId: "socialSignIn", body: socialSignInBodySchema, metadata: { $Infer: { body: {}, returned: {} }, openapi: { description: "Sign in with a social provider", operationId: "socialSignIn", responses: { "200": { description: "Success - Returns either session details or redirect URL", content: { "application/json": { schema: { type: "object", description: "Session response when idToken is provided", properties: { token: { type: "string" }, user: { type: "object", $ref: "#/components/schemas/User" }, url: { type: "string" }, redirect: { type: "boolean", enum: [false] } }, required: [ "redirect", "token", "user" ] } } } } } } } }, async (c) => { const provider = c.context.socialProviders.find((p) => p.id === c.body.provider); if (!provider) { c.context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: c.body.provider }); throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND }); } if (c.body.idToken) { if (!provider.verifyIdToken) { c.context.logger.error("Provider does not support id token verification", { provider: c.body.provider }); throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED }); } const { token, nonce } = c.body.idToken; if (!await provider.verifyIdToken(token, nonce)) { c.context.logger.error("Invalid id token", { provider: c.body.provider }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_TOKEN }); } const userInfo = await provider.getUserInfo({ idToken: token, accessToken: c.body.idToken.accessToken, refreshToken: c.body.idToken.refreshToken }); if (!userInfo || !userInfo?.user) { 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 }); } 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 }); } const data = await handleOAuthUserInfo(c, { userInfo: { ...userInfo.user, email: userInfo.user.email, id: String(userInfo.user.id), name: userInfo.user.name || "", image: userInfo.user.image, emailVerified: userInfo.user.emailVerified || false }, account: { providerId: provider.id, accountId: String(userInfo.user.id), accessToken: c.body.idToken.accessToken }, callbackURL: c.body.callbackURL, disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.disableSignUp }); if (data.error) throw new APIError("UNAUTHORIZED", { message: data.error }); await setSessionCookie(c, data.data); return c.json({ redirect: false, token: data.data.session.token, url: void 0, user: parseUserOutput(c.context.options, data.data.user) }); } const { codeVerifier, state } = await generateState(c, void 0, c.body.additionalData); const url = await provider.createAuthorizationURL({ state, codeVerifier, redirectURI: `${c.context.baseURL}/callback/${provider.id}`, scopes: c.body.scopes, loginHint: c.body.loginHint }); if (!c.body.disableRedirect) c.setHeader("Location", url.toString()); return c.json({ url: url.toString(), redirect: !c.body.disableRedirect }); }); const signInEmail = () => createAuthEndpoint("/sign-in/email", { method: "POST", operationId: "signInEmail", use: [formCsrfMiddleware], body: z.object({ email: z.string().meta({ description: "Email of the user" }), password: z.string().meta({ description: "Password of the user" }), callbackURL: z.string().meta({ description: "Callback URL to use as a redirect for email verification" }).optional(), rememberMe: z.boolean().meta({ description: "If this is false, the session will not be remembered. Default is `true`." }).default(true).optional() }), metadata: { allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"], $Infer: { body: {}, returned: {} }, openapi: { operationId: "signInEmail", description: "Sign in with email and password", responses: { "200": { description: "Success - Returns either session details or redirect URL", content: { "application/json": { schema: { type: "object", description: "Session response when idToken is provided", properties: { redirect: { type: "boolean", enum: [false] }, token: { type: "string", description: "Session token" }, url: { type: "string", nullable: true }, user: { type: "object", $ref: "#/components/schemas/User" } }, required: [ "redirect", "token", "user" ] } } } } } } } }, async (ctx) => { if (!ctx.context.options?.emailAndPassword?.enabled) { 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", { message: "Email and password is not enabled" }); } const { email, password } = ctx.body; if (!z.email().safeParse(email).success) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL }); const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true }); if (!user) { 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 }); } const credentialAccount = user.accounts.find((a) => a.providerId === "credential"); 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 }); } 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 }); } if (!await ctx.context.password.verify({ hash: currentPassword, password })) { ctx.context.logger.error("Invalid password"); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD }); } if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.user.emailVerified) { if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED }); if (ctx.context.options?.emailVerification?.sendOnSignIn) { const token = await createEmailVerificationToken(ctx.context.secret, user.user.email, void 0, ctx.context.options.emailVerification?.expiresIn); const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/"); const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user: user.user, url, token }, ctx.request)); } throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED }); } const session = await ctx.context.internalAdapter.createSession(user.user.id, ctx.body.rememberMe === false); if (!session) { ctx.context.logger.error("Failed to create session"); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION }); } await setSessionCookie(ctx, { session, user: user.user }, ctx.body.rememberMe === false); if (ctx.body.callbackURL) ctx.setHeader("Location", ctx.body.callbackURL); return ctx.json({ redirect: !!ctx.body.callbackURL, token: session.token, url: ctx.body.callbackURL, user: parseUserOutput(ctx.context.options, user.user) }); }); const signOut = createAuthEndpoint("/sign-out", { method: "POST", operationId: "signOut", requireHeaders: true, metadata: { openapi: { operationId: "signOut", description: "Sign out the current user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" } } } } } } } } } }, async (ctx) => { const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret); if (sessionCookieToken) try { await ctx.context.internalAdapter.deleteSession(sessionCookieToken); } catch (e) { ctx.context.logger.error("Failed to delete session from database", e); } deleteSessionCookie(ctx); return ctx.json({ success: true }); }); const signUpEmailBodySchema = z.object({ name: z.string(), email: z.email(), password: z.string().nonempty(), image: z.string().optional(), callbackURL: z.string().optional(), rememberMe: z.boolean().optional() }).and(z.record(z.string(), z.any())); const signUpEmail = () => createAuthEndpoint("/sign-up/email", { method: "POST", operationId: "signUpWithEmailAndPassword", use: [formCsrfMiddleware], body: signUpEmailBodySchema, metadata: { allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"], $Infer: { body: {}, returned: {} }, openapi: { operationId: "signUpWithEmailAndPassword", description: "Sign up a user using email and password", requestBody: { content: { "application/json": { schema: { type: "object", properties: { name: { type: "string", description: "The name of the user" }, email: { type: "string", description: "The email of the user" }, password: { type: "string", description: "The password of the user" }, image: { type: "string", description: "The profile image URL of the user" }, callbackURL: { type: "string", description: "The URL to use for email verification callback" }, rememberMe: { type: "boolean", description: "If this is false, the session will not be remembered. Default is `true`." } }, required: [ "name", "email", "password" ] } } } }, responses: { "200": { description: "Successfully created user", content: { "application/json": { schema: { type: "object", properties: { token: { type: "string", nullable: true, description: "Authentication token for the session" }, user: { type: "object", properties: { id: { type: "string", description: "The unique identifier of the user" }, email: { type: "string", format: "email", description: "The email address of the user" }, name: { type: "string", description: "The name of the user" }, image: { type: "string", format: "uri", nullable: true, description: "The profile image URL of the user" }, emailVerified: { type: "boolean", description: "Whether the email has been verified" }, createdAt: { type: "string", format: "date-time", description: "When the user was created" }, updatedAt: { type: "string", format: "date-time", description: "When the user was last updated" } }, required: [ "id", "email", "name", "emailVerified", "createdAt", "updatedAt" ] } }, required: ["user"] } } } }, "422": { description: "Unprocessable Entity. User already exists or failed to create user.", content: { "application/json": { schema: { type: "object", properties: { message: { type: "string" } } } } } } } } } }, async (ctx) => { return runWithTransaction(ctx.context.adapter, async () => { if (!ctx.context.options.emailAndPassword?.enabled || ctx.context.options.emailAndPassword?.disableSignUp) throw new APIError("BAD_REQUEST", { message: "Email and password sign up is not enabled" }); const body = ctx.body; const { name, email, password, image, callbackURL: _callbackURL, rememberMe, ...rest } = body; if (!z.email().safeParse(email).success) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL }); if (!password || typeof password !== "string") throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD }); 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 }); } 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 }); } if ((await ctx.context.internalAdapter.findUserByEmail(email))?.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 }); } const hash = await ctx.context.password.hash(password); let createdUser; try { const data = parseUserInput(ctx.context.options, rest, "create"); createdUser = await ctx.context.internalAdapter.createUser({ email: email.toLowerCase(), name, image, ...data, emailVerified: false }); if (!createdUser) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER }); } catch (e) { if (isDevelopment()) ctx.context.logger.error("Failed to create user", e); if (e instanceof APIError) throw e; ctx.context.logger?.error("Failed to create user", e); throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER }); } if (!createdUser) throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER }); await ctx.context.internalAdapter.linkAccount({ userId: createdUser.id, providerId: "credential", accountId: createdUser.id, password: hash }); if (ctx.context.options.emailVerification?.sendOnSignUp ?? ctx.context.options.emailAndPassword.requireEmailVerification) { const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn); const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/"); const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user: createdUser, url, token }, ctx.request)); } if (ctx.context.options.emailAndPassword.autoSignIn === false || ctx.context.options.emailAndPassword.requireEmailVerification) return ctx.json({ token: null, user: parseUserOutput(ctx.context.options, createdUser) }); const session = await ctx.context.internalAdapter.createSession(createdUser.id, rememberMe === false); if (!session) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION }); await setSessionCookie(ctx, { session, user: createdUser }, rememberMe === false); return ctx.json({ token: session.token, user: parseUserOutput(ctx.context.options, createdUser) }); }); }); const updateUserBodySchema = z.record(z.string().meta({ description: "Field name must be a string" }), z.any()); const updateUser = () => createAuthEndpoint("/update-user", { method: "POST", operationId: "updateUser", body: updateUserBodySchema, use: [sessionMiddleware], metadata: { $Infer: { body: {} }, openapi: { operationId: "updateUser", description: "Update the current user", requestBody: { content: { "application/json": { schema: { type: "object", properties: { name: { type: "string", description: "The name of the user" }, image: { type: "string", description: "The image of the user", nullable: true } } } } } }, responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { user: { type: "object", $ref: "#/components/schemas/User" } } } } } } } } } }, async (ctx) => { const body = ctx.body; if (typeof body !== "object" || Array.isArray(body)) throw new APIError("BAD_REQUEST", { message: "Body must be an object" }); if (body.email) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.EMAIL_CAN_NOT_BE_UPDATED }); const { name, image, ...rest } = body; const session = ctx.context.session; const additionalFields = parseUserInput(ctx.context.options, rest, "update"); if (image === void 0 && name === void 0 && Object.keys(additionalFields).length === 0) throw new APIError("BAD_REQUEST", { message: "No fields to update" }); const updatedUser = await ctx.context.internalAdapter.updateUser(session.user.id, { name, image, ...additionalFields }) ?? { ...session.user, ...name !== void 0 && { name }, ...image !== void 0 && { image }, ...additionalFields }; await setSessionCookie(ctx, { session: session.session, user: updatedUser }); return ctx.json({ status: true }); }); const changePassword = createAuthEndpoint("/change-password", { method: "POST", operationId: "changePassword", body: z.object({ newPassword: z.string().meta({ description: "The new password to set" }), currentPassword: z.string().meta({ description: "The current password is required" }), revokeOtherSessions: z.boolean().meta({ description: "Must be a boolean value" }).optional() }), use: [sensitiveSessionMiddleware], metadata: { openapi: { operationId: "changePassword", description: "Change the password of the user", responses: { "200": { description: "Password successfully changed", content: { "application/json": { schema: { type: "object", properties: { token: { type: "string", nullable: true, description: "New session token if other sessions were revoked" }, user: { type: "object", properties: { id: { type: "string", description: "The unique identifier of the user" }, email: { type: "string", format: "email", description: "The email address of the user" }, name: { type: "string", description: "The name of the user" }, image: { type: "string", format: "uri", nullable: true, description: "The profile image URL of the user" }, emailVerified: { type: "boolean", description: "Whether the email has been verified" }, createdAt: { type: "string", format: "date-time", description: "When the user was created" }, updatedAt: { type: "string", format: "date-time", description: "When the user was last updated" } }, required: [ "id", "email", "name", "emailVerified", "createdAt", "updatedAt" ] } }, required: ["user"] } } } } } } } }, async (ctx) => { const { newPassword, currentPassword, revokeOtherSessions: revokeOtherSessions2 } = ctx.body; const session = ctx.context.session; 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 }); } 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 }); } const account = (await ctx.context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password); if (!account || !account.password) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND }); const passwordHash = await ctx.context.password.hash(newPassword); if (!await ctx.context.password.verify({ hash: account.password, password: currentPassword })) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD }); await ctx.context.internalAdapter.updateAccount(account.id, { password: passwordHash }); let token = null; if (revokeOtherSessions2) { await ctx.context.internalAdapter.deleteSessions(session.user.id); const newSession = await ctx.context.internalAdapter.createSession(session.user.id); if (!newSession) throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION }); await setSessionCookie(ctx, { session: newSession, user: session.user }); token = newSession.token; } return ctx.json({ token, user: parseUserOutput(ctx.context.options, session.user) }); }); const setPassword = createAuthEndpoint({ method: "POST", body: z.object({ newPassword: z.string().meta({ description: "The new password to set is required" }) }), use: [sensitiveSessionMiddleware] }, async (ctx) => { const { newPassword } = ctx.body; const session = ctx.context.session; 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 }); } 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 }); } const account = (await ctx.context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password); const passwordHash = await ctx.context.password.hash(newPassword); if (!account) { await ctx.context.internalAdapter.linkAccount({ userId: session.user.id, providerId: "credential", accountId: session.user.id, password: passwordHash }); return ctx.json({ status: true }); } throw new APIError("BAD_REQUEST", { message: "user already has a password" }); }); const deleteUser = createAuthEndpoint("/delete-user", { method: "POST", use: [sensitiveSessionMiddleware], body: z.object({ callbackURL: z.string().meta({ description: "The callback URL to redirect to after the user is deleted" }).optional(), password: z.string().meta({ description: "The password of the user is required to delete the user" }).optional(), token: z.string().meta({ description: "The token to delete the user is required" }).optional() }), metadata: { openapi: { operationId: "deleteUser", description: "Delete the user", requestBody: { content: { "application/json": { schema: { type: "object", properties: { callbackURL: { type: "string", description: "The callback URL to redirect to after the user is deleted" }, password: { type: "string", description: "The user's password. Required if session is not fresh" }, token: { type: "string", description: "The deletion verification token" } } } } } }, responses: { "200": { description: "User deletion processed successfully", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean", description: "Indicates if the operation was successful" }, message: { type: "string", enum: ["User deleted", "Verification email sent"], description: "Status message of the deletion process" } }, required: ["success", "message"] } } } } } } } }, async (ctx) => { if (!ctx.context.options.user?.deleteUser?.enabled) { ctx.context.logger.error("Delete user is disabled. Enable it in the options"); throw new APIError("NOT_FOUND"); } const session = ctx.context.session; if (ctx.body.password) { const account = (await ctx.context.internalAdapter.findAccounts(session.user.id)).find((account$1) => account$1.providerId === "credential" && account$1.password); if (!account || !account.password) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND }); if (!await ctx.context.password.verify({ hash: account.password, password: ctx.body.password })) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD }); } if (ctx.body.token) { await deleteUserCallback({ ...ctx, query: { token: ctx.body.token } }); return ctx.json({ success: true, message: "User deleted" }); } if (ctx.context.options.user.deleteUser?.sendDeleteAccountVerification) { const token = generateRandomString(32, "0-9", "a-z"); await ctx.context.internalAdapter.createVerificationValue({ value: session.user.id, identifier: `delete-account-${token}`, expiresAt: new Date(Date.now() + (ctx.context.options.user.deleteUser?.deleteTokenExpiresIn || 3600 * 24) * 1e3) }); const url = `${ctx.context.baseURL}/delete-user/callback?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`; await ctx.context.runInBackgroundOrAwait(ctx.context.options.user.deleteUser.sendDeleteAccountVerification({ user: session.user, url, token }, ctx.request)); return ctx.json({ success: true, message: "Verification email sent" }); } if (!ctx.body.password && ctx.context.sessionConfig.freshAge !== 0) { const currentAge = new Date(session.session.createdAt).getTime(); const freshAge = ctx.context.sessionConfig.freshAge * 1e3; if (Date.now() - currentAge > freshAge * 1e3) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.SESSION_EXPIRED }); } const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete; if (beforeDelete) await beforeDelete(session.user, ctx.request); await ctx.context.internalAdapter.deleteUser(session.user.id); await ctx.context.internalAdapter.deleteSessions(session.user.id); deleteSessionCookie(ctx); const afterDelete = ctx.context.options.user.deleteUser?.afterDelete; if (afterDelete) await afterDelete(session.user, ctx.request); return ctx.json({ success: true, message: "User deleted" }); }); const deleteUserCallback = createAuthEndpoint("/delete-user/callback", { method: "GET", query: z.object({ token: z.string().meta({ description: "The token to verify the deletion request" }), callbackURL: z.string().meta({ description: "The URL to redirect to after deletion" }).optional() }), use: [originCheck((ctx) => ctx.query.callbackURL)], metadata: { openapi: { description: "Callback to complete user deletion with verification token", responses: { "200": { description: "User successfully deleted", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean", description: "Indicates if the deletion was successful" }, message: { type: "string", enum: ["User deleted"], description: "Confirmation message" } }, required: ["success", "message"] } } } } } } } }, async (ctx) => { if (!ctx.context.options.user?.deleteUser?.enabled) { ctx.context.logger.error("Delete user is disabled. Enable it in the options"); throw new APIError("NOT_FOUND"); } const session = await getSessionFromCtx(ctx); if (!session) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO }); const token = await ctx.context.internalAdapter.findVerificationValue(`delete-account-${ctx.query.token}`); if (!token || token.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.INVALID_TOKEN }); if (token.value !== session.user.id) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.INVALID_TOKEN }); const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete; if (beforeDelete) await beforeDelete(session.user, ctx.request); await ctx.context.internalAdapter.deleteUser(session.user.id); await ctx.context.internalAdapter.deleteSessions(session.user.id); await ctx.context.internalAdapter.deleteAccounts(session.user.id); await ctx.context.internalAdapter.deleteVerificationValue(token.id); deleteSessionCookie(ctx); const afterDelete = ctx.context.options.user.deleteUser?.afterDelete; if (afterDelete) await afterDelete(session.user, ctx.request); if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL || "/"); return ctx.json({ success: true, message: "User deleted" }); }); const changeEmail = createAuthEndpoint("/change-email", { method: "POST", body: z.object({ newEmail: z.email().meta({ description: "The new email address to set must be a valid email address" }), callbackURL: z.string().meta({ description: "The URL to redirect to after email verification" }).optional() }), use: [sensitiveSessionMiddleware], metadata: { openapi: { operationId: "changeEmail", responses: { "200": { description: "Email change request processed successfully", content: { "application/json": { schema: { type: "object", properties: { user: { type: "object", $ref: "#/components/schemas/User" }, status: { type: "boolean", description: "Indicates if the request was successful" }, message: { type: "string", enum: ["Email updated", "Verification email sent"], description: "Status message of the email change process", nullable: true } }, required: ["status"] } } } }, "422": { description: "Unprocessable Entity. Email already exists", content: { "application/json": { schema: { type: "object", properties: { message: { type: "string" } } } } } } } } } }, async (ctx) => { if (!ctx.context.options.user?.changeEmail?.enabled) { ctx.context.logger.error("Change email is disabled."); throw new APIError("BAD_REQUEST", { message: "Change email is disabled" }); } const newEmail = ctx.body.newEmail.toLowerCase(); if (newEmail === ctx.context.session.user.email) { ctx.context.logger.error("Email is the same"); throw new APIError("BAD_REQUEST", { message: "Email is the same" }); } if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) { ctx.context.logger.error("Email already exists"); throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL }); } if (ctx.context.session.user.emailVerified !== true && ctx.context.options.user.changeEmail.updateEmailWithoutVerification) { await ctx.context.internalAdapter.updateUserByEmail(ctx.context.session.user.email, { email: newEmail }); await setSessionCookie(ctx, { session: ctx.context.session.session, user: { ...ctx.context.session.user, email: newEmail } }); if (ctx.context.options.emailVerification?.sendVerificationEmail) { const token$1 = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn); const url$1 = `${ctx.context.baseURL}/verify-email?token=${token$1}&callbackURL=${ctx.body.callbackURL || "/"}`; await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user: { ...ctx.context.session.user, email: newEmail }, url: url$1, token: token$1 }, ctx.request)); } return ctx.json({ status: true }); } if (ctx.context.session.user.emailVerified && (ctx.context.options.user.changeEmail.sendChangeEmailConfirmation || ctx.context.options.user.changeEmail.sendChangeEmailVerification)) { const token$1 = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-confirmation" }); const url$1 = `${ctx.context.baseURL}/verify-email?token=${token$1}&callbackURL=${ctx.body.callbackURL || "/"}`; const sendFn = ctx.context.options.user.changeEmail.sendChangeEmailConfirmation || ctx.context.options.user.changeEmail.sendChangeEmailVerification; if (sendFn) await ctx.context.runInBackgroundOrAwait(sendFn({ user: ctx.context.session.user, newEmail, url: url$1, token: token$1 }, ctx.request)); return ctx.json({ status: true }); } 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" }); } const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { requestType: "change-email-verification" }); const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`; await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({ user: { ...ctx.context.session.user, email: newEmail }, url, token }, ctx.request)); return ctx.json({ status: true }); }); const defuReplaceArrays = createDefu((obj, key, value) => { if (Array.isArray(obj[key]) && Array.isArray(value)) { obj[key] = value; return true; } }); const hooksSourceWeakMap = /* @__PURE__ */ new WeakMap(); function toAuthEndpoints(endpoints, ctx) { const api = {}; for (const [key, endpoint] of Object.entries(endpoints)) { api[key] = async (context) => { const run = async () => { const authContext = await ctx; let internalContext = { ...context, context: { ...authContext, returned: void 0, responseHeaders: void 0, session: null }, path: endpoint.path, headers: context?.headers ? new Headers(context?.headers) : void 0 }; return runWithEndpointContext(internalContext, async () => { const { beforeHooks, afterHooks } = getHooks(authContext); const before = await runBeforeHooks(internalContext, beforeHooks); if ("context" in before && before.context && typeof before.context === "object") { const { headers, ...rest } = before.context; if (headers) headers.forEach((value, key$1) => { internalContext.headers.set(key$1, value); }); internalContext = defuReplaceArrays(rest, internalContext); } else if (before) return context?.asResponse ? toResponse(before, { headers: context?.headers }) : context?.returnHeaders ? { headers: context?.headers, response: before } : before; internalContext.asResponse = false; internalContext.returnHeaders = true; internalContext.returnStatus = true; const result = await runWithEndpointContext(internalContext, () => endpoint(internalContext)).catch((e) => { if (e instanceof APIError) return { response: e, status: e.statusCode, headers: e.headers ? new Headers(e.headers) : null }; throw e; }); if (result && result instanceof Response) return result; internalContext.context.returned = result.response; internalContext.context.responseHeaders = result.headers; const after = await runAfterHooks(internalContext, afterHooks); if (after.response) result.response = after.response; if (result.response instanceof APIError && shouldPublishLog(authContext.logger.level, "debug")) result.response.stack = result.response.errorStack; if (result.response instanceof APIError && !context?.asResponse) throw result.response; return context?.asResponse ? toResponse(result.response, { headers: result.headers, status: result.status }) : context?.returnHeaders ? context?.returnStatus ? { headers: result.headers, response: result.response, status: result.status } : { headers: result.headers, response: result.response } : context?.returnStatus ? { response: result.response, status: result.status } : result.response; }); }; if (await hasRequestState()) return run(); else return runWithRequestState(/* @__PURE__ */ new WeakMap(), run); }; api[key].path = endpoint.path; api[key].options = endpoint.options; } return api; } async function runBeforeHooks(context, hooks) { let modifiedContext = {}; for (const hook of hooks) { let matched = false; try { matched = hook.matcher(context); } catch (error2) { const hookSource = hooksSourceWeakMap.get(hook.handler) ?? "unknown"; context.context.logger.error(`An error occurred during ${hookSource} hook matcher execution:`, error2); throw new APIError("INTERNAL_SERVER_ERROR", { message: `An error occurred during hook matcher execution. Check the logs for more details.` }); } if (matched) { const result = await hook.handler({ ...context, returnHeaders: false }).catch((e) => { if (e instanceof APIError && shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack; throw e; }); if (result && typeof result === "object") { if ("context" in result && typeof result.context === "object") { const { headers, ...rest } = result.context; if (headers instanceof Headers) if (modifiedContext.headers) headers.forEach((value, key) => { modifiedContext.headers?.set(key, value); }); else modifiedContext.headers = headers; modifiedContext = defuReplaceArrays(rest, modifiedContext); continue; } return result; } } } return { context: modifiedContext }; } async function runAfterHooks(context, hooks) { for (const hook of hooks) if (hook.matcher(context)) { const result = await hook.handler(context).catch((e) => { if (e instanceof APIError) { if (shouldPublishLog(context.context.logger.level, "debug")) e.stack = e.errorStack; return { response: e, headers: e.headers ? new Headers(e.headers) : null }; } throw e; }); if (result.headers) result.headers.forEach((value, key) => { if (!context.context.responseHeaders) context.context.responseHeaders = new Headers({ [key]: value }); else if (key.toLowerCase() === "set-cookie") context.context.responseHeaders.append(key, value); else context.context.responseHeaders.set(key, value); }); if (result.response) context.context.returned = result.response; } return { response: context.context.returned, headers: context.context.responseHeaders }; } function getHooks(authContext) { const plugins = authContext.options.plugins || []; const beforeHooks = []; const afterHooks = []; const beforeHookHandler = authContext.options.hooks?.before; if (beforeHookHandler) { hooksSourceWeakMap.set(beforeHookHandler, "user"); beforeHooks.push({ matcher: () => true, handler: beforeHookHandler }); } const afterHookHandler = authContext.options.hooks?.after; if (afterHookHandler) { hooksSourceWeakMap.set(afterHookHandler, "user"); afterHooks.push({ matcher: () => true, handler: afterHookHandler }); } const pluginBeforeHooks = plugins.filter((plugin) => plugin.hooks?.before).map((plugin) => plugin.hooks?.before).flat(); const pluginAfterHooks = plugins.filter((plugin) => plugin.hooks?.after).map((plugin) => plugin.hooks?.after).flat(); if (pluginBeforeHooks.length) beforeHooks.push(...pluginBeforeHooks); if (pluginAfterHooks.length) afterHooks.push(...pluginAfterHooks); return { beforeHooks, afterHooks }; } function checkEndpointConflicts(options, logger$1) { const endpointRegistry = /* @__PURE__ */ new Map(); options.plugins?.forEach((plugin) => { if (plugin.endpoints) { for (const [key, endpoint] of Object.entries(plugin.endpoints)) if (endpoint && "path" in endpoint && typeof endpoint.path === "string") { const path = endpoint.path; let methods = []; if (endpoint.options && "method" in endpoint.options) { if (Array.isArray(endpoint.options.method)) methods = endpoint.options.method; else if (typeof endpoint.options.method === "string") methods = [endpoint.options.method]; } if (methods.length === 0) methods = ["*"]; if (!endpointRegistry.has(path)) endpointRegistry.set(path, []); endpointRegistry.get(path).push({ pluginId: plugin.id, endpointKey: key, methods }); } } }); const conflicts = []; for (const [path, entries] of endpointRegistry.entries()) if (entries.length > 1) { const methodMap = /* @__PURE__ */ new Map(); let hasConflict = false; for (const entry of entries) for (const method of entry.methods) { if (!methodMap.has(method)) methodMap.set(method, []); methodMap.get(method).push(entry.pluginId); if (methodMap.get(method).length > 1) hasConflict = true; if (method === "*" && entries.length > 1) hasConflict = true; else if (method !== "*" && methodMap.has("*")) hasConflict = true; } if (hasConflict) { const uniquePlugins = [...new Set(entries.map((e) => e.pluginId))]; const conflictingMethods = []; for (const [method, plugins] of methodMap.entries()) if (plugins.length > 1 || method === "*" && entries.length > 1 || method !== "*" && methodMap.has("*")) conflictingMethods.push(method); conflicts.push({ path, plugins: uniquePlugins, conflictingMethods }); } } if (conflicts.length > 0) { const conflictMessages = conflicts.map((conflict) => ` - "${conflict.path}" [${conflict.conflictingMethods.join(", ")}] used by plugins: ${conflict.plugins.join(", ")}`).join("\n"); logger$1.error(`Endpoint path conflicts detected! Multiple plugins are trying to use the same endpoint paths with conflicting HTTP methods: ${conflictMessages} To resolve this, you can: 1. Use only one of the conflicting plugins 2. Configure the plugins to use different paths (if supported) 3. Ensure plugins use different HTTP methods for the same path `); } } function getEndpoints(ctx, options) { const pluginEndpoints = options.plugins?.reduce((acc, plugin) => { return { ...acc, ...plugin.endpoints }; }, {}) ?? {}; const middlewares = options.plugins?.map((plugin) => plugin.middlewares?.map((m) => { const middleware = (async (context) => { const authContext = await ctx; return m.middleware({ ...context, context: { ...authContext, ...context.context } }); }); middleware.options = m.middleware.options; return { path: m.path, middleware }; })).filter((plugin) => plugin !== void 0).flat() || []; return { api: toAuthEndpoints({ signInSocial: signInSocial(), callbackOAuth, getSession: getSession(), signOut, signUpEmail: signUpEmail(), signInEmail: signInEmail(), resetPassword, verifyPassword, verifyEmail, sendVerificationEmail, changeEmail, changePassword, setPassword, updateUser: updateUser(), deleteUser, requestPasswordReset, requestPasswordResetCallback, listSessions: listSessions(), revokeSession, revokeSessions, revokeOtherSessions, linkSocialAccount, listUserAccounts, deleteUserCallback, unlinkAccount, refreshToken, getAccessToken, accountInfo, ...pluginEndpoints, ok, error }, ctx), middlewares }; } const router = (ctx, options) => { const { api, middlewares } = getEndpoints(ctx, options); const basePath = new URL(ctx.baseURL).pathname; return createRouter(api, { routerContext: ctx, openapi: { disabled: true }, basePath, routerMiddleware: [{ path: "/**", middleware: originCheckMiddleware }, ...middlewares], allowedMediaTypes: ["application/json"], skipTrailingSlashes: options.advanced?.skipTrailingSlashes ?? false, async onRequest(req) { const disabledPaths = ctx.options.disabledPaths || []; const normalizedPath = normalizePathname(req.url, basePath); if (disabledPaths.includes(normalizedPath)) return new Response("Not Found", { status: 404 }); let currentRequest = req; for (const plugin of ctx.options.plugins || []) if (plugin.onRequest) { const response = await plugin.onRequest(currentRequest, ctx); if (response && "response" in response) return response.response; if (response && "request" in response) currentRequest = response.request; } const rateLimitResponse2 = await onRequestRateLimit(currentRequest, ctx); if (rateLimitResponse2) return rateLimitResponse2; return currentRequest; }, async onResponse(res) { for (const plugin of ctx.options.plugins || []) if (plugin.onResponse) { const response = await plugin.onResponse(res, ctx); if (response) return response.response; } return res; }, onError(e) { if (e instanceof APIError && e.status === "FOUND") return; if (options.onAPIError?.throw) throw e; if (options.onAPIError?.onError) { options.onAPIError.onError(e, ctx); return; } const optLogLevel = options.logger?.level; const log = optLogLevel === "error" || optLogLevel === "warn" || optLogLevel === "debug" ? logger : void 0; if (options.logger?.disabled !== true) { if (e && typeof e === "object" && "message" in e && typeof e.message === "string") { if (e.message.includes("no column") || e.message.includes("column") || e.message.includes("relation") || e.message.includes("table") || e.message.includes("does not exist")) { ctx.logger?.error(e.message); return; } } if (e instanceof APIError) { if (e.status === "INTERNAL_SERVER_ERROR") ctx.logger.error(e.status, e); log?.error(e.message); } else ctx.logger?.error(e && typeof e === "object" && "name" in e ? e.name : "", e); } } }); }; async function runPluginInit(ctx) { let options = ctx.options; const plugins = options.plugins || []; let context = ctx; const dbHooks = []; for (const plugin of plugins) if (plugin.init) { const initPromise = plugin.init(context); let result; if (isPromise(initPromise)) result = await initPromise; else result = initPromise; if (typeof result === "object") { if (result.options) { const { databaseHooks, ...restOpts } = result.options; if (databaseHooks) dbHooks.push(databaseHooks); options = defu(options, restOpts); } if (result.context) context = { ...context, ...result.context }; } } dbHooks.push(options.databaseHooks); context.internalAdapter = createInternalAdapter(context.adapter, { options, logger: context.logger, hooks: dbHooks.filter((u) => u !== void 0), generateId: context.generateId }); context.options = options; return { context }; } function getInternalPlugins(options) { const plugins = []; if (options.advanced?.crossSubDomainCookies?.enabled) ; return plugins; } async function getTrustedOrigins(options, request) { const baseURL = getBaseURL(options.baseURL, options.basePath, request); const trustedOrigins = baseURL ? [new URL(baseURL).origin] : []; if (options.trustedOrigins) { if (Array.isArray(options.trustedOrigins)) trustedOrigins.push(...options.trustedOrigins); if (typeof options.trustedOrigins === "function") { const validOrigins = await options.trustedOrigins(request); trustedOrigins.push(...validOrigins); } } const envTrustedOrigins = env.BETTER_AUTH_TRUSTED_ORIGINS; if (envTrustedOrigins) trustedOrigins.push(...envTrustedOrigins.split(",")); return trustedOrigins.filter((v) => Boolean(v)); } const createBetterAuth = (options, initFn) => { const authContext = initFn(options); const { api } = getEndpoints(authContext, options); return { handler: async (request) => { const ctx = await authContext; const basePath = ctx.options.basePath || "/api/auth"; if (!ctx.options.baseURL) { const baseURL = getBaseURL(void 0, basePath, request, void 0, ctx.options.advanced?.trustedProxyHeaders); if (baseURL) { ctx.baseURL = baseURL; ctx.options.baseURL = getOrigin(ctx.baseURL) || void 0; } else throw new BetterAuthError("Could not get base URL from request. Please provide a valid base URL."); } ctx.trustedOrigins = await getTrustedOrigins(ctx.options, request); const { handler } = router(ctx, options); return runWithAdapter(ctx.adapter, () => handler(request)); }, api, options, $context: authContext, $ERROR_CODES: { ...options.plugins?.reduce((acc, plugin) => { if (plugin.$ERROR_CODES) return { ...acc, ...plugin.$ERROR_CODES }; return acc; }, {}), ...BASE_ERROR_CODES } }; }; const DEFAULT_SECRET = "better-auth-secret-12345678901234567890"; function estimateEntropy(str) { const unique = new Set(str).size; if (unique === 0) return 0; return Math.log2(Math.pow(unique, str.length)); } function validateSecret(secret, logger$1) { const isDefaultSecret = secret === DEFAULT_SECRET; if (isTest()) return; if (isDefaultSecret && isProduction) throw new BetterAuthError("You are using the default secret. Please set `BETTER_AUTH_SECRET` in your environment variables or pass `secret` in your auth config."); if (secret.length < 32) logger$1.warn(`[better-auth] Warning: your BETTER_AUTH_SECRET should be at least 32 characters long for adequate security. Generate one with \`npx @better-auth/cli secret\` or \`openssl rand -base64 32\`.`); if (estimateEntropy(secret) < 120) logger$1.warn("[better-auth] Warning: your BETTER_AUTH_SECRET appears low-entropy. Use a randomly generated secret for production."); } async function createAuthContext(adapter, options, getDatabaseType) { if (!options.database) options = defu$1(options, { session: { cookieCache: { enabled: true, strategy: "jwe", refreshCache: true } }, account: { storeStateStrategy: "cookie", storeAccountCookie: true } }); const plugins = options.plugins || []; const internalPlugins = getInternalPlugins(options); const logger$1 = createLogger(options.logger); const baseURL = getBaseURL(options.baseURL, options.basePath); if (!baseURL) logger$1.warn(`[better-auth] Base URL could not be determined. Please set a valid base URL using the baseURL config option or the BETTER_AUTH_URL environment variable. Without this, callbacks and redirects may not work correctly.`); if (adapter.id === "memory" && options.advanced?.database?.generateId === false) logger$1.error(`[better-auth] Misconfiguration detected. You are using the memory DB with generateId: false. This will cause no id to be generated for any model. Most of the features of Better Auth will not work correctly.`); const secret = options.secret || env.BETTER_AUTH_SECRET || env.AUTH_SECRET || DEFAULT_SECRET; validateSecret(secret, logger$1); options = { ...options, secret, baseURL: baseURL ? new URL(baseURL).origin : "", basePath: options.basePath || "/api/auth", plugins: plugins.concat(internalPlugins) }; checkEndpointConflicts(options, logger$1); const cookies = getCookies(options); const tables = getAuthTables(options); const providers = Object.entries(options.socialProviders || {}).map(([key, config2]) => { if (config2 == null) return null; if (config2.enabled === false) return null; if (!config2.clientId) logger$1.warn(`Social provider ${key} is missing clientId or clientSecret`); const provider = socialProviders[key](config2); provider.disableImplicitSignUp = config2.disableImplicitSignUp; return provider; }).filter((x) => x !== null); const generateIdFunc = ({ model, size }) => { if (typeof options.advanced?.generateId === "function") return options.advanced.generateId({ model, size }); const dbGenerateId = options?.advanced?.database?.generateId; if (typeof dbGenerateId === "function") return dbGenerateId({ model, size }); if (dbGenerateId === "uuid") return crypto.randomUUID(); if (dbGenerateId === "serial" || dbGenerateId === false) return false; return generateId(size); }; const { publish } = await createTelemetry(options, { adapter: adapter.id, database: typeof options.database === "function" ? "adapter" : getDatabaseType(options.database) }); const trustedOrigins = await getTrustedOrigins(options); const initOrPromise = runPluginInit({ appName: options.appName || "Better Auth", baseURL: baseURL || "", version: getBetterAuthVersion(), socialProviders: providers, options, oauthConfig: { storeStateStrategy: options.account?.storeStateStrategy || (options.database ? "database" : "cookie"), skipStateCookieCheck: !!options.account?.skipStateCookieCheck }, tables, trustedOrigins, isTrustedOrigin(url, settings) { return this.trustedOrigins.some((origin) => matchesOriginPattern(url, origin, settings)); }, sessionConfig: { updateAge: options.session?.updateAge !== void 0 ? options.session.updateAge : 1440 * 60, expiresIn: options.session?.expiresIn || 3600 * 24 * 7, freshAge: options.session?.freshAge === void 0 ? 3600 * 24 : options.session.freshAge, cookieRefreshCache: (() => { const refreshCache = options.session?.cookieCache?.refreshCache; const maxAge = options.session?.cookieCache?.maxAge || 300; if ((!!options.database || !!options.secondaryStorage) && refreshCache) { logger$1.warn("[better-auth] `session.cookieCache.refreshCache` is enabled while `database` or `secondaryStorage` is configured. `refreshCache` is meant for stateless (DB-less) setups. Disabling `refreshCache` — remove it from your config to silence this warning."); return false; } if (refreshCache === false || refreshCache === void 0) return false; if (refreshCache === true) return { enabled: true, updateAge: Math.floor(maxAge * 0.2) }; return { enabled: true, updateAge: refreshCache.updateAge !== void 0 ? refreshCache.updateAge : Math.floor(maxAge * 0.2) }; })() }, secret, rateLimit: { ...options.rateLimit, enabled: options.rateLimit?.enabled ?? isProduction, window: options.rateLimit?.window || 10, max: options.rateLimit?.max || 100, storage: options.rateLimit?.storage || (options.secondaryStorage ? "secondary-storage" : "memory") }, authCookies: cookies, logger: logger$1, generateId: generateIdFunc, session: null, secondaryStorage: options.secondaryStorage, password: { hash: options.emailAndPassword?.password?.hash || hashPassword, verify: options.emailAndPassword?.password?.verify || verifyPassword$1, config: { minPasswordLength: options.emailAndPassword?.minPasswordLength || 8, maxPasswordLength: options.emailAndPassword?.maxPasswordLength || 128 }, checkPassword }, setNewSession(session) { this.newSession = session; }, newSession: null, adapter, internalAdapter: createInternalAdapter(adapter, { options, logger: logger$1, hooks: options.databaseHooks ? [options.databaseHooks] : [], generateId: generateIdFunc }), createAuthCookie: createCookieGetter(options), async runMigrations() { throw new BetterAuthError("runMigrations will be set by the specific init implementation"); }, publishTelemetry: publish, skipCSRFCheck: !!options.advanced?.disableCSRFCheck, skipOriginCheck: options.advanced?.disableOriginCheck !== void 0 ? options.advanced.disableOriginCheck : isTest() ? true : false, runInBackground: options.advanced?.backgroundTasks?.handler ?? ((p) => { p.catch(() => { }); }), async runInBackgroundOrAwait(promise) { try { if (options.advanced?.backgroundTasks?.handler) { if (promise instanceof Promise) options.advanced.backgroundTasks.handler(promise.catch((e) => { logger$1.error("Failed to run background task:", e); })); } else await promise; } catch (e) { logger$1.error("Failed to run background task:", e); } }, getPlugin: (id) => options.plugins.find((p) => p.id === id) ?? null }); let context; if (isPromise(initOrPromise)) ({ context } = await initOrPromise); else ({ context } = initOrPromise); if (typeof context.options.emailVerification?.onEmailVerification === "function") context.options.emailVerification.onEmailVerification = deprecate(context.options.emailVerification.onEmailVerification, "Use `afterEmailVerification` instead. This will be removed in 1.5", context.logger); return context; } const initMinimal = async (options) => { const adapter = await getBaseAdapter(options, async () => { throw new BetterAuthError("Direct database connection requires Kysely. Please use `better-auth` instead of `better-auth/minimal`, or provide an adapter (drizzleAdapter, prismaAdapter, etc.)"); }); const getDatabaseType = (_database) => "unknown"; const ctx = await createAuthContext(adapter, options, getDatabaseType); ctx.runMigrations = async function() { throw new BetterAuthError("Migrations are not supported in 'better-auth/minimal'. Please use 'better-auth' for migration support."); }; return ctx; }; const betterAuth = (options) => { return createBetterAuth(options, initMinimal); }; const drizzleAdapter = (db2, config2) => { let lazyOptions = null; const createCustomAdapter = (db$1) => ({ getFieldName, getDefaultFieldName, options }) => { function getSchema2(model) { const schema2 = config2.schema || db$1._.fullSchema; if (!schema2) throw new BetterAuthError("Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object."); const schemaModel = schema2[model]; if (!schemaModel) throw new BetterAuthError(`[# Drizzle Adapter]: The model "${model}" was not found in the schema object. Please pass the schema directly to the adapter options.`); return schemaModel; } const withReturning = async (model, builder, data, where) => { if (config2.provider !== "mysql") return (await builder.returning())[0]; await builder.execute(); const schemaModel = getSchema2(model); const builderVal = builder.config?.values; if (where?.length) { const clause = convertWhereClause(where.map((w) => { if (data[w.field] !== void 0) return { ...w, value: data[w.field] }; return w; }), model); return (await db$1.select().from(schemaModel).where(...clause))[0]; } else if (builderVal && builderVal[0]?.id?.value) { let tId = builderVal[0]?.id?.value; if (!tId) tId = (await db$1.select({ id: sql$1`LAST_INSERT_ID()` }).from(schemaModel).orderBy(desc(schemaModel.id)).limit(1))[0].id; return (await db$1.select().from(schemaModel).where(eq(schemaModel.id, tId)).limit(1).execute())[0]; } else if (data.id) return (await db$1.select().from(schemaModel).where(eq(schemaModel.id, data.id)).limit(1).execute())[0]; else { if (!("id" in schemaModel)) throw new BetterAuthError(`The model "${model}" does not have an "id" field. Please use the "id" field as your primary key.`); return (await db$1.select().from(schemaModel).orderBy(desc(schemaModel.id)).limit(1).execute())[0]; } }; function convertWhereClause(where, model) { const schemaModel = getSchema2(model); if (!where) return []; if (where.length === 1) { const w = where[0]; if (!w) return []; const field = getFieldName({ model, field: w.field }); if (!schemaModel[field]) throw new BetterAuthError(`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`); if (w.operator === "in") { if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`); return [inArray(schemaModel[field], w.value)]; } if (w.operator === "not_in") { if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "not_in" operator.`); return [notInArray(schemaModel[field], w.value)]; } if (w.operator === "contains") return [like(schemaModel[field], `%${w.value}%`)]; if (w.operator === "starts_with") return [like(schemaModel[field], `${w.value}%`)]; if (w.operator === "ends_with") return [like(schemaModel[field], `%${w.value}`)]; if (w.operator === "lt") return [lt(schemaModel[field], w.value)]; if (w.operator === "lte") return [lte(schemaModel[field], w.value)]; if (w.operator === "ne") return [ne(schemaModel[field], w.value)]; if (w.operator === "gt") return [gt(schemaModel[field], w.value)]; if (w.operator === "gte") return [gte(schemaModel[field], w.value)]; return [eq(schemaModel[field], w.value)]; } const andGroup = where.filter((w) => w.connector === "AND" || !w.connector); const orGroup = where.filter((w) => w.connector === "OR"); const andClause = and(...andGroup.map((w) => { const field = getFieldName({ model, field: w.field }); if (w.operator === "in") { if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`); return inArray(schemaModel[field], w.value); } if (w.operator === "not_in") { if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "not_in" operator.`); return notInArray(schemaModel[field], w.value); } if (w.operator === "contains") return like(schemaModel[field], `%${w.value}%`); if (w.operator === "starts_with") return like(schemaModel[field], `${w.value}%`); if (w.operator === "ends_with") return like(schemaModel[field], `%${w.value}`); if (w.operator === "lt") return lt(schemaModel[field], w.value); if (w.operator === "lte") return lte(schemaModel[field], w.value); if (w.operator === "gt") return gt(schemaModel[field], w.value); if (w.operator === "gte") return gte(schemaModel[field], w.value); if (w.operator === "ne") return ne(schemaModel[field], w.value); return eq(schemaModel[field], w.value); })); const orClause = or(...orGroup.map((w) => { const field = getFieldName({ model, field: w.field }); if (w.operator === "in") { if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`); return inArray(schemaModel[field], w.value); } if (w.operator === "not_in") { if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "not_in" operator.`); return notInArray(schemaModel[field], w.value); } if (w.operator === "contains") return like(schemaModel[field], `%${w.value}%`); if (w.operator === "starts_with") return like(schemaModel[field], `${w.value}%`); if (w.operator === "ends_with") return like(schemaModel[field], `%${w.value}`); if (w.operator === "lt") return lt(schemaModel[field], w.value); if (w.operator === "lte") return lte(schemaModel[field], w.value); if (w.operator === "gt") return gt(schemaModel[field], w.value); if (w.operator === "gte") return gte(schemaModel[field], w.value); if (w.operator === "ne") return ne(schemaModel[field], w.value); return eq(schemaModel[field], w.value); })); const clause = []; if (andGroup.length) clause.push(andClause); if (orGroup.length) clause.push(orClause); return clause; } function checkMissingFields(schema2, model, values) { if (!schema2) throw new BetterAuthError("Drizzle adapter failed to initialize. Drizzle Schema not found. Please provide a schema object in the adapter options object."); for (const key in values) if (!schema2[key]) throw new BetterAuthError(`The field "${key}" does not exist in the "${model}" Drizzle schema. Please update your drizzle schema or re-generate using "npx @better-auth/cli@latest generate".`); } return { async create({ model, data: values }) { const schemaModel = getSchema2(model); checkMissingFields(schemaModel, model, values); return await withReturning(model, db$1.insert(schemaModel).values(values), values); }, async findOne({ model, where, select, join }) { const schemaModel = getSchema2(model); const clause = convertWhereClause(where, model); if (options.experimental?.joins) if (!db$1.query || !db$1.query[model]) { logger.error(`[# Drizzle Adapter]: The model "${model}" was not found in the query object. Please update your Drizzle schema to include relations or re-generate using "npx @better-auth/cli@latest generate".`); logger.info("Falling back to regular query"); } else { let includes; const pluralJoinResults = []; if (join) { includes = {}; const joinEntries = Object.entries(join); for (const [model$1, joinAttr] of joinEntries) { const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100; const isUnique = joinAttr.relation === "one-to-one"; const pluralSuffix = isUnique || config2.usePlural ? "" : "s"; includes[`${model$1}${pluralSuffix}`] = isUnique ? true : { limit }; if (!isUnique) pluralJoinResults.push(`${model$1}${pluralSuffix}`); } } const res$1 = await db$1.query[model].findFirst({ where: clause[0], columns: select?.length && select.length > 0 ? select.reduce((acc, field) => { acc[getFieldName({ model, field })] = true; return acc; }, {}) : void 0, with: includes }); if (res$1) for (const pluralJoinResult of pluralJoinResults) { const singularKey = !config2.usePlural ? pluralJoinResult.slice(0, -1) : pluralJoinResult; res$1[singularKey] = res$1[pluralJoinResult]; if (pluralJoinResult !== singularKey) delete res$1[pluralJoinResult]; } return res$1; } const res = await db$1.select(select?.length && select.length > 0 ? select.reduce((acc, field) => { const fieldName = getFieldName({ model, field }); return { ...acc, [fieldName]: schemaModel[fieldName] }; }, {}) : void 0).from(schemaModel).where(...clause); if (!res.length) return null; return res[0]; }, async findMany({ model, where, sortBy, limit, select, offset, join }) { const schemaModel = getSchema2(model); const clause = where ? convertWhereClause(where, model) : []; const sortFn = sortBy?.direction === "desc" ? desc : asc; if (options.experimental?.joins) if (!db$1.query[model]) { logger.error(`[# Drizzle Adapter]: The model "${model}" was not found in the query object. Please update your Drizzle schema to include relations or re-generate using "npx @better-auth/cli@latest generate".`); logger.info("Falling back to regular query"); } else { let includes; const pluralJoinResults = []; if (join) { includes = {}; const joinEntries = Object.entries(join); for (const [model$1, joinAttr] of joinEntries) { const isUnique = joinAttr.relation === "one-to-one"; const limit$1 = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100; const pluralSuffix = isUnique || config2.usePlural ? "" : "s"; includes[`${model$1}${pluralSuffix}`] = isUnique ? true : { limit: limit$1 }; if (!isUnique) pluralJoinResults.push(`${model$1}${pluralSuffix}`); } } let orderBy = void 0; if (sortBy?.field) orderBy = [sortFn(schemaModel[getFieldName({ model, field: sortBy?.field })])]; const res = await db$1.query[model].findMany({ where: clause[0], with: includes, columns: select?.length && select.length > 0 ? select.reduce((acc, field) => { acc[getFieldName({ model, field })] = true; return acc; }, {}) : void 0, limit: limit ?? 100, offset: offset ?? 0, orderBy }); if (res) for (const item of res) for (const pluralJoinResult of pluralJoinResults) { const singularKey = !config2.usePlural ? pluralJoinResult.slice(0, -1) : pluralJoinResult; if (singularKey === pluralJoinResult) continue; item[singularKey] = item[pluralJoinResult]; delete item[pluralJoinResult]; } return res; } let builder = db$1.select(select?.length && select.length > 0 ? select.reduce((acc, field) => { const fieldName = getFieldName({ model, field }); return { ...acc, [fieldName]: schemaModel[fieldName] }; }, {}) : void 0).from(schemaModel); const effectiveLimit = limit; const effectiveOffset = offset; if (typeof effectiveLimit !== "undefined") builder = builder.limit(effectiveLimit); if (typeof effectiveOffset !== "undefined") builder = builder.offset(effectiveOffset); if (sortBy?.field) builder = builder.orderBy(sortFn(schemaModel[getFieldName({ model, field: sortBy?.field })])); return await builder.where(...clause); }, async count({ model, where }) { const schemaModel = getSchema2(model); const clause = where ? convertWhereClause(where, model) : []; return (await db$1.select({ count: count() }).from(schemaModel).where(...clause))[0].count; }, async update({ model, where, update: values }) { const schemaModel = getSchema2(model); const clause = convertWhereClause(where, model); return await withReturning(model, db$1.update(schemaModel).set(values).where(...clause), values, where); }, async updateMany({ model, where, update: values }) { const schemaModel = getSchema2(model); const clause = convertWhereClause(where, model); return await db$1.update(schemaModel).set(values).where(...clause); }, async delete({ model, where }) { const schemaModel = getSchema2(model); const clause = convertWhereClause(where, model); return await db$1.delete(schemaModel).where(...clause); }, async deleteMany({ model, where }) { const schemaModel = getSchema2(model); const clause = convertWhereClause(where, model); const res = await db$1.delete(schemaModel).where(...clause); let count$1 = 0; if (res && "rowCount" in res) count$1 = res.rowCount; else if (Array.isArray(res)) count$1 = res.length; else if (res && ("affectedRows" in res || "rowsAffected" in res || "changes" in res)) count$1 = res.affectedRows ?? res.rowsAffected ?? res.changes; if (typeof count$1 !== "number") logger.error("[Drizzle Adapter] The result of the deleteMany operation is not a number. This is likely a bug in the adapter. Please report this issue to the Better Auth team.", { res, model, where }); return count$1; }, options: config2 }; }; let adapterOptions = null; adapterOptions = { config: { adapterId: "drizzle", adapterName: "Drizzle Adapter", usePlural: config2.usePlural ?? false, debugLogs: config2.debugLogs ?? false, supportsUUIDs: config2.provider === "pg" ? true : false, supportsJSON: config2.provider === "pg" ? true : false, supportsArrays: config2.provider === "pg" ? true : false, transaction: config2.transaction ?? false ? (cb) => db2.transaction((tx) => { return cb(createAdapterFactory({ config: adapterOptions.config, adapter: createCustomAdapter(tx) })(lazyOptions)); }) : false }, adapter: createCustomAdapter(db2) }; const adapter = createAdapterFactory(adapterOptions); return (options) => { lazyOptions = options; return adapter(options); }; }; const svelteKitHandler = async ({ auth: auth2, event, resolve, building }) => { if (building) return resolve(event); const { request, url } = event; if (isAuthPath(url.toString(), auth2.options)) return auth2.handler(request); return resolve(event); }; function isAuthPath(url, options) { const _url = new URL(url); const baseURL = new URL(`${options.baseURL || _url.origin}${options.basePath || "/api/auth"}`); if (_url.origin !== baseURL.origin) return false; if (!_url.pathname.startsWith(baseURL.pathname.endsWith("/") ? baseURL.pathname : `${baseURL.pathname}/`)) return false; return true; } const sveltekitCookies = (getRequestEvent2) => { return { id: "sveltekit-cookies", hooks: { after: [{ matcher() { return true; }, handler: createAuthMiddleware(async (ctx) => { const returned = ctx.context.responseHeaders; if ("_flag" in ctx && ctx._flag === "router") return; if (returned instanceof Headers) { const setCookies = returned?.get("set-cookie"); if (!setCookies) return; const event = getRequestEvent2(); if (!event) return; const parsed = parseSetCookieHeader(setCookies); for (const [name, { value, ...ops }] of parsed) try { event.cookies.set(name, decodeURIComponent(value), { sameSite: ops.samesite, path: ops.path || "/", expires: ops.expires, secure: ops.secure, httpOnly: ops.httponly, domain: ops.domain, maxAge: ops["max-age"] }); } catch { } } }) }] } }; }; defineErrorCodes({ FAILED_TO_CREATE_USER: "Failed to create user", USER_ALREADY_EXISTS: "User already exists.", USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.", YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself", YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE: "You are not allowed to change users role", YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS: "You are not allowed to create users", YOU_ARE_NOT_ALLOWED_TO_LIST_USERS: "You are not allowed to list users", YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS: "You are not allowed to list users sessions", YOU_ARE_NOT_ALLOWED_TO_BAN_USERS: "You are not allowed to ban users", YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS: "You are not allowed to impersonate users", YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS: "You are not allowed to revoke users sessions", YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS: "You are not allowed to delete users", YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD: "You are not allowed to set users password", BANNED_USER: "You have been banned from this application", YOU_ARE_NOT_ALLOWED_TO_GET_USER: "You are not allowed to get user", NO_DATA_TO_UPDATE: "No data to update", YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS: "You are not allowed to update users", YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself", YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: "You are not allowed to set a non-existent role value", YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins", INVALID_ROLE_TYPE: "Invalid role type" }); createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) throw new APIError("UNAUTHORIZED"); return { session }; }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }), role: z.union([z.string().meta({ description: "The role to set. `admin` or `user` by default" }), z.array(z.string().meta({ description: "The roles to set. `admin` or `user` by default" }))]).meta({ description: "The role to set, this can be a string or an array of strings. Eg: `admin` or `[admin, user]`" }) }); z.object({ id: z.string().meta({ description: "The id of the User" }) }); z.object({ email: z.string().meta({ description: "The email of the user" }), password: z.string().optional().meta({ description: "The password of the user. If not provided, the user will be created without a credential account (useful for magic link or social login only users)." }), name: z.string().meta({ description: "The name of the user" }), role: z.union([z.string().meta({ description: "The role of the user" }), z.array(z.string().meta({ description: "The roles of user" }))]).optional().meta({ description: `A string or array of strings representing the roles to apply to the new user. Eg: "user"` }), data: z.record(z.string(), z.any()).optional().meta({ description: "Extra fields for the user. Including custom additional fields." }) }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }), data: z.record(z.any(), z.any()).meta({ description: "The user data to update" }) }); z.object({ searchValue: z.string().optional().meta({ description: 'The value to search for. Eg: "some name"' }), searchField: z.enum(["email", "name"]).meta({ description: 'The field to search in, defaults to email. Can be `email` or `name`. Eg: "name"' }).optional(), searchOperator: z.enum([ "contains", "starts_with", "ends_with" ]).meta({ description: 'The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. Eg: "contains"' }).optional(), limit: z.string().meta({ description: "The number of users to return" }).or(z.number()).optional(), offset: z.string().meta({ description: "The offset to start from" }).or(z.number()).optional(), sortBy: z.string().meta({ description: "The field to sort by" }).optional(), sortDirection: z.enum(["asc", "desc"]).meta({ description: "The direction to sort by" }).optional(), filterField: z.string().meta({ description: "The field to filter by" }).optional(), filterValue: z.string().meta({ description: "The value to filter by" }).or(z.number()).or(z.boolean()).optional(), filterOperator: z.enum([ "eq", "ne", "lt", "lte", "gt", "gte", "contains" ]).meta({ description: "The operator to use for the filter" }).optional() }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }), banReason: z.string().meta({ description: "The reason for the ban" }).optional(), banExpiresIn: z.number().meta({ description: "The number of seconds until the ban expires" }).optional() }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }); z.object({ sessionToken: z.string().meta({ description: "The session token" }) }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }); z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }); z.object({ newPassword: z.string().nonempty("newPassword cannot be empty").meta({ description: "The new password" }), userId: z.coerce.string().nonempty("userId cannot be empty").meta({ description: "The user id" }) }); z.object({ userId: z.coerce.string().optional().meta({ description: `The user id. Eg: "user-id"` }), role: z.string().optional().meta({ description: `The role to check permission for. Eg: "admin"` }) }).and(z.union([z.object({ permission: z.record(z.string(), z.array(z.string())), permissions: z.undefined() }), z.object({ permission: z.undefined(), permissions: z.record(z.string(), z.array(z.string())) })])); defineErrorCodes({ INVALID_EMAIL_FORMAT: "Email was not generated in a valid format", 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", FAILED_TO_DELETE_ANONYMOUS_USER: "Failed to delete anonymous user", USER_IS_NOT_ANONYMOUS: "User is not anonymous", DELETE_ANONYMOUS_USER_DISABLED: "Deleting anonymous users is disabled" }); z.object({ key: z.string().meta({ description: "The key to verify" }), permissions: z.record(z.string(), z.array(z.string())).meta({ description: "The permissions to verify." }).optional() }); z.object({ name: z.string().meta({ description: "Name of the Api Key" }).optional(), expiresIn: z.number().meta({ description: "Expiration time of the Api Key in seconds" }).min(1).optional().nullable().default(null), userId: z.coerce.string().meta({ description: 'User Id of the user that the Api Key belongs to. server-only. Eg: "user-id"' }).optional(), prefix: z.string().meta({ description: "Prefix of the Api Key" }).regex(/^[a-zA-Z0-9_-]+$/, { message: "Invalid prefix format, must be alphanumeric and contain only underscores and hyphens." }).optional(), remaining: z.number().meta({ description: "Remaining number of requests. Server side only" }).min(0).optional().nullable().default(null), metadata: z.any().optional(), refillAmount: z.number().meta({ description: "Amount to refill the remaining count of the Api Key. server-only. Eg: 100" }).min(1).optional(), refillInterval: z.number().meta({ description: "Interval to refill the Api Key in milliseconds. server-only. Eg: 1000" }).optional(), rateLimitTimeWindow: z.number().meta({ description: "The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 1000" }).optional(), rateLimitMax: z.number().meta({ description: "Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100" }).optional(), rateLimitEnabled: z.boolean().meta({ description: "Whether the key has rate limiting enabled. server-only. Eg: true" }).optional(), permissions: z.record(z.string(), z.array(z.string())).meta({ description: "Permissions of the Api Key." }).optional() }); z.object({ keyId: z.string().meta({ description: "The id of the Api Key" }) }); z.object({ id: z.string().meta({ description: "The id of the Api Key" }) }); z.object({ keyId: z.string().meta({ description: "The id of the Api Key" }), userId: z.coerce.string().meta({ description: 'The id of the user which the api key belongs to. server-only. Eg: "some-user-id"' }).optional(), name: z.string().meta({ description: "The name of the key" }).optional(), enabled: z.boolean().meta({ description: "Whether the Api Key is enabled or not" }).optional(), remaining: z.number().meta({ description: "The number of remaining requests" }).min(1).optional(), refillAmount: z.number().meta({ description: "The refill amount" }).optional(), refillInterval: z.number().meta({ description: "The refill interval" }).optional(), metadata: z.any().optional(), expiresIn: z.number().meta({ description: "Expiration time of the Api Key in seconds" }).min(1).optional().nullable(), rateLimitEnabled: z.boolean().meta({ description: "Whether the key has rate limiting enabled." }).optional(), rateLimitTimeWindow: z.number().meta({ description: "The duration in milliseconds where each request is counted. server-only. Eg: 1000" }).optional(), rateLimitMax: z.number().meta({ description: "Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100" }).optional(), permissions: z.record(z.string(), z.array(z.string())).meta({ description: "Update the permissions on the API Key. server-only." }).optional().nullable() }); 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." }); defineErrorCodes({ VERIFICATION_FAILED: "Captcha verification failed", MISSING_RESPONSE: "Missing CAPTCHA response", UNKNOWN_ERROR: "Something went wrong" }); defineErrorCodes({ MISSING_SECRET_KEY: "Missing secret key", SERVICE_UNAVAILABLE: "CAPTCHA service unavailable" }); z.optional(z.object({ disableCookieCache: z.boolean().meta({ description: "Disable cookie cache and fetch session from database" }).or(z.string().transform((v) => v === "true")).optional(), disableRefresh: z.boolean().meta({ description: "Disable session refresh. Useful for checking session status, without updating the session" }).optional() })); const DEVICE_AUTHORIZATION_ERROR_CODES = defineErrorCodes({ INVALID_DEVICE_CODE: "Invalid device code", EXPIRED_DEVICE_CODE: "Device code has expired", EXPIRED_USER_CODE: "User code has expired", AUTHORIZATION_PENDING: "Authorization pending", ACCESS_DENIED: "Access denied", INVALID_USER_CODE: "Invalid user code", DEVICE_CODE_ALREADY_PROCESSED: "Device code already processed", POLLING_TOO_FREQUENTLY: "Polling too frequently", USER_NOT_FOUND: "User not found", FAILED_TO_CREATE_SESSION: "Failed to create session", INVALID_DEVICE_CODE_STATUS: "Invalid device code status", AUTHENTICATION_REQUIRED: "Authentication required" }); z.object({ client_id: z.string().meta({ description: "The client ID of the application" }), scope: z.string().meta({ description: "Space-separated list of scopes" }).optional() }); z.object({ error: z.enum(["invalid_request", "invalid_client"]).meta({ description: "Error code" }), error_description: z.string().meta({ description: "Detailed error description" }) }); z.object({ grant_type: z.literal("urn:ietf:params:oauth:grant-type:device_code").meta({ description: "The grant type for device flow" }), device_code: z.string().meta({ description: "The device verification code" }), client_id: z.string().meta({ description: "The client ID of the application" }) }); z.object({ error: z.enum([ "authorization_pending", "slow_down", "expired_token", "access_denied", "invalid_request", "invalid_grant" ]).meta({ description: "Error code" }), error_description: z.string().meta({ description: "Detailed error description" }) }); createAuthEndpoint("/device", { method: "GET", query: z.object({ user_code: z.string().meta({ description: "The user code to verify" }) }), error: z.object({ error: z.enum(["invalid_request"]).meta({ description: "Error code" }), error_description: z.string().meta({ description: "Detailed error description" }) }), metadata: { openapi: { description: "Verify user code and get device authorization status", responses: { 200: { description: "Device authorization status", content: { "application/json": { schema: { type: "object", properties: { user_code: { type: "string", description: "The user code to verify" }, status: { type: "string", enum: [ "pending", "approved", "denied" ], description: "Current status of the device authorization" } } } } } } } } } }, async (ctx) => { const { user_code } = ctx.query; const cleanUserCode = user_code.replace(/-/g, ""); const deviceCodeRecord = await ctx.context.adapter.findOne({ model: "deviceCode", where: [{ field: "userCode", value: cleanUserCode }] }); if (!deviceCodeRecord) throw new APIError("BAD_REQUEST", { error: "invalid_request", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE }); if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { error: "expired_token", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE }); return ctx.json({ user_code, status: deviceCodeRecord.status }); }); createAuthEndpoint("/device/approve", { method: "POST", body: z.object({ userCode: z.string().meta({ description: "The user code to approve" }) }), error: z.object({ error: z.enum([ "invalid_request", "expired_token", "device_code_already_processed", "unauthorized", "access_denied" ]).meta({ description: "Error code" }), error_description: z.string().meta({ description: "Detailed error description" }) }), requireHeaders: true, metadata: { openapi: { description: "Approve device authorization", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" } } } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) throw new APIError("UNAUTHORIZED", { error: "unauthorized", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED }); const { userCode } = ctx.body; const cleanUserCode = userCode.replace(/-/g, ""); const deviceCodeRecord = await ctx.context.adapter.findOne({ model: "deviceCode", where: [{ field: "userCode", value: cleanUserCode }] }); if (!deviceCodeRecord) throw new APIError("BAD_REQUEST", { error: "invalid_request", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE }); if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { error: "expired_token", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE }); if (deviceCodeRecord.status !== "pending") throw new APIError("BAD_REQUEST", { error: "invalid_request", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED }); if (deviceCodeRecord.userId && deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", { error: "access_denied", error_description: "You are not authorized to approve this device authorization" }); await ctx.context.adapter.update({ model: "deviceCode", where: [{ field: "id", value: deviceCodeRecord.id }], update: { status: "approved", userId: session.user.id } }); return ctx.json({ success: true }); }); createAuthEndpoint("/device/deny", { method: "POST", body: z.object({ userCode: z.string().meta({ description: "The user code to deny" }) }), error: z.object({ error: z.enum([ "invalid_request", "expired_token", "unauthorized", "access_denied" ]).meta({ description: "Error code" }), error_description: z.string().meta({ description: "Detailed error description" }) }), requireHeaders: true, metadata: { openapi: { description: "Deny device authorization", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" } } } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) throw new APIError("UNAUTHORIZED", { error: "unauthorized", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED }); const { userCode } = ctx.body; const cleanUserCode = userCode.replace(/-/g, ""); const deviceCodeRecord = await ctx.context.adapter.findOne({ model: "deviceCode", where: [{ field: "userCode", value: cleanUserCode }] }); if (!deviceCodeRecord) throw new APIError("BAD_REQUEST", { error: "invalid_request", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE }); if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { error: "expired_token", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE }); if (deviceCodeRecord.status !== "pending") throw new APIError("BAD_REQUEST", { error: "invalid_request", error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED }); if (deviceCodeRecord.userId && deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", { error: "access_denied", error_description: "You are not authorized to deny this device authorization" }); await ctx.context.adapter.update({ model: "deviceCode", where: [{ field: "id", value: deviceCodeRecord.id }], update: { status: "denied", userId: deviceCodeRecord.userId || session.user.id } }); return ctx.json({ success: true }); }); z.object({ id: z.string(), deviceCode: z.string(), userCode: z.string(), userId: z.string().optional(), expiresAt: z.date(), status: z.string(), lastPolledAt: z.date().optional(), pollingInterval: z.number().optional(), clientId: z.string().optional(), scope: z.string().optional() }); const timeStringSchema = z.custom((val) => { if (typeof val !== "string") return false; try { ms(val); return true; } catch { return false; } }, { message: "Invalid time string format. Use formats like '30m', '5s', '1h', etc." }); z.object({ expiresIn: timeStringSchema.default("30m").describe("Time in seconds until the device code expires. Use formats like '30m', '5s', '1h', etc."), interval: timeStringSchema.default("5s").describe("Time in seconds between polling attempts. Use formats like '30m', '5s', '1h', etc."), deviceCodeLength: z.number().int().positive().default(40).describe("Length of the device code to be generated. Default is 40 characters."), userCodeLength: z.number().int().positive().default(8).describe("Length of the user code to be generated. Default is 8 characters."), generateDeviceCode: z.custom((val) => typeof val === "function", { message: "generateDeviceCode must be a function that returns a string or a promise that resolves to a string." }).optional().describe("Function to generate a device code. If not provided, a default random string generator will be used."), generateUserCode: z.custom((val) => typeof val === "function", { message: "generateUserCode must be a function that returns a string or a promise that resolves to a string." }).optional().describe("Function to generate a user code. If not provided, a default random string generator will be used."), validateClient: z.custom((val) => typeof val === "function", { message: "validateClient must be a function that returns a boolean or a promise that resolves to a boolean." }).optional().describe("Function to validate the client ID. If not provided, no validation will be performed."), onDeviceAuthRequest: z.custom((val) => typeof val === "function", { message: "onDeviceAuthRequest must be a function that returns void or a promise that resolves to void." }).optional().describe("Function to handle device authorization requests. If not provided, no additional actions will be taken."), verificationUri: z.string().optional().describe("The URI where users verify their device code. Can be an absolute URL (https://example.com/device) or relative path (/custom-path). This will be returned as verification_uri in the device code response. If not provided, defaults to /device."), schema: z.custom(() => true) }); const types = [ "email-verification", "sign-in", "forget-password" ]; defineErrorCodes({ OTP_EXPIRED: "OTP expired", INVALID_OTP: "Invalid OTP", TOO_MANY_ATTEMPTS: "Too many attempts" }); z.object({ email: z.string({}).meta({ description: "Email address to send the OTP" }), type: z.enum(types).meta({ description: "Type of the OTP" }) }); z.object({ email: z.string({}).meta({ description: "Email address to send the OTP" }), type: z.enum(types).meta({ required: true, description: "Type of the OTP" }) }); z.object({ email: z.string({}).meta({ description: "Email address the OTP was sent to" }), type: z.enum(types).meta({ required: true, description: "Type of the OTP" }) }); z.object({ email: z.string().meta({ description: "Email address the OTP was sent to" }), type: z.enum(types).meta({ required: true, description: "Type of the OTP" }), otp: z.string().meta({ required: true, description: "OTP to verify" }) }); z.object({ email: z.string({}).meta({ description: "Email address to verify" }), otp: z.string().meta({ required: true, description: "OTP to verify" }) }); z.object({ email: z.string({}).meta({ description: "Email address to sign in" }), otp: z.string().meta({ required: true, description: "OTP sent to the email" }) }); z.object({ email: z.string().meta({ description: "Email address to send the OTP" }) }); z.object({ email: z.string().meta({ description: "Email address to send the OTP" }) }); z.object({ email: z.string().meta({ description: "Email address to reset the password" }), otp: z.string().meta({ description: "OTP sent to the email" }), password: z.string().meta({ description: "New password" }) }); const GENERIC_OAUTH_ERROR_CODES = defineErrorCodes({ INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration", TOKEN_URL_NOT_FOUND: "Invalid OAuth configuration. Token URL not found.", PROVIDER_CONFIG_NOT_FOUND: "No config found for provider", PROVIDER_ID_REQUIRED: "Provider ID is required", INVALID_OAUTH_CONFIG: "Invalid OAuth configuration.", SESSION_REQUIRED: "Session is required", ISSUER_MISMATCH: "OAuth issuer mismatch. The authorization server issuer does not match the expected value (RFC 9207).", ISSUER_MISSING: "OAuth issuer parameter missing. The authorization server did not include the required iss parameter (RFC 9207)." }); const signInWithOAuth2BodySchema = z.object({ providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }), callbackURL: z.string().meta({ description: "The URL to redirect to after sign in" }).optional(), errorCallbackURL: z.string().meta({ description: "The URL to redirect to if an error occurs" }).optional(), newUserCallbackURL: z.string().meta({ description: 'The URL to redirect to after login if the user is new. Eg: "/welcome"' }).optional(), disableRedirect: z.boolean().meta({ description: "Disable redirect" }).optional(), scopes: z.array(z.string()).meta({ description: "Scopes to be passed to the provider authorization request." }).optional(), requestSignUp: z.boolean().meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. Eg: false" }).optional(), additionalData: z.record(z.string(), z.any()).optional() }); const signInWithOAuth2 = (options) => createAuthEndpoint("/sign-in/oauth2", { method: "POST", body: signInWithOAuth2BodySchema, metadata: { openapi: { description: "Sign in with OAuth2", responses: { 200: { description: "Sign in with OAuth2", content: { "application/json": { schema: { type: "object", properties: { url: { type: "string" }, redirect: { type: "boolean" } } } } } } } } } }, async (ctx) => { const { providerId } = ctx.body; const config2 = options.config.find((c) => c.providerId === providerId); if (!config2) throw new APIError("BAD_REQUEST", { message: `${GENERIC_OAUTH_ERROR_CODES.PROVIDER_CONFIG_NOT_FOUND} ${providerId}` }); const { discoveryUrl, authorizationUrl, tokenUrl, clientId, clientSecret, scopes, redirectURI, responseType, pkce, prompt, accessType, authorizationUrlParams, responseMode } = config2; let finalAuthUrl = authorizationUrl; let finalTokenUrl = tokenUrl; if (discoveryUrl) { const discovery = await betterFetch(discoveryUrl, { method: "GET", headers: config2.discoveryHeaders, onError(context) { ctx.context.logger.error(context.error.message, context.error, { discoveryUrl }); } }); if (discovery.data) { finalAuthUrl = discovery.data.authorization_endpoint; finalTokenUrl = discovery.data.token_endpoint; } } if (!finalAuthUrl || !finalTokenUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION }); if (authorizationUrlParams) { const withAdditionalParams = new URL(finalAuthUrl); for (const [paramName, paramValue] of Object.entries(authorizationUrlParams)) withAdditionalParams.searchParams.set(paramName, paramValue); finalAuthUrl = withAdditionalParams.toString(); } const additionalParams = typeof authorizationUrlParams === "function" ? authorizationUrlParams(ctx) : authorizationUrlParams; const { state, codeVerifier } = await generateState(ctx, void 0, ctx.body.additionalData); const authUrl = await createAuthorizationURL({ id: providerId, options: { clientId, clientSecret, redirectURI }, authorizationEndpoint: finalAuthUrl, state, codeVerifier: pkce ? codeVerifier : void 0, scopes: ctx.body.scopes ? [...ctx.body.scopes, ...scopes || []] : scopes || [], redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerId}`, prompt, accessType, responseType, responseMode, additionalParams }); return ctx.json({ url: authUrl.toString(), redirect: !ctx.body.disableRedirect }); }); const OAuth2CallbackQuerySchema = z.object({ code: z.string().meta({ description: "The OAuth2 code" }).optional(), error: z.string().meta({ description: "The error message, if any" }).optional(), error_description: z.string().meta({ description: "The error description, if any" }).optional(), state: z.string().meta({ description: "The state parameter from the OAuth2 request" }).optional(), iss: z.string().meta({ description: "The issuer identifier" }).optional() }); const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:providerId", { method: "GET", query: OAuth2CallbackQuerySchema, metadata: { ...HIDE_METADATA, allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"], openapi: { description: "OAuth2 callback", responses: { 200: { description: "OAuth2 callback", content: { "application/json": { schema: { type: "object", properties: { url: { type: "string" } } } } } } } } } }, async (ctx) => { const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`; if (ctx.query.error || !ctx.query.code) throw ctx.redirect(`${defaultErrorURL}?error=${ctx.query.error || "oAuth_code_missing"}&error_description=${ctx.query.error_description}`); const providerId = ctx.params?.providerId; if (!providerId) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.PROVIDER_ID_REQUIRED }); const providerConfig = options.config.find((p) => p.providerId === providerId); if (!providerConfig) throw new APIError("BAD_REQUEST", { message: `${GENERIC_OAUTH_ERROR_CODES.PROVIDER_CONFIG_NOT_FOUND} ${providerId}` }); let tokens = void 0; const { callbackURL, codeVerifier, errorURL, requestSignUp, newUserURL, link } = await parseState(ctx); const code = ctx.query.code; function redirectOnError(error2) { const defaultErrorURL$1 = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`; let url = errorURL || defaultErrorURL$1; if (url.includes("?")) url = `${url}&error=${error2}`; else url = `${url}?error=${error2}`; throw ctx.redirect(url); } let finalTokenUrl = providerConfig.tokenUrl; let finalUserInfoUrl = providerConfig.userInfoUrl; let expectedIssuer = providerConfig.issuer; if (providerConfig.discoveryUrl) { const discovery = await betterFetch(providerConfig.discoveryUrl, { method: "GET", headers: providerConfig.discoveryHeaders }); if (discovery.data) { finalTokenUrl = discovery.data.token_endpoint; finalUserInfoUrl = discovery.data.userinfo_endpoint; if (!expectedIssuer && discovery.data.issuer) expectedIssuer = discovery.data.issuer; } } if (expectedIssuer) { if (ctx.query.iss) { if (ctx.query.iss !== expectedIssuer) { ctx.context.logger.error("OAuth issuer mismatch", { expected: expectedIssuer, received: ctx.query.iss }); return redirectOnError("issuer_mismatch"); } } else if (providerConfig.requireIssuerValidation) { ctx.context.logger.error("OAuth issuer parameter missing", { expected: expectedIssuer }); return redirectOnError("issuer_missing"); } } try { if (providerConfig.getToken) tokens = await providerConfig.getToken({ code, redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerConfig.providerId}`, codeVerifier: providerConfig.pkce ? codeVerifier : void 0 }); else { if (!finalTokenUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG }); const additionalParams = typeof providerConfig.tokenUrlParams === "function" ? providerConfig.tokenUrlParams(ctx) : providerConfig.tokenUrlParams; tokens = await validateAuthorizationCode({ headers: providerConfig.authorizationHeaders, code, codeVerifier: providerConfig.pkce ? codeVerifier : void 0, redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerConfig.providerId}`, options: { clientId: providerConfig.clientId, clientSecret: providerConfig.clientSecret, redirectURI: providerConfig.redirectURI }, tokenEndpoint: finalTokenUrl, authentication: providerConfig.authentication, additionalParams }); } } catch (e) { ctx.context.logger.error(e && typeof e === "object" && "name" in e ? e.name : "", e); throw redirectOnError("oauth_code_verification_failed"); } if (!tokens) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG }); const userInfo = await (async function handleUserInfo() { const userInfo$1 = providerConfig.getUserInfo ? await providerConfig.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl); if (!userInfo$1) throw redirectOnError("user_info_is_missing"); const mapUser = providerConfig.mapProfileToUser ? await providerConfig.mapProfileToUser(userInfo$1) : userInfo$1; const email = mapUser.email ? mapUser.email.toLowerCase() : userInfo$1.email?.toLowerCase(); if (!email) { ctx.context.logger.error("Unable to get user info", userInfo$1); throw redirectOnError("email_is_missing"); } const id = mapUser.id ? String(mapUser.id) : String(userInfo$1.id); const name = mapUser.name ? mapUser.name : userInfo$1.name; if (!name) { ctx.context.logger.error("Unable to get user info", userInfo$1); throw redirectOnError("name_is_missing"); } return { ...userInfo$1, ...mapUser, email, id, name }; })(); if (link) { if (ctx.context.options.account?.accountLinking?.allowDifferentEmails !== true && link.email.toLowerCase() !== userInfo.email.toLowerCase()) return redirectOnError("email_doesn't_match"); const existingAccount = await ctx.context.internalAdapter.findAccountByProviderId(String(userInfo.id), providerConfig.providerId); if (existingAccount) { if (existingAccount.userId !== link.userId) return redirectOnError("account_already_linked_to_different_user"); const updateData = Object.fromEntries(Object.entries({ accessToken: await setTokenUtil(tokens.accessToken, ctx.context), idToken: tokens.idToken, refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context), accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(",") }).filter(([_, value]) => value !== void 0)); await ctx.context.internalAdapter.updateAccount(existingAccount.id, updateData); } else if (!await ctx.context.internalAdapter.createAccount({ userId: link.userId, providerId: providerConfig.providerId, accountId: userInfo.id, accessToken: await setTokenUtil(tokens.accessToken, ctx.context), accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(","), refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context), idToken: tokens.idToken })) return redirectOnError("unable_to_link_account"); let toRedirectTo$1; try { toRedirectTo$1 = callbackURL.toString(); } catch { toRedirectTo$1 = callbackURL; } throw ctx.redirect(toRedirectTo$1); } const result = await handleOAuthUserInfo(ctx, { userInfo, account: { providerId: providerConfig.providerId, accountId: userInfo.id, ...tokens, scope: tokens.scopes?.join(",") }, callbackURL, disableSignUp: providerConfig.disableImplicitSignUp && !requestSignUp || providerConfig.disableSignUp, overrideUserInfo: providerConfig.overrideUserInfo }); if (result.error) return redirectOnError(result.error.split(" ").join("_")); const { session, user } = result.data; await setSessionCookie(ctx, { session, user }); let toRedirectTo; try { toRedirectTo = (result.isRegister ? newUserURL || callbackURL : callbackURL).toString(); } catch { toRedirectTo = result.isRegister ? newUserURL || callbackURL : callbackURL; } throw ctx.redirect(toRedirectTo); }); const OAuth2LinkAccountBodySchema = z.object({ providerId: z.string(), callbackURL: z.string(), scopes: z.array(z.string()).meta({ description: "Additional scopes to request when linking the account" }).optional(), errorCallbackURL: z.string().meta({ description: "The URL to redirect to if there is an error during the link process" }).optional() }); const oAuth2LinkAccount = (options) => createAuthEndpoint("/oauth2/link", { method: "POST", body: OAuth2LinkAccountBodySchema, use: [sessionMiddleware], metadata: { openapi: { description: "Link an OAuth2 account to the current user session", responses: { "200": { description: "Authorization URL generated successfully for linking an OAuth2 account", content: { "application/json": { schema: { type: "object", properties: { url: { type: "string", format: "uri", description: "The authorization URL to redirect the user to for linking the OAuth2 account" }, redirect: { type: "boolean", description: "Indicates that the client should redirect to the provided URL", enum: [true] } }, required: ["url", "redirect"] } } } } } } } }, async (c) => { const session = c.context.session; if (!session) throw new APIError("UNAUTHORIZED", { message: 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 }); const { providerId, clientId, clientSecret, redirectURI, authorizationUrl, discoveryUrl, pkce, scopes, prompt, accessType, authorizationUrlParams } = provider; let finalAuthUrl = authorizationUrl; if (!finalAuthUrl) { if (!discoveryUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION }); const discovery = await betterFetch(discoveryUrl, { method: "GET", headers: provider.discoveryHeaders, onError(context) { c.context.logger.error(context.error.message, context.error, { discoveryUrl }); } }); if (discovery.data) finalAuthUrl = discovery.data.authorization_endpoint; } if (!finalAuthUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION }); const state = await generateState(c, { userId: session.user.id, email: session.user.email }, void 0); const additionalParams = typeof authorizationUrlParams === "function" ? authorizationUrlParams(c) : authorizationUrlParams; const url = await createAuthorizationURL({ id: providerId, options: { clientId, clientSecret, redirectURI: redirectURI || `${c.context.baseURL}/oauth2/callback/${providerId}` }, authorizationEndpoint: finalAuthUrl, state: state.state, codeVerifier: pkce ? state.codeVerifier : void 0, scopes: c.body.scopes || scopes || [], redirectURI: redirectURI || `${c.context.baseURL}/oauth2/callback/${providerId}`, prompt, accessType, additionalParams }); return c.json({ url: url.toString(), redirect: true }); }); async function getUserInfo(tokens, finalUserInfoUrl) { if (tokens.idToken) { const decoded = decodeJwt(tokens.idToken); if (decoded) { if (decoded.sub && decoded.email) return { id: decoded.sub, emailVerified: decoded.email_verified, image: decoded.picture, ...decoded }; } } if (!finalUserInfoUrl) return null; const userInfo = await betterFetch(finalUserInfoUrl, { method: "GET", headers: { Authorization: `Bearer ${tokens.accessToken}` } }); return { id: userInfo.data?.sub ?? "", emailVerified: userInfo.data?.email_verified ?? false, email: userInfo.data?.email, image: userInfo.data?.picture, name: userInfo.data?.name, ...userInfo.data }; } const genericOAuth = (options) => { const seenIds = /* @__PURE__ */ new Set(); const nonUniqueIds = /* @__PURE__ */ new Set(); for (const config2 of options.config) { const id = config2.providerId; if (seenIds.has(id)) nonUniqueIds.add(id); seenIds.add(id); } if (nonUniqueIds.size > 0) console.warn(`Duplicate provider IDs found: ${Array.from(nonUniqueIds).join(", ")}`); return { id: "generic-oauth", init: (ctx) => { return { context: { socialProviders: options.config.map((c) => { let finalUserInfoUrl = c.userInfoUrl; return { id: c.providerId, name: c.providerId, async createAuthorizationURL(data) { let finalAuthUrl = c.authorizationUrl; if (!finalAuthUrl && c.discoveryUrl) { const discovery = await betterFetch(c.discoveryUrl, { method: "GET", headers: c.discoveryHeaders }); if (discovery.data) { finalAuthUrl = discovery.data.authorization_endpoint; finalUserInfoUrl = finalUserInfoUrl ?? discovery.data.userinfo_endpoint; } } if (!finalAuthUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION }); return createAuthorizationURL({ id: c.providerId, options: { clientId: c.clientId, clientSecret: c.clientSecret, redirectURI: c.redirectURI }, authorizationEndpoint: finalAuthUrl, state: data.state, codeVerifier: c.pkce ? data.codeVerifier : void 0, scopes: c.scopes || [], redirectURI: `${ctx.baseURL}/oauth2/callback/${c.providerId}` }); }, async validateAuthorizationCode(data) { if (c.getToken) return c.getToken(data); let finalTokenUrl = c.tokenUrl; if (c.discoveryUrl) { const discovery = await betterFetch(c.discoveryUrl, { method: "GET", headers: c.discoveryHeaders }); if (discovery.data) { finalTokenUrl = discovery.data.token_endpoint; finalUserInfoUrl = discovery.data.userinfo_endpoint; } } if (!finalTokenUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND }); return validateAuthorizationCode({ headers: c.authorizationHeaders, code: data.code, codeVerifier: data.codeVerifier, redirectURI: data.redirectURI, options: { clientId: c.clientId, clientSecret: c.clientSecret, redirectURI: c.redirectURI }, tokenEndpoint: finalTokenUrl, authentication: c.authentication }); }, async refreshAccessToken(refreshToken2) { let finalTokenUrl = c.tokenUrl; if (c.discoveryUrl) { const discovery = await betterFetch(c.discoveryUrl, { method: "GET", headers: c.discoveryHeaders }); if (discovery.data) finalTokenUrl = discovery.data.token_endpoint; } if (!finalTokenUrl) throw new APIError("BAD_REQUEST", { message: GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND }); return refreshAccessToken({ refreshToken: refreshToken2, options: { clientId: c.clientId, clientSecret: c.clientSecret }, authentication: c.authentication, tokenEndpoint: finalTokenUrl }); }, async getUserInfo(tokens) { const userInfo = c.getUserInfo ? await c.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl); if (!userInfo) return null; const userMap = await c.mapProfileToUser?.(userInfo); return { user: { id: userInfo?.id, email: userInfo?.email, emailVerified: userInfo?.emailVerified, image: userInfo?.image, name: userInfo?.name, ...userMap }, data: userInfo }; }, options: { overrideUserInfoOnSignIn: c.overrideUserInfo } }; }).concat(ctx.socialProviders) } }; }, endpoints: { signInWithOAuth2: signInWithOAuth2(options), oAuth2Callback: oAuth2Callback(options), oAuth2LinkAccount: oAuth2LinkAccount(options) }, options, $ERROR_CODES: GENERIC_OAUTH_ERROR_CODES }; }; defineErrorCodes({ PASSWORD_COMPROMISED: "The password you entered has been compromised. Please choose a different password." }); z.object({ payload: z.record(z.string(), z.any()), overrideOptions: z.record(z.string(), z.any()).optional() }); z.object({ token: z.string(), issuer: z.string().optional() }); z.object({ email: z.email().meta({ description: "Email address to send the magic link" }), name: z.string().meta({ description: 'User display name. Only used if the user is registering for the first time. Eg: "my-name"' }).optional(), callbackURL: z.string().meta({ description: "URL to redirect after magic link verification" }).optional(), newUserCallbackURL: z.string().meta({ description: "URL to redirect after new user signup. Only used if the user is registering for the first time." }).optional(), errorCallbackURL: z.string().meta({ description: "URL to redirect after error." }).optional() }); z.object({ token: z.string().meta({ description: "Verification token" }), callbackURL: z.string().meta({ description: 'URL to redirect after magic link verification, if not provided the user will be redirected to the root URL. Eg: "/dashboard"' }).optional(), errorCallbackURL: z.string().meta({ description: "URL to redirect after error." }).optional(), newUserCallbackURL: z.string().meta({ description: "URL to redirect after new user signup. Only used if the user is registering for the first time." }).optional() }); z.object({ clientId: z.string(), clientSecret: z.string().optional(), type: z.enum([ "web", "native", "user-agent-based", "public" ]), name: z.string(), icon: z.string().optional(), metadata: z.string().optional(), disabled: z.boolean().optional().default(false), redirectUrls: z.string(), userId: z.string().optional(), createdAt: z.date(), updatedAt: z.date() }); z.object({ accept: z.boolean(), consent_code: z.string().optional().nullish() }); z.record(z.any(), z.any()); z.object({ redirect_uris: z.array(z.string()).meta({ description: 'A list of redirect URIs. Eg: ["https://client.example.com/callback"]' }), token_endpoint_auth_method: z.enum([ "none", "client_secret_basic", "client_secret_post" ]).meta({ description: 'The authentication method for the token endpoint. Eg: "client_secret_basic"' }).default("client_secret_basic").optional(), grant_types: z.array(z.enum([ "authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer", "urn:ietf:params:oauth:grant-type:saml2-bearer" ])).meta({ description: 'The grant types supported by the application. Eg: ["authorization_code"]' }).default(["authorization_code"]).optional(), response_types: z.array(z.enum(["code", "token"])).meta({ description: 'The response types supported by the application. Eg: ["code"]' }).default(["code"]).optional(), client_name: z.string().meta({ description: 'The name of the application. Eg: "My App"' }).optional(), client_uri: z.string().meta({ description: 'The URI of the application. Eg: "https://client.example.com"' }).optional(), logo_uri: z.string().meta({ description: 'The URI of the application logo. Eg: "https://client.example.com/logo.png"' }).optional(), scope: z.string().meta({ description: 'The scopes supported by the application. Separated by spaces. Eg: "profile email"' }).optional(), contacts: z.array(z.string()).meta({ description: 'The contact information for the application. Eg: ["admin@example.com"]' }).optional(), tos_uri: z.string().meta({ description: 'The URI of the application terms of service. Eg: "https://client.example.com/tos"' }).optional(), policy_uri: z.string().meta({ description: 'The URI of the application privacy policy. Eg: "https://client.example.com/policy"' }).optional(), jwks_uri: z.string().meta({ description: 'The URI of the application JWKS. Eg: "https://client.example.com/jwks"' }).optional(), jwks: z.record(z.any(), z.any()).meta({ description: 'The JWKS of the application. Eg: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]}' }).optional(), metadata: z.record(z.any(), z.any()).meta({ description: 'The metadata of the application. Eg: {"key": "value"}' }).optional(), software_id: z.string().meta({ description: 'The software ID of the application. Eg: "my-software"' }).optional(), software_version: z.string().meta({ description: 'The software version of the application. Eg: "1.0.0"' }).optional(), software_statement: z.string().meta({ description: "The software statement of the application." }).optional() }); z.object({ redirect_uris: z.array(z.string()), token_endpoint_auth_method: z.enum([ "none", "client_secret_basic", "client_secret_post" ]).default("client_secret_basic").optional(), grant_types: z.array(z.enum([ "authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer", "urn:ietf:params:oauth:grant-type:saml2-bearer" ])).default(["authorization_code"]).optional(), response_types: z.array(z.enum(["code", "token"])).default(["code"]).optional(), client_name: z.string().optional(), client_uri: z.string().optional(), logo_uri: z.string().optional(), scope: z.string().optional(), contacts: z.array(z.string()).optional(), tos_uri: z.string().optional(), policy_uri: z.string().optional(), jwks_uri: z.string().optional(), jwks: z.record(z.string(), z.any()).optional(), metadata: z.record(z.any(), z.any()).optional(), software_id: z.string().optional(), software_version: z.string().optional(), software_statement: z.string().optional() }); z.record(z.any(), z.any()); defineErrorCodes({ INVALID_SESSION_TOKEN: "Invalid session token" }); z.object({ sessionToken: z.string().meta({ description: "The session token to set as active" }) }); z.object({ sessionToken: z.string().meta({ description: "The session token to revoke" }) }); z.object({ callbackURL: z.string().meta({ description: "The URL to redirect to after the proxy" }), cookies: z.string().meta({ description: "The cookies to set after the proxy" }) }); z.object({ idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }) }); z.object({ token: z.string().meta({ description: 'The token to verify. Eg: "some-token"' }) }); createAuthMiddleware(async () => { return {}; }); createAuthMiddleware({ use: [sessionMiddleware] }, async (ctx) => { return { session: ctx.context.session }; }); defineErrorCodes({ 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_SLUG_ALREADY_TAKEN: "Organization slug already taken", 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_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM: "You are not allowed to create a new team", TEAM_ALREADY_EXISTS: "Team already exists", TEAM_NOT_FOUND: "Team not found", YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER: "You cannot leave the organization as the only owner", YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER: "You cannot leave the organization without an 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", EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION: "Email verification required before accepting or rejecting 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", YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE: "You are not allowed to invite a user with this role", FAILED_TO_RETRIEVE_INVITATION: "Failed to retrieve invitation", YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS: "You have reached the maximum number of teams", UNABLE_TO_REMOVE_LAST_TEAM: "Unable to remove last team", YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER: "You are not allowed to update this member", ORGANIZATION_MEMBERSHIP_LIMIT_REACHED: "Organization membership limit reached", YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION: "You are not allowed to create teams in this organization", YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION: "You are not allowed to delete teams in this organization", YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM: "You are not allowed to update this team", YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM: "You are not allowed to delete this team", INVITATION_LIMIT_REACHED: "Invitation limit reached", TEAM_MEMBER_LIMIT_REACHED: "Team member limit reached", USER_IS_NOT_A_MEMBER_OF_THE_TEAM: "User is not a member of the team", YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM: "You are not allowed to list the members of this team", YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM: "You do not have an active team", YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER: "You are not allowed to create a new member", YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER: "You are not allowed to remove a team member", YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION: "You are not allowed to access this organization as an owner", YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION: "You are not a member of this organization", MISSING_AC_INSTANCE: "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information", YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE: "You must be in an organization to create a role", YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE: "You are not allowed to create a role", YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE: "You are not allowed to update a role", YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE: "You are not allowed to delete a role", YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE: "You are not allowed to read a role", YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE: "You are not allowed to list a role", YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE: "You are not allowed to get a role", TOO_MANY_ROLES: "This organization has too many roles", INVALID_RESOURCE: "The provided permission includes an invalid resource", ROLE_NAME_IS_ALREADY_TAKEN: "That role name is already taken", CANNOT_DELETE_A_PRE_DEFINED_ROLE: "Cannot delete a pre-defined role", ROLE_IS_ASSIGNED_TO_MEMBERS: "Cannot delete a role that is assigned to members. Please reassign the members to a different role first" }); z.object({ organizationId: z.string().optional().meta({ description: "The id of the organization to create the role in. If not provided, the user's active organization will be used." }), role: z.string().meta({ description: "The name of the role to create" }), permission: z.record(z.string(), z.array(z.string())).meta({ description: "The permission to assign to the role" }) }); z.object({ organizationId: z.string().optional().meta({ description: "The id of the organization to create the role in. If not provided, the user's active organization will be used." }) }).and(z.union([z.object({ roleName: z.string().nonempty().meta({ description: "The name of the role to delete" }) }), z.object({ roleId: z.string().nonempty().meta({ description: "The id of the role to delete" }) })])); z.object({ organizationId: z.string().optional().meta({ description: "The id of the organization to list roles for. If not provided, the user's active organization will be used." }) }).optional(); z.object({ organizationId: z.string().optional().meta({ description: "The id of the organization to read a role for. If not provided, the user's active organization will be used." }) }).and(z.union([z.object({ roleName: z.string().nonempty().meta({ description: "The name of the role to read" }) }), z.object({ roleId: z.string().nonempty().meta({ description: "The id of the role to read" }) })])).optional(); z.union([z.object({ roleName: z.string().nonempty().meta({ description: "The name of the role to update" }) }), z.object({ roleId: z.string().nonempty().meta({ description: "The id of the role to update" }) })]); z.object({ email: z.string().meta({ description: "The email address of the user to invite" }), role: z.union([z.string().meta({ description: "The role to assign to the user" }), z.array(z.string().meta({ description: "The roles to assign to the user" }))]).meta({ description: 'The role(s) to assign to the user. It can be `admin`, `member`, owner. Eg: "member"' }), organizationId: z.string().meta({ description: "The organization ID to invite the user to" }).optional(), resend: z.boolean().meta({ description: "Resend the invitation email, if the user is already invited. Eg: true" }).optional(), teamId: z.union([z.string().meta({ description: "The team ID to invite the user to" }).optional(), z.array(z.string()).meta({ description: "The team IDs to invite the user to" }).optional()]) }); z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to accept" }) }); z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to reject" }) }); z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to cancel" }) }); z.object({ id: z.string().meta({ description: "The ID of the invitation to get" }) }); z.object({ organizationId: z.string().meta({ description: "The ID of the organization to list invitations for" }).optional() }).optional(); z.object({ userId: z.coerce.string().meta({ description: 'The user Id which represents the user to be added as a member. If `null` is provided, then it\'s expected to provide session headers. Eg: "user-id"' }), role: z.union([z.string(), z.array(z.string())]).meta({ description: 'The role(s) to assign to the new member. Eg: ["admin", "sale"]' }), organizationId: z.string().meta({ description: `An optional organization ID to pass. If not provided, will default to the user's active organization. Eg: "org-id"` }).optional(), teamId: z.string().meta({ description: 'An optional team ID to add the member to. Eg: "team-id"' }).optional() }); z.object({ memberIdOrEmail: z.string().meta({ description: "The ID or email of the member to remove" }), organizationId: z.string().meta({ description: 'The ID of the organization to remove the member from. If not provided, the active organization will be used. Eg: "org-id"' }).optional() }); z.object({ role: z.union([z.string(), z.array(z.string())]).meta({ description: 'The new role to be applied. This can be a string or array of strings representing the roles. Eg: ["admin", "sale"]' }), memberId: z.string().meta({ description: 'The member id to apply the role update to. Eg: "member-id"' }), organizationId: z.string().meta({ description: 'An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization. Eg: "organization-id"' }).optional() }); z.object({ organizationId: z.string().meta({ description: 'The organization Id for the member to leave. Eg: "organization-id"' }) }); z.object({ userId: z.string().meta({ description: "The user ID to get the role for. If not provided, will default to the current user's" }).optional(), organizationId: z.string().meta({ description: `The organization ID to list members for. If not provided, will default to the user's active organization. Eg: "organization-id"` }).optional(), organizationSlug: z.string().meta({ description: `The organization slug to list members for. If not provided, will default to the user's active organization. Eg: "organization-slug"` }).optional() }).optional(); z.object({ name: z.string().min(1).meta({ description: "The name of the organization" }), slug: z.string().min(1).meta({ description: "The slug of the organization" }), userId: z.coerce.string().meta({ description: 'The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: "user-id"' }).optional(), logo: z.string().meta({ description: "The logo of the organization" }).optional(), metadata: z.record(z.string(), z.any()).meta({ description: "The metadata of the organization" }).optional(), keepCurrentActiveOrganization: z.boolean().meta({ description: "Whether to keep the current active organization active after creating a new one. Eg: true" }).optional() }); z.object({ slug: z.string().meta({ description: 'The organization slug to check. Eg: "my-org"' }) }); z.object({ name: z.string().min(1).meta({ description: "The name of the organization" }).optional(), slug: z.string().min(1).meta({ description: "The slug of the organization" }).optional(), logo: z.string().meta({ description: "The logo of the organization" }).optional(), metadata: z.record(z.string(), z.any()).meta({ description: "The metadata of the organization" }).optional() }); z.object({ organizationId: z.string().meta({ description: "The organization id to delete" }) }); z.optional(z.object({ organizationId: z.string().meta({ description: "The organization id to get" }).optional(), organizationSlug: z.string().meta({ description: "The organization slug to get" }).optional(), membersLimit: z.number().or(z.string().transform((val) => parseInt(val))).meta({ description: "The limit of members to get. By default, it uses the membershipLimit option which defaults to 100." }).optional() })); z.object({ organizationId: z.string().meta({ description: 'The organization id to set as active. It can be null to unset the active organization. Eg: "org-id"' }).nullable().optional(), organizationSlug: z.string().meta({ description: 'The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: "org-slug"' }).optional() }); const roleSchema = z.string(); const invitationStatus = z.enum([ "pending", "accepted", "rejected", "canceled" ]).default("pending"); z.object({ id: z.string().default(generateId), name: z.string(), slug: z.string(), logo: z.string().nullish().optional(), metadata: z.record(z.string(), z.unknown()).or(z.string().transform((v) => JSON.parse(v))).optional(), createdAt: z.date() }); z.object({ id: z.string().default(generateId), organizationId: z.string(), userId: z.coerce.string(), role: roleSchema, createdAt: z.date().default(() => /* @__PURE__ */ new Date()) }); z.object({ id: z.string().default(generateId), organizationId: z.string(), email: z.string(), role: roleSchema, status: invitationStatus, teamId: z.string().nullish(), inviterId: z.string(), expiresAt: z.date(), createdAt: z.date().default(() => /* @__PURE__ */ new Date()) }); z.object({ id: z.string().default(generateId), name: z.string().min(1), organizationId: z.string(), createdAt: z.date(), updatedAt: z.date().optional() }); z.object({ id: z.string().default(generateId), teamId: z.string(), userId: z.string(), createdAt: z.date().default(() => /* @__PURE__ */ new Date()) }); z.object({ id: z.string().default(generateId), organizationId: z.string(), role: z.string(), permission: z.record(z.string(), z.array(z.string())), createdAt: z.date().default(() => /* @__PURE__ */ new Date()), updatedAt: z.date().optional() }); const defaultRoles = [ "admin", "member", "owner" ]; z.union([z.enum(defaultRoles), z.array(z.enum(defaultRoles))]); z.object({ name: z.string().meta({ description: 'The name of the team. Eg: "my-team"' }), organizationId: z.string().meta({ description: 'The organization ID which the team will be created in. Defaults to the active organization. Eg: "organization-id"' }).optional() }); z.object({ teamId: z.string().meta({ description: `The team ID of the team to remove. Eg: "team-id"` }), organizationId: z.string().meta({ description: `The organization ID which the team falls under. If not provided, it will default to the user's active organization. Eg: "organization-id"` }).optional() }); z.optional(z.object({ organizationId: z.string().meta({ description: `The organization ID which the teams are under to list. Defaults to the users active organization. Eg: "organization-id"` }).optional() })); z.object({ teamId: z.string().meta({ description: "The team id to set as active. It can be null to unset the active team" }).nullable().optional() }); z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) })); z.object({ teamId: z.string().meta({ description: "The team the user should be a member of." }), userId: z.coerce.string().meta({ description: "The user Id which represents the user to be added as a member." }) }); z.object({ teamId: z.string().meta({ description: "The team the user should be removed from." }), userId: z.coerce.string().meta({ description: "The user which should be removed from the team." }) }); z.object({ organizationId: z.string().optional() }).and(z.union([z.object({ permission: z.record(z.string(), z.array(z.string())), permissions: z.undefined() }), z.object({ permission: z.undefined(), permissions: z.record(z.string(), z.array(z.string())) })])); defineErrorCodes({ INVALID_PHONE_NUMBER: "Invalid phone number", PHONE_NUMBER_EXIST: "Phone number already exists", PHONE_NUMBER_NOT_EXIST: "phone number isn't registered", 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", PHONE_NUMBER_CANNOT_BE_UPDATED: "Phone number cannot be updated", SEND_OTP_NOT_IMPLEMENTED: "sendOTP not implemented", TOO_MANY_ATTEMPTS: "Too many attempts" }); z.object({ phoneNumber: z.string().meta({ description: 'Phone number to sign in. Eg: "+1234567890"' }), password: z.string().meta({ description: "Password to use for sign in." }), rememberMe: z.boolean().meta({ description: "Remember the session. Eg: true" }).optional() }); z.object({ phoneNumber: z.string().meta({ description: 'Phone number to send OTP. Eg: "+1234567890"' }) }); z.object({ phoneNumber: z.string().meta({ description: 'Phone number to verify. Eg: "+1234567890"' }), code: z.string().meta({ description: 'OTP code. Eg: "123456"' }), disableSession: z.boolean().meta({ description: "Disable session creation after verification. Eg: false" }).optional(), updatePhoneNumber: z.boolean().meta({ description: "Check if there is a session and update the phone number. Eg: true" }).optional() }).and(z.record(z.string(), z.any())); z.object({ phoneNumber: z.string() }); z.object({ otp: z.string().meta({ description: 'The one time password to reset the password. Eg: "123456"' }), phoneNumber: z.string().meta({ description: 'The phone number to the account which intends to reset the password for. Eg: "+1234567890"' }), newPassword: z.string().meta({ description: `The new password. Eg: "new-and-secure-password"` }) }); z.object({ walletAddress: z.string().regex(/^0[xX][a-fA-F0-9]{40}$/i).length(42), chainId: z.number().int().positive().max(2147483647).optional().default(1) }); defineErrorCodes({ 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", INVALID_CODE: "Invalid code", TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: "Too many attempts. Please request a new code.", INVALID_TWO_FACTOR_COOKIE: "Invalid two factor cookie" }); z.object({ code: z.string().meta({ description: `A backup code to verify. Eg: "123456"` }), disableSession: z.boolean().meta({ description: "If true, the session cookie will not be set." }).optional(), trustDevice: z.boolean().meta({ description: "If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true" }).optional() }); z.object({ userId: z.coerce.string().meta({ description: `The user ID to view all backup codes. Eg: "user-id"` }) }); z.object({ password: z.string().meta({ description: "The users password." }) }); z.object({ code: z.string().meta({ description: 'The otp code to verify. Eg: "012345"' }), trustDevice: z.boolean().optional().meta({ description: "If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true" }) }); z.object({ trustDevice: z.boolean().optional().meta({ description: "If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true" }) }).optional(); z.object({ secret: z.string().meta({ description: "The secret to generate the TOTP code" }) }); z.object({ password: z.string().meta({ description: "User password" }) }); z.object({ code: z.string().meta({ description: 'The otp code to verify. Eg: "012345"' }), trustDevice: z.boolean().meta({ description: "If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true" }).optional() }); z.object({ password: z.string().meta({ description: "User password" }), issuer: z.string().meta({ description: "Custom issuer for the TOTP URI" }).optional() }); z.object({ password: z.string().meta({ description: "User password" }) }); defineErrorCodes({ INVALID_USERNAME_OR_PASSWORD: "Invalid username or password", EMAIL_NOT_VERIFIED: "Email not verified", UNEXPECTED_ERROR: "Unexpected error", USERNAME_IS_ALREADY_TAKEN: "Username is already taken. Please try another.", USERNAME_TOO_SHORT: "Username is too short", USERNAME_TOO_LONG: "Username is too long", INVALID_USERNAME: "Username is invalid", INVALID_DISPLAY_USERNAME: "Display username is invalid" }); z.object({ username: z.string().meta({ description: "The username of the user" }), password: z.string().meta({ description: "The password of the user" }), rememberMe: z.boolean().meta({ description: "Remember the user session" }).optional(), callbackURL: z.string().meta({ description: "The URL to redirect to after email verification" }).optional() }); z.object({ username: z.string().meta({ description: "The username to check" }) }); const auth = betterAuth({ baseURL: private_env.ORIGIN, secret: private_env.BETTER_AUTH_SECRET, database: drizzleAdapter(db, { provider: "pg" }), emailAndPassword: { enabled: true }, databaseHooks: { user: { create: { after: async (user) => { const [personalTeam] = await db.insert(team).values({ name: user.name, isPersonal: true, ownerId: user.id, avatarUrl: user.image }).returning(); await db.insert(teamMember).values({ teamId: personalTeam.id, userId: user.id, role: "owner" }); } } } }, plugins: [ genericOAuth({ config: [ { providerId: "osu", clientId: private_env.OSU_CLIENT_ID, clientSecret: private_env.OSU_CLIENT_SECRET, authorizationUrl: "https://osu.ppy.sh/oauth/authorize", tokenUrl: "https://osu.ppy.sh/oauth/token", userInfoUrl: "https://osu.ppy.sh/api/v2/me", scopes: ["identify", "public"], pkce: false, authentication: "post", // ── FIX: Use ORIGIN env var instead of hardcoded localhost ── redirectURI: `${private_env.ORIGIN}/api/auth/oauth2/callback/osu`, getToken: async ({ code, redirectURI }) => { const res = await fetch("https://osu.ppy.sh/oauth/token", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: private_env.OSU_CLIENT_ID, client_secret: private_env.OSU_CLIENT_SECRET, code, grant_type: "authorization_code", redirect_uri: redirectURI }) }); const data = await res.json(); if (!res.ok) { console.error("osu! token exchange failed:", data); throw new Error("Token exchange failed"); } return { accessToken: data.access_token, refreshToken: data.refresh_token, accessTokenExpiresAt: new Date(Date.now() + data.expires_in * 1e3), scopes: ["identify", "public"], raw: data }; }, mapProfileToUser(profile) { return { name: profile.username, image: profile.avatar_url, email: `${profile.id}@osu.local` }; } } ] }), sveltekitCookies(getRequestEvent) // must be last ] }); export { auth as a, createKyselyAdapter as c, getKyselyDatabaseType as g, svelteKitHandler as s };