UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

860 lines (859 loc) • 30.7 kB
import { getSessionDefaultFields, parseSessionOutput, parseUserOutput } from "./schema.mjs"; import { getDate } from "../utils/date.mjs"; import { getStorageOption, processIdentifier } from "./verification-token-storage.mjs"; import { getWithHooks } from "./with-hooks.mjs"; import { getCurrentAdapter, getCurrentAuthContext, runWithTransaction } from "@better-auth/core/context"; import { generateId } from "@better-auth/core/utils/id"; import { safeJSONParse } from "@better-auth/core/utils/json"; import { getIp } from "@better-auth/core/utils/ip"; import { base64Url } from "@better-auth/utils/base64"; import { createHash } from "@better-auth/utils/hash"; //#region src/db/internal-adapter.ts function getTTLSeconds(expiresAt, now = Date.now()) { const expiresMs = typeof expiresAt === "number" ? expiresAt : expiresAt.getTime(); return Math.max(Math.floor((expiresMs - now) / 1e3), 0); } const createInternalAdapter = (adapter, ctx) => { const logger = ctx.logger; const options = ctx.options; const secondaryStorage = options.secondaryStorage; const verificationConsumeLocks = /* @__PURE__ */ new Map(); let warnedNonAtomicConsume = false; const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7; const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx); async function refreshUserSessions(user) { if (!secondaryStorage) return; const listRaw = await secondaryStorage.get(`active-sessions-${user.id}`); if (!listRaw) return; const now = Date.now(); const validSessions = (safeJSONParse(listRaw) || []).filter((s) => s.expiresAt > now); 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 = getTTLSeconds(parsed.session.expiresAt, now); await secondaryStorage.set(token, JSON.stringify({ session: parsed.session, user }), Math.floor(sessionTTL)); })); } async function withVerificationConsumeLock(key, fn) { const previous = verificationConsumeLocks.get(key) ?? Promise.resolve(); let release; const current = new Promise((resolve) => { release = resolve; }); const next = previous.catch(() => {}).then(() => current); verificationConsumeLocks.set(key, next); await previous.catch(() => {}); try { return await fn(); } finally { release(); if (verificationConsumeLocks.get(key) === next) verificationConsumeLocks.delete(key); } } return { createOAuthUser: async (user, account) => { return runWithTransaction(adapter, async () => { const createdUser = await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...user, email: user.email?.toLowerCase() }, "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, options) => { if (secondaryStorage) { const currentList = await secondaryStorage.get(`active-sessions-${userId}`); if (!currentList) return []; const list = safeJSONParse(currentList) || []; const now = Date.now(); const seenTokens = /* @__PURE__ */ new Set(); const sessions = []; for (const { token, expiresAt } of list) { if (expiresAt <= now || 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 }, ...options?.onlyActiveSessions ? [{ field: "expiresAt", value: /* @__PURE__ */ new Date(), operator: "gt" }] : []] }); }, 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 headers = await (async () => { const ctx = await getCurrentAuthContext().catch(() => null); return ctx?.headers || ctx?.request?.headers; })(); const storeInDb = options.session?.storeSessionInDatabase; const { id: _, ...rest } = override || {}; let sessionId; if (secondaryStorage && !storeInDb) { const generatedId = ctx.generateId({ model: "session" }); sessionId = generatedId !== false ? generatedId : generateId(); } const defaultAdditionalFields = getSessionDefaultFields(options); const data = { ...sessionId ? { id: sessionId } : {}, ipAddress: headers ? getIp(headers, options) || "" : "", userAgent: headers?.get("user-agent") || "", ...rest, /** * If the user doesn't want to be remembered * set the session to expire in 1 day. * The cookie will be set to expire at the end of the session */ 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) => { /** * store the session token for the user * so we can retrieve it later for listing sessions */ const currentList = await secondaryStorage.get(`active-sessions-${userId}`); let list = []; const now = Date.now(); if (currentList) { list = safeJSONParse(currentList) || []; list = list.filter((session) => session.expiresAt > now && session.token !== data.token); } const sorted = [...list, { token: data.token, expiresAt: data.expiresAt.getTime() }].sort((a, b) => a.expiresAt - b.expiresAt); const furthestSessionTTL = getTTLSeconds(sorted.at(-1)?.expiresAt ?? data.expiresAt.getTime(), now); 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 = getTTLSeconds(data.expiresAt, now); if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({ session: sessionData, user }), sessionTTL); return sessionData; }, executeMainFn: storeInDb } : void 0); }, findSession: async (token) => { if (secondaryStorage) { const sessionStringified = await secondaryStorage.get(token); if (!sessionStringified && (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase)) 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, options) => { if (secondaryStorage) { const sessions = []; 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) return []; const expiresAt = new Date(s.session.expiresAt); if (options?.onlyActiveSessions && expiresAt <= /* @__PURE__ */ new Date()) 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.push(session); } catch { continue; } } return sessions; } const sessions = await (await getCurrentAdapter(adapter)).findMany({ model: "session", where: [{ field: "token", value: sessionTokens, operator: "in" }, ...options?.onlyActiveSessions ? [{ field: "expiresAt", value: /* @__PURE__ */ new Date(), operator: "gt" }] : []], 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 now = Date.now(); const expiresMs = new Date(updatedSession.expiresAt).getTime(); const sessionTTL = getTTLSeconds(expiresMs, now); 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 > now).concat([{ token: sessionToken, expiresAt: expiresMs }]).sort((a, b) => a.expiresAt - b.expiresAt); const furthestSessionExp = sorted.at(-1)?.expiresAt; if (furthestSessionExp && furthestSessionExp > now) await secondaryStorage.set(listKey, JSON.stringify(sorted), getTTLSeconds(furthestSessionExp, now)); 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) { logger.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 now = Date.now(); const filtered = list.filter((session) => session.expiresAt > now && session.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), getTTLSeconds(furthestSessionExp, now)); else await secondaryStorage.delete(`active-sessions-${userId}`); } else logger.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); }, /** * Delete an account by its primary key. * * @param id - The account row's primary key (the `id` column, not the `accountId` column). */ deleteAccount: async (id) => { await deleteWithHooks([{ field: "id", value: id }], "account", void 0); }, deleteUserSessions: async (userId) => { if (secondaryStorage) { const activeSession = await secondaryStorage.get(`active-sessions-${userId}`); const sessions = activeSession ? safeJSONParse(activeSession) : []; if (!sessions) return; for (const session of sessions) await secondaryStorage.delete(session.token); await secondaryStorage.delete(`active-sessions-${userId}`); if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return; } await deleteManyWithHooks([{ field: "userId", value: userId }], "session", void 0); }, deleteSessions: async (sessionTokens) => { if (secondaryStorage) { for (const sessionToken of sessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken); if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return; } await deleteManyWithHooks([{ field: "token", value: sessionTokens, operator: "in" }], "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) => { const result = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }], join: { ...options?.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, ...data.email ? { email: data.email.toLowerCase() } : {} }, [{ field: "id", value: userId }], "user", void 0); await refreshUserSessions(user); return user; }, updateUserByEmail: async (email, data) => { const user = await updateWithHooks({ ...data, ...data.email ? { email: data.email.toLowerCase() } : {} }, [{ 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 }] }); }, 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) => { const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier); const storedIdentifier = await processIdentifier(data.identifier, storageOption); return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...data, identifier: storedIdentifier }, "verification", secondaryStorage ? { async fn(verificationData) { const ttl = getTTLSeconds(verificationData.expiresAt); if (ttl > 0) await secondaryStorage.set(`verification:${storedIdentifier}`, JSON.stringify(verificationData), ttl); return verificationData; }, executeMainFn: options.verification?.storeInDatabase } : void 0); }, findVerificationValue: async (identifier) => { const storageOption = getStorageOption(identifier, options.verification?.storeIdentifier); const storedIdentifier = await processIdentifier(identifier, storageOption); if (secondaryStorage) { const cached = await secondaryStorage.get(`verification:${storedIdentifier}`); if (cached) { const parsed = safeJSONParse(cached); if (parsed) return parsed; } if (storageOption && storageOption !== "plain") { const plainCached = await secondaryStorage.get(`verification:${identifier}`); if (plainCached) { const parsed = safeJSONParse(plainCached); if (parsed) return parsed; } } if (!options.verification?.storeInDatabase) return null; } const currentAdapter = await getCurrentAdapter(adapter); async function findByIdentifier(id) { return currentAdapter.findMany({ model: "verification", where: [{ field: "identifier", value: id }], sortBy: { field: "createdAt", direction: "desc" }, limit: 1 }); } let verification = await findByIdentifier(storedIdentifier); if (!verification.length && storageOption && storageOption !== "plain") verification = await findByIdentifier(identifier); if (!options.verification?.disableCleanup) await deleteManyWithHooks([{ field: "expiresAt", value: /* @__PURE__ */ new Date(), operator: "lt" }], "verification", void 0); return verification[0] || null; }, deleteVerificationByIdentifier: async (identifier) => { const storedIdentifier = await processIdentifier(identifier, getStorageOption(identifier, options.verification?.storeIdentifier)); if (secondaryStorage) await secondaryStorage.delete(`verification:${storedIdentifier}`); if (!secondaryStorage || options.verification?.storeInDatabase) await deleteWithHooks([{ field: "identifier", value: storedIdentifier }], "verification", void 0); }, /** * Atomically consume a single-use verification row by `identifier` and * return it. The first concurrent caller receives the latest row for the * identifier; every other caller racing against it receives `null`. * * Race-safe replacement for the `findVerificationValue` then * `deleteVerificationByIdentifier` pair. Callers MUST gate any state * change (issue session, mint token, change password) on a non-null * return value, because consuming one row invalidates the whole * identifier and stale rows cannot be replayed. * * Rows past their `expiresAt` are treated as already invalid: the row * is still deleted (so it cannot be replayed later) but `null` is * returned. Callers do not need their own `expiresAt` gate. * * The secondary-storage-only path (`storeInDatabase: false`) is atomic * only when the configured storage implements `getAndDelete`; otherwise * it falls back to an in-process lock around `get` then `delete` and * warns once, since that fallback cannot coordinate across processes. * * FIXME(consume-atomic): make `SecondaryStorage.getAndDelete` required * in the next breaking release, or require database-backed verification * storage for security-sensitive consume paths, so the non-atomic * fallback can be removed entirely. */ consumeVerificationValue: async (identifier) => { const storageOption = getStorageOption(identifier, options.verification?.storeIdentifier); const storedIdentifier = await processIdentifier(identifier, storageOption); const identifiersToTry = storageOption && storageOption !== "plain" ? [storedIdentifier, identifier] : [storedIdentifier]; const hydrateCachedVerification = (raw) => { if (!raw) return null; const candidate = typeof raw === "string" ? safeJSONParse(raw) : typeof raw === "object" ? raw : null; if (!candidate) return null; const expiresAt = new Date(candidate.expiresAt); if (!Number.isFinite(expiresAt.getTime())) return null; return { ...candidate, expiresAt }; }; let consumed = null; if (secondaryStorage && !options.verification?.storeInDatabase) { const consumeCacheKey = async (key) => { if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key)); if (!warnedNonAtomicConsume) { warnedNonAtomicConsume = true; logger.warn("Secondary storage does not implement `getAndDelete`, so single-use verification values cannot be consumed atomically across processes. Implement `getAndDelete` or use database-backed verification storage to guarantee single use."); } return withVerificationConsumeLock(key, async () => { const raw = await secondaryStorage.get(key); const parsed = hydrateCachedVerification(raw); if (!parsed) return null; await secondaryStorage.delete(key); return parsed; }); }; for (const stored of identifiersToTry) { const cached = await consumeCacheKey(`verification:${stored}`); if (!cached) continue; await Promise.all(identifiersToTry.filter((candidate) => candidate !== stored).map((candidate) => secondaryStorage.delete(`verification:${candidate}`))); consumed = cached; break; } } else { const consumeByIdentifier = async (id) => withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => { const txAdapter = await getCurrentAdapter(adapter); const where = [{ field: "identifier", value: id }]; const latest = (await txAdapter.findMany({ model: "verification", where, sortBy: { field: "createdAt", direction: "desc" }, limit: 1 }))[0] ?? null; if (!latest) return null; return consumeOneWithHooks("verification", [{ field: "id", value: latest.id }], async () => { const row = await txAdapter.consumeOne({ model: "verification", where: [{ field: "id", value: latest.id }] }); if (!row) return null; await txAdapter.deleteMany({ model: "verification", where }); return row; }, latest); })); for (const stored of identifiersToTry) { consumed = await consumeByIdentifier(stored); if (consumed) break; } if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`))); } if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null; return consumed; }, /** * First-writer-wins create keyed by a deterministic primary key derived * from `identifier`. Returns `true` when this caller created the row and * `false` when a row for the same identifier already existed. * * The dual of `consumeVerificationValue`: where consume races to delete a * marker exactly once, reserve races to create a marker exactly once. Use * it for replay tombstones (a SAML assertion id, a JWT `jti`) where the * first caller wins and every later caller must observe that the marker is * already taken. * * The `verification.identifier` column is non-unique, so uniqueness comes * from a deterministic primary key (`SHA-256` of `reserve:<identifier>`). * The database path is atomic: the primary key turns the INSERT into the * first-writer-wins gate, and a duplicate is detected portably by * re-reading the row rather than matching adapter-specific errors. The * secondary-storage-only path has no primary key to enforce uniqueness, so * it is best-effort under concurrency. * * The atomic guarantee requires the configured adapter to reject a * duplicate primary key on insert, which every real database enforces. The * in-memory adapter does not enforce primary-key uniqueness, so reservation * is best-effort there (it is intended for development and tests). */ reserveVerificationValue: async (data) => { const reservationId = base64Url.encode(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), { padding: false }); const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier); const storedIdentifier = await processIdentifier(data.identifier, storageOption); if (secondaryStorage && !options.verification?.storeInDatabase) { const cacheKey = `verification:${storedIdentifier}`; if (await secondaryStorage.get(cacheKey)) return false; await secondaryStorage.set(cacheKey, JSON.stringify({ id: reservationId, identifier: storedIdentifier, value: data.value, expiresAt: data.expiresAt }), getTTLSeconds(data.expiresAt)); return true; } try { await adapter.create({ model: "verification", data: { id: reservationId, identifier: storedIdentifier, value: data.value, expiresAt: data.expiresAt, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }, forceAllowId: true }); } catch (error) { if (await adapter.findOne({ model: "verification", where: [{ field: "id", value: reservationId }] })) return false; throw error; } if (secondaryStorage) { const ttl = getTTLSeconds(data.expiresAt); if (ttl > 0) await secondaryStorage.set(`verification:${storedIdentifier}`, JSON.stringify({ id: reservationId, identifier: storedIdentifier, value: data.value, expiresAt: data.expiresAt }), ttl); } return true; }, updateVerificationByIdentifier: async (identifier, data) => { const storedIdentifier = await processIdentifier(identifier, getStorageOption(identifier, options.verification?.storeIdentifier)); if (secondaryStorage) { const cached = await secondaryStorage.get(`verification:${storedIdentifier}`); if (cached) { const parsed = safeJSONParse(cached); if (parsed) { const updated = { ...parsed, ...data }; const expiresAt = updated.expiresAt ?? parsed.expiresAt; const ttl = getTTLSeconds(expiresAt instanceof Date ? expiresAt : new Date(expiresAt)); if (ttl > 0) await secondaryStorage.set(`verification:${storedIdentifier}`, JSON.stringify(updated), ttl); if (!options.verification?.storeInDatabase) return updated; } } } if (!secondaryStorage || options.verification?.storeInDatabase) return await updateWithHooks(data, [{ field: "identifier", value: storedIdentifier }], "verification", void 0); return data; }, refreshUserSessions }; }; //#endregion export { createInternalAdapter };