diff --git a/packages/core/src/adapters.ts b/packages/core/src/adapters.ts index 43c4800870..7f8f861a71 100644 --- a/packages/core/src/adapters.ts +++ b/packages/core/src/adapters.ts @@ -183,6 +183,20 @@ export interface AdapterUser extends User { * It is `null` if the user has not signed in with the Email provider yet, or the date of the first successful signin. */ emailVerified: Date | null + /** The user's phoneNumber. */ + phoneNumber?: string + /** + * Whether the user has verified their phoneNumber via an [SMS provider](https://authjs.dev/getting-started/authentication/sms). + * It is `null` if the user has not signed in with the SMS provider yet, or the date of the first successful signin. + */ + phoneNumberVerified?: Date | null + /** A unique identifier for the anonymous user. */ + anonymousId?: string + /** + * Whether the user has a verified anonymousId. + * It is `null` if the user has not signed in with the Anonymous provider yet, or the date of the first successful signin. + */ + anonymousIdVerified?: Date | null } /** @@ -190,7 +204,7 @@ export interface AdapterUser extends User { */ export type AdapterAccountType = Extract< ProviderType, - "oauth" | "oidc" | "email" | "webauthn" + "oauth" | "oidc" | "email" | "webauthn" | "sms" | "anonymous" > /** @@ -298,6 +312,18 @@ export interface Adapter { * See also [Verification tokens](https://authjs.dev/guides/creating-a-database-adapter#verification-tokens) */ getUserByEmail?(email: string): Awaitable + /** + * Returns a user from the database via the user's phone number address. + * + * See also [Verification tokens](https://authjs.dev/guides/creating-a-database-adapter#verification-tokens) + */ + getUserByPhoneNumber?(phone_number: string): Awaitable + /** + * Returns a user from the database via the user's anonymous id. + * + * See also [Verification tokens](https://authjs.dev/guides/creating-a-database-adapter#verification-tokens) + */ + getUserByAnonymousId?(anonymous_id: string): Awaitable /** * Using the provider id and the id of the user for a specific account, get the user. * diff --git a/packages/core/src/lib/actions/callback/handle-login.ts b/packages/core/src/lib/actions/callback/handle-login.ts index e053c3ef63..4a504a76ad 100644 --- a/packages/core/src/lib/actions/callback/handle-login.ts +++ b/packages/core/src/lib/actions/callback/handle-login.ts @@ -32,7 +32,11 @@ export async function handleLoginOrRegister( // Input validation if (!_account?.providerAccountId || !_account.type) throw new Error("Missing or invalid provider account") - if (!["email", "oauth", "oidc", "webauthn"].includes(_account.type)) + if ( + !["email", "sms", "anonymous", "oauth", "oidc", "webauthn"].includes( + _account.type + ) + ) throw new Error("Provider not supported") const { @@ -57,6 +61,8 @@ export async function handleLoginOrRegister( getUser, getUserByAccount, getUserByEmail, + getUserByPhoneNumber, + getUserByAnonymousId, linkAccount, createSession, getSessionAndUser, @@ -115,6 +121,83 @@ export async function handleLoginOrRegister( isNewUser = true } + // Create new session + session = useJwtSession + ? {} + : await createSession({ + sessionToken: generateSessionToken(), + userId: user.id, + expires: fromDate(options.session.maxAge), + }) + + return { session, user, isNewUser } + } else if (account.type === "sms") { + // If signing in with an email, check if an account with the same email address exists already + const userByPhoneNumber = await getUserByPhoneNumber(profile.phoneNumber!) + if (userByPhoneNumber) { + // If they are not already signed in as the same user, this flow will + // sign them out of the current session and sign them in as the new user + if (user?.id !== userByPhoneNumber.id && !useJwtSession && sessionToken) { + // Delete existing session if they are currently signed in as another user. + // This will switch user accounts for the session in cases where the user was + // already logged in with a different account. + await deleteSession(sessionToken) + } + + // Update emailVerified property on the user object + user = await updateUser({ + id: userByPhoneNumber.id, + phoneNumberVerified: new Date(), + }) + await events.updateUser?.({ user }) + } else { + // Create user account if there isn't one for the email address already + user = await createUser({ ...profile, phoneNumberVerified: new Date() }) + await events.createUser?.({ user }) + isNewUser = true + } + + // Create new session + session = useJwtSession + ? {} + : await createSession({ + sessionToken: generateSessionToken(), + userId: user.id, + expires: fromDate(options.session.maxAge), + }) + + return { session, user, isNewUser } + } else if (account.type === "anonymous") { + // If signing in as anonymous, check if an account already exists + const userByAnonymousId = await getUserByAnonymousId(profile.anonymousId!) + if (userByAnonymousId) { + // If they are not already signed in as the same user, this flow will + // sign them out of the current session and sign them in as the new user + if (user?.id !== userByAnonymousId.id && !useJwtSession && sessionToken) { + // Delete existing session if they are currently signed in as another user. + // This will switch user accounts for the session in cases where the user was + // already logged in with a different account. + await deleteSession(sessionToken) + } + + // Update emailVerified property on the user object + user = await updateUser({ + id: userByAnonymousId.id, + anonymousId: profile.anonymousId, + anonymousIdVerified: new Date(), + }) + await events.updateUser?.({ user }) + } else { + // Create user account if there isn't one for the anonymousId already + user = await createUser({ + ...profile, + anonymousId: profile.anonymousId, + anonymousIdVerified: new Date(), + }) + await events.createUser?.({ user }) + isNewUser = true + } + // Create new session session = useJwtSession ? {} diff --git a/packages/core/src/lib/actions/callback/index.ts b/packages/core/src/lib/actions/callback/index.ts index d11048f7cb..a74ff30395 100644 --- a/packages/core/src/lib/actions/callback/index.ts +++ b/packages/core/src/lib/actions/callback/index.ts @@ -1,8 +1,8 @@ // TODO: Make this file smaller import { - AuthError, AccessDenied, + AuthError, CallbackRouteError, CredentialsSignin, InvalidProvider, @@ -11,9 +11,9 @@ import { import { handleLoginOrRegister } from "./handle-login.js" import { handleOAuth } from "./oauth/callback.js" import { state } from "./oauth/checks.js" -import { createHash } from "../../utils/web.js" +import { createHash, randomString, toRequest } from "../../utils/web.js" -import type { AdapterSession } from "../../../adapters.js" +import type { AdapterSession, VerificationToken } from "../../../adapters.js" import type { Account, Authenticator, @@ -203,8 +203,9 @@ export async function callback( return { redirect: callbackUrl, cookies } } else if (provider.type === "email") { - const paramToken = query?.token as string | undefined - const paramIdentifier = query?.email as string | undefined + const paramToken = body?.token || (query?.token as string | undefined) + const paramIdentifier = + body?.email || (query?.email as string | undefined) if (!paramToken) { const e = new TypeError( @@ -218,7 +219,6 @@ export async function callback( const secret = provider.secret ?? options.secret // @ts-expect-error -- Verified in `assertConfig`. const invite = await adapter.useVerificationToken({ - // @ts-expect-error User-land adapters might decide to omit the identifier during lookup identifier: paramIdentifier, // TODO: Drop this requirement for lookup in official adapters too token: await createHash(`${paramToken}${secret}`), }) @@ -320,6 +320,242 @@ export async function callback( } } + // Callback URL is already verified at this point, so safe to use if specified + return { redirect: callbackUrl, cookies } + } else if (provider.type === "sms") { + const paramToken = body?.token || (query?.token as string | undefined) + const paramIdentifier = + body?.phone_number || (query?.phone_number as string | undefined) + + if (!paramIdentifier) { + const e = new TypeError( + "Missing phone_number. The sign-in URL was manually opened without phone_number or the link was not sent correctly in the sms.", + { cause: { hasToken: !!paramToken } } + ) + e.name = "Configuration" + throw e + } + + if (!paramToken) { + const e = new TypeError( + "Missing token. The sign-in URL was manually opened without token or the link was not sent correctly in the sms.", + { cause: { hasToken: !!paramToken } } + ) + e.name = "Configuration" + throw e + } + + const secret = provider.secret ?? options.secret + let invite: VerificationToken | null + if (provider?.checkVerificationRequest) { + invite = await provider.checkVerificationRequest({ + identifier: paramIdentifier, + url: `${url}/callback/${provider.id}`, + expires: new Date(Date.now() + (provider.maxAge ?? 300) * 1000), + provider, + secret, + token: paramToken, + theme: options.theme, + request: toRequest(request), + adapter, + }) + } else { + // @ts-expect-error -- Verified in `assertConfig`. + invite = await adapter.useVerificationToken({ + identifier: paramIdentifier, // TODO: Drop this requirement for lookup in official adapters too + token: await createHash(`${paramToken}${secret}`), + }) + } + if (!invite) { + throw new Verification({ hasInvite: false, expired: false }) + } + + const hasInvite = !!invite + const expired = hasInvite && invite.expires.valueOf() < Date.now() + const invalidInvite = + !hasInvite || + expired || + // The user might have configured the link to not contain the identifier + // so we only compare if it exists + (paramIdentifier && invite.identifier !== paramIdentifier) + if (invalidInvite) throw new Verification({ hasInvite, expired }) + + const { identifier } = invite + const user = (await adapter!.getUserByPhoneNumber(identifier)) ?? { + id: crypto.randomUUID(), + phoneNumber: identifier, + phoneNumberVerified: null, + } + + const account: Account = { + providerAccountId: user.phoneNumber!, + userId: user.id, + type: "sms" as const, + provider: provider.id, + } + + const redirect = await handleAuthorized({ user, account }, options) + if (redirect) return { redirect, cookies } + + // Sign user in + const { + user: loggedInUser, + session, + isNewUser, + } = await handleLoginOrRegister( + sessionStore.value, + user, + account, + options + ) + + if (useJwtSession) { + const defaultToken = { + name: loggedInUser.name, + phoneNumber: loggedInUser.phoneNumber, + picture: loggedInUser.image, + sub: loggedInUser.id?.toString(), + } + const token = await callbacks.jwt({ + token: defaultToken, + user: loggedInUser, + account, + isNewUser, + trigger: isNewUser ? "signUp" : "signIn", + }) + + // Clear cookies if token is null + if (token === null) { + cookies.push(...sessionStore.clean()) + } else { + const salt = options.cookies.sessionToken.name + // Encode token + const newToken = await jwt.encode({ ...jwt, token, salt }) + + // Set cookie expiry date + const cookieExpires = new Date() + cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000) + + const sessionCookies = sessionStore.chunk(newToken, { + expires: cookieExpires, + }) + cookies.push(...sessionCookies) + } + } else { + // Save Session Token in cookie + cookies.push({ + name: options.cookies.sessionToken.name, + value: (session as AdapterSession).sessionToken, + options: { + ...options.cookies.sessionToken.options, + expires: (session as AdapterSession).expires, + }, + }) + } + + await events.signIn?.({ user: loggedInUser, account, isNewUser }) + + // Handle first logins on new accounts + // e.g. option to send users to a new account landing page on initial login + // Note that the callback URL is preserved, so the journey can still be resumed + if (isNewUser && pages.newUser) { + return { + redirect: `${pages.newUser}${ + pages.newUser.includes("?") ? "&" : "?" + }${new URLSearchParams({ callbackUrl })}`, + cookies, + } + } + + // Callback URL is already verified at this point, so safe to use if specified + return { redirect: callbackUrl, cookies } + } else if (provider.type === "anonymous") { + const anonymousId = randomString(64) + const user = (await adapter!.getUserByPhoneNumber(anonymousId)) ?? { + id: crypto.randomUUID(), + anonymousId, + anonymousIdVerified: null, + } + + const account: Account = { + providerAccountId: user.id!, + userId: user.id, + type: "anonymous" as const, + provider: provider.id, + } + const redirect = await handleAuthorized({ user, account }, options) + if (redirect) return { redirect, cookies } + + // Sign user in + const { + user: loggedInUser, + session, + isNewUser, + } = await handleLoginOrRegister( + sessionStore.value, + user, + account, + options + ) + + if (useJwtSession) { + const defaultToken = { + name: loggedInUser.name, + phoneNumber: loggedInUser.phoneNumber, + picture: loggedInUser.image, + sub: loggedInUser.id?.toString(), + } + const token = await callbacks.jwt({ + token: defaultToken, + user: loggedInUser, + account, + isNewUser, + trigger: isNewUser ? "signUp" : "signIn", + }) + + // Clear cookies if token is null + if (token === null) { + cookies.push(...sessionStore.clean()) + } else { + const salt = options.cookies.sessionToken.name + // Encode token + const newToken = await jwt.encode({ ...jwt, token, salt }) + + // Set cookie expiry date + const cookieExpires = new Date() + cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000) + + const sessionCookies = sessionStore.chunk(newToken, { + expires: cookieExpires, + }) + cookies.push(...sessionCookies) + } + } else { + // Save Session Token in cookie + cookies.push({ + name: options.cookies.sessionToken.name, + value: (session as AdapterSession).sessionToken, + options: { + ...options.cookies.sessionToken.options, + expires: (session as AdapterSession).expires, + }, + }) + } + + await events.signIn?.({ user: loggedInUser, account, isNewUser }) + + // Handle first logins on new accounts + // e.g. option to send users to a new account landing page on initial login + // Note that the callback URL is preserved, so the journey can still be resumed + if (isNewUser && pages.newUser) { + return { + redirect: `${pages.newUser}${ + pages.newUser.includes("?") ? "&" : "?" + }${new URLSearchParams({ callbackUrl })}`, + cookies, + } + } + // Callback URL is already verified at this point, so safe to use if specified return { redirect: callbackUrl, cookies } } else if (provider.type === "credentials" && method === "POST") { @@ -332,7 +568,7 @@ export async function callback( const userFromAuthorize = await provider.authorize( credentials, // prettier-ignore - new Request(url, { headers, method, body: JSON.stringify(body) }) + new Request(url, {headers, method, body: JSON.stringify(body)}) ) const user = userFromAuthorize diff --git a/packages/core/src/lib/actions/signin/index.ts b/packages/core/src/lib/actions/signin/index.ts index e47c934ea0..52422cbd59 100644 --- a/packages/core/src/lib/actions/signin/index.ts +++ b/packages/core/src/lib/actions/signin/index.ts @@ -7,6 +7,7 @@ import type { RequestInternal, ResponseInternal, } from "../../../types.js" +import { sendSmsToken } from "./send-sms-token.js" export async function signIn( request: RequestInternal, @@ -31,6 +32,11 @@ export async function signIn( const response = await sendToken(request, options) return { ...response, cookies } } + case "sms": { + const response = await sendSmsToken(request, options) + return { ...response, cookies } + } + case "anonymous": default: return { redirect: signInUrl, cookies } } diff --git a/packages/core/src/lib/actions/signin/send-sms-token.ts b/packages/core/src/lib/actions/signin/send-sms-token.ts new file mode 100644 index 0000000000..5ae1dc6cea --- /dev/null +++ b/packages/core/src/lib/actions/signin/send-sms-token.ts @@ -0,0 +1,120 @@ +import { createHash, randomNumber, toRequest } from "../../utils/web.js" +import { AccessDenied } from "../../../errors.js" + +import type { + Account, + InternalOptions, + RequestInternal, +} from "../../../types.js" +import { AdapterUser } from "../../../adapters.js" + +/** + * Starts an e-mail login flow, by generating a token, + * and sending it to the user's e-mail (with the help of a DB adapter). + * At the end, it returns a redirect to the `verify-request` page. + */ +export async function sendSmsToken( + request: RequestInternal, + options: InternalOptions<"sms"> +) { + const { body } = request + const { provider, callbacks, adapter } = options + const normalizer = provider.normalizeIdentifier ?? defaultNormalizer + const phone_number = normalizer(body?.phone_number) + + const captchaVerified = + (await provider?.verifyCaptchaToken?.(body?.captcha_token)) ?? true + if (!captchaVerified) { + throw new AccessDenied("Captcha verification failed.") + } + + const defaultUser = { id: crypto.randomUUID(), phone_number } + const user = + (await adapter!.getUserByPhoneNumber(phone_number)) ?? defaultUser + + const account = { + providerAccountId: phone_number, + userId: user.id, + type: "sms", + provider: provider.id, + } satisfies Account + + let authorized + try { + authorized = await callbacks.signIn({ + user, + account, + email: { verificationRequest: true }, + }) + } catch (e) { + throw new AccessDenied(e as Error) + } + if (!authorized) throw new AccessDenied("AccessDenied") + if (typeof authorized === "string") { + return { + redirect: await callbacks.redirect({ + url: authorized, + baseUrl: options.url.origin, + }), + } + } + + const { callbackUrl, theme } = options + const token = + (await provider.generateVerificationToken?.({ + identifier: phone_number, + user: user as AdapterUser, + provider, + theme, + request: toRequest(request), + })) ?? randomNumber(6) + + const QUARTER_HOUR_IN_SECONDS = 3600 + const expires = new Date( + Date.now() + (provider.maxAge ?? QUARTER_HOUR_IN_SECONDS) * 1000 + ) + + const secret = provider.secret ?? options.secret + + const baseUrl = new URL(options.basePath, options.url.origin) + + const sendRequest = provider.sendVerificationRequest({ + identifier: phone_number, + token, + expires, + url: `${baseUrl}/callback/${provider.id}?${new URLSearchParams({ + callbackUrl, + token, + phone_number, + })}`, + provider, + theme, + request: toRequest(request), + }) + + const createToken = adapter!.createVerificationToken?.({ + identifier: phone_number, + token: await createHash(`${token}${secret}`), + expires, + }) + + await Promise.all([sendRequest, createToken]) + + return { + redirect: `${baseUrl}/verify-request?${new URLSearchParams({ + provider: provider.id, + type: provider.type, + phone_number: phone_number, + })}`, + } +} + +function defaultNormalizer(phone_number?: string) { + if (!phone_number) throw new Error("Missing phone_number from request body.") + if (!/^\+\d{11}$/.test(phone_number)) { + throw new Error( + "Invalid phone number format, it should be like +XXXXXXXXXXXX" + ) + } + return phone_number +} diff --git a/packages/core/src/lib/actions/signin/send-token.ts b/packages/core/src/lib/actions/signin/send-token.ts index 27024affaf..8454dfe1af 100644 --- a/packages/core/src/lib/actions/signin/send-token.ts +++ b/packages/core/src/lib/actions/signin/send-token.ts @@ -1,8 +1,11 @@ import { createHash, randomString, toRequest } from "../../utils/web.js" import { AccessDenied } from "../../../errors.js" -import type { InternalOptions, RequestInternal } from "../../../types.js" -import type { Account } from "../../../types.js" +import type { + Account, + InternalOptions, + RequestInternal, +} from "../../../types.js" /** * Starts an e-mail login flow, by generating a token, @@ -18,6 +21,12 @@ export async function sendToken( const normalizer = provider.normalizeIdentifier ?? defaultNormalizer const email = normalizer(body?.email) + const captchaVerified = + (await provider?.verifyCaptchaToken?.(body?.captcha_token)) ?? true + if (!captchaVerified) { + throw new AccessDenied("Captcha verification failed.") + } + const defaultUser = { id: crypto.randomUUID(), email, emailVerified: null } const user = (await adapter!.getUserByEmail(email)) ?? defaultUser @@ -87,6 +96,7 @@ export async function sendToken( redirect: `${baseUrl}/verify-request?${new URLSearchParams({ provider: provider.id, type: provider.type, + email: email, })}`, } } diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 00ee0027c1..e1e9e70c9a 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -70,7 +70,10 @@ export async function AuthInternal( const { csrfTokenVerified } = options switch (action) { case "callback": - if (options.provider.type === "credentials") + if ( + options.provider.type === "credentials" || + options.provider.type === "anonymous" + ) // Verified CSRF Token required for credentials providers only validateCSRF(action, csrfTokenVerified) return await actions.callback(request, options, sessionStore, cookies) diff --git a/packages/core/src/lib/pages/index.ts b/packages/core/src/lib/pages/index.ts index b95c975b2e..83d790516f 100644 --- a/packages/core/src/lib/pages/index.ts +++ b/packages/core/src/lib/pages/index.ts @@ -121,7 +121,9 @@ export default function renderPage(params: RenderPageParams) { providers: params.providers?.filter( (provider) => // Always render oauth and email type providers - ["email", "oauth", "oidc"].includes(provider.type) || + ["email", "sms", "anonymous", "oauth", "oidc"].includes( + provider.type + ) || // Only render credentials type provider if credentials are defined (provider.type === "credentials" && provider.credentials) || // Only render webauthn type provider if formFields are defined diff --git a/packages/core/src/lib/pages/signin.tsx b/packages/core/src/lib/pages/signin.tsx index ca3fd27200..ffbf50d953 100644 --- a/packages/core/src/lib/pages/signin.tsx +++ b/packages/core/src/lib/pages/signin.tsx @@ -174,6 +174,43 @@ export default function SigninPage(props: { )} + {provider.type === "sms" && ( +
+ + + + +
+ )} + {provider.type === "anonymous" && ( +
+ + + +
+ )} {provider.type === "credentials" && (
diff --git a/packages/core/src/lib/utils/assert.ts b/packages/core/src/lib/utils/assert.ts index 4f1a255044..9c144f97b6 100644 --- a/packages/core/src/lib/utils/assert.ts +++ b/packages/core/src/lib/utils/assert.ts @@ -46,6 +46,8 @@ function isSemverString(version: string): version is SemverString { let hasCredentials = false let hasEmail = false +let hasSMS = false +let hasAnonymous = false let hasWebAuthn = false const emailMethods: (keyof Adapter)[] = [ @@ -54,6 +56,14 @@ const emailMethods: (keyof Adapter)[] = [ "getUserByEmail", ] +const smsMethods: (keyof Adapter)[] = [ + "createVerificationToken", + "useVerificationToken", + "getUserByPhoneNumber", +] + +const anonymousMethods: (keyof Adapter)[] = ["getUserByAnonymousId"] + const sessionMethods: (keyof Adapter)[] = [ "createUser", "getUser", @@ -148,6 +158,8 @@ export function assertConfig( if (provider.type === "credentials") hasCredentials = true else if (provider.type === "email") hasEmail = true + else if (provider.type === "sms") hasSMS = true + else if (provider.type === "anonymous") hasAnonymous = true else if (provider.type === "webauthn") { hasWebAuthn = true @@ -211,6 +223,8 @@ export function assertConfig( const requiredMethods: (keyof Adapter)[] = [] if ( + hasSMS || + hasAnonymous || hasEmail || session?.strategy === "database" || (!session?.strategy && adapter) @@ -218,6 +232,13 @@ export function assertConfig( if (hasEmail) { if (!adapter) return new MissingAdapter("Email login requires an adapter") requiredMethods.push(...emailMethods) + } else if (hasSMS) { + if (!adapter) return new MissingAdapter("SMS login requires an adapter") + requiredMethods.push(...smsMethods) + } else if (hasSMS) { + if (!adapter) + return new MissingAdapter("Anonymous login requires an adapter") + requiredMethods.push(...anonymousMethods) } else { if (!adapter) return new MissingAdapter("Database session requires an adapter") diff --git a/packages/core/src/lib/utils/web.ts b/packages/core/src/lib/utils/web.ts index fdbafbcadc..f790cb20cd 100644 --- a/packages/core/src/lib/utils/web.ts +++ b/packages/core/src/lib/utils/web.ts @@ -114,6 +114,24 @@ export function randomString(size: number) { return Array.from(bytes).reduce(r, "") } +/** Web compatible method to create a random number of a given length */ +export function randomNumber(size: number) { + if (size <= 0) { + throw new Error("Size must be a positive integer.") + } + + // Generate an array of random bytes + const bytes = crypto.getRandomValues(new Uint8Array(size)) + + // Convert the bytes to a single number + let result = 0 + for (let i = 0; i < size; i++) { + result = (result << 8) + bytes[i] + } + + return result.toString() +} + /** @internal Parse the action and provider id from a URL pathname. */ export function parseActionAndProviderId( pathname: string, diff --git a/packages/core/src/providers/anonymous.ts b/packages/core/src/providers/anonymous.ts new file mode 100644 index 0000000000..b81f7aeaa9 --- /dev/null +++ b/packages/core/src/providers/anonymous.ts @@ -0,0 +1,27 @@ +import { CommonProviderOptions } from "./index.js" + +export interface AnonymousConfig extends CommonProviderOptions { + id: string + type: "anonymous" + maxAge?: number + /** Used to hash the verification token. */ + secret?: string + options?: AnonymousUserConfig +} + +export type AnonymousUserConfig = Omit< + Partial, + "options" | "type" +> + +export default function AnonymousProvider( + config: AnonymousUserConfig +): AnonymousConfig { + return { + id: "anonymous", + type: "anonymous", + name: "Anonymous", + maxAge: 60 * 24 * 60 * 60, + options: config, + } +} diff --git a/packages/core/src/providers/email.ts b/packages/core/src/providers/email.ts index 35bd306d77..7223b496d6 100644 --- a/packages/core/src/providers/email.ts +++ b/packages/core/src/providers/email.ts @@ -1,12 +1,12 @@ import type { CommonProviderOptions } from "./index.js" import type { Awaitable, Theme } from "../types.js" -export type { EmailProviderId } from "./provider-types.js" - +import type { NodemailerConfig, NodemailerUserConfig } from "./nodemailer.js" // TODO: Kepts for backwards compatibility // Remove this import and encourage users // to import it from @auth/core/providers/nodemailer directly import Nodemailer from "./nodemailer.js" -import type { NodemailerConfig, NodemailerUserConfig } from "./nodemailer.js" + +export type { EmailProviderId } from "./provider-types.js" /** * @deprecated @@ -53,6 +53,7 @@ export interface EmailConfig extends CommonProviderOptions { /** Used with SMTP-based email providers. */ server?: NodemailerConfig["server"] generateVerificationToken?: () => Awaitable + verifyCaptchaToken?: (captcha_token: string) => Awaitable normalizeIdentifier?: (identifier: string) => string options?: EmailUserConfig } diff --git a/packages/core/src/providers/index.ts b/packages/core/src/providers/index.ts index 9edfe85872..7a77588137 100644 --- a/packages/core/src/providers/index.ts +++ b/packages/core/src/providers/index.ts @@ -10,6 +10,8 @@ import type { OIDCConfig, } from "./oauth.js" import type { WebAuthnConfig, WebAuthnProviderType } from "./webauthn.js" +import { SMSConfig } from "./sms.js" +import { AnonymousConfig } from "./anonymous.js" export * from "./credentials.js" export * from "./email.js" @@ -28,6 +30,8 @@ export type ProviderType = | "oauth" | "email" | "credentials" + | "sms" + | "anonymous" | WebAuthnProviderType /** Shared across all {@link ProviderType} */ @@ -70,6 +74,8 @@ export type Provider

= ( | OIDCConfig

| OAuth2Config

| EmailConfig + | SMSConfig + | AnonymousConfig | CredentialsConfig | WebAuthnConfig ) & @@ -80,6 +86,8 @@ export type Provider

= ( | OAuth2Config

| OIDCConfig

| EmailConfig + | SMSConfig + | AnonymousConfig | CredentialsConfig | WebAuthnConfig ) & diff --git a/packages/core/src/providers/sms.ts b/packages/core/src/providers/sms.ts new file mode 100644 index 0000000000..4434adbc25 --- /dev/null +++ b/packages/core/src/providers/sms.ts @@ -0,0 +1,72 @@ +import { CommonProviderOptions } from "./index.js" +import { Awaitable, Theme } from "../types.js" +import { Adapter, AdapterUser, VerificationToken } from "../adapters.js" + +export type SmsProviderSendVerificationRequestParams = { + identifier: string + url: string + expires: Date + provider: SMSConfig + token: string + theme: Theme + request: Request +} + +export type SmsProviderGenerateVerificationTokenParams = { + identifier: string + user: AdapterUser | null + provider: SMSConfig + theme: Theme + request: Request +} + +export type SmsProviderCheckVerificationTokenParams = { + identifier: string + url: string + expires: Date + provider: SMSConfig + secret: string | string[] + token: string + theme: Theme + request: Request + adapter?: Required +} + +export interface SMSConfig extends CommonProviderOptions { + id: string + type: "sms" + maxAge?: number + sendVerificationRequest: ( + params: SmsProviderSendVerificationRequestParams + ) => Awaitable + /** Used to hash the verification token. */ + secret?: string + generateVerificationToken?: ( + params: SmsProviderGenerateVerificationTokenParams + ) => Awaitable + checkVerificationRequest?: ( + params: SmsProviderCheckVerificationTokenParams + ) => Awaitable + normalizeIdentifier?: (identifier: string) => string + verifyCaptchaToken?: (captcha_token: string) => Awaitable + options?: SMSUserConfig +} + +export type SMSUserConfig = Omit, "options" | "type"> + +export default function SMSProvider(config: SMSUserConfig): SMSConfig { + return { + id: "sms", + type: "sms", + name: "SMS", + maxAge: 5 * 60, + async generateVerificationToken() { + const random = crypto.getRandomValues(new Uint8Array(6)) + return Array.from(random, (byte) => byte % 10).join("") + }, + async sendVerificationRequest(params) { + throw new Error(`sendVerificationRequest not implemented: ${params}`) + }, + options: config, + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b6653ffe04..221eb081ae 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -70,6 +70,8 @@ import type { WebAuthnConfig, WebAuthnProviderType, } from "./providers/webauthn.js" +import { SMSConfig } from "./providers/sms.js" +import { AnonymousConfig } from "./providers/anonymous.js" export type { WebAuthnOptionsResponseBody } from "./lib/utils/webauthn-utils.js" export type { AuthConfig } from "./index.js" @@ -253,6 +255,7 @@ export interface DefaultUser { id?: string name?: string | null email?: string | null + phoneNumber?: string | null image?: string | null } @@ -272,11 +275,15 @@ export type InternalProvider = (T extends "oauth" ? OIDCConfigInternal : T extends "email" ? EmailConfig - : T extends "credentials" - ? CredentialsConfig - : T extends WebAuthnProviderType - ? WebAuthnConfig - : never) & { + : T extends "sms" + ? SMSConfig + : T extends "anonymous" + ? AnonymousConfig + : T extends "credentials" + ? CredentialsConfig + : T extends WebAuthnProviderType + ? WebAuthnConfig + : never) & { signinUrl: string /** @example `"https://example.com/api/auth/callback/id"` */ callbackUrl: string diff --git a/packages/next-auth/package.json b/packages/next-auth/package.json index 072e04ce3b..2a258e1a10 100644 --- a/packages/next-auth/package.json +++ b/packages/next-auth/package.json @@ -1,9 +1,12 @@ { "name": "next-auth", - "version": "5.0.0-beta.28", + "version": "5.0.0-beta.29", "description": "Authentication for Next.js", "homepage": "https://nextjs.authjs.dev", - "repository": "https://github.com/nextauthjs/next-auth.git", + "repository": { + "type": "git", + "url": "git+https://github.com/nextauthjs/next-auth.git" + }, "author": "Balázs Orbán ", "contributors": [ "Iain Collins ", diff --git a/packages/next-auth/src/index.ts b/packages/next-auth/src/index.ts index ebe08fcd42..fca65aca84 100644 --- a/packages/next-auth/src/index.ts +++ b/packages/next-auth/src/index.ts @@ -386,6 +386,7 @@ export default function NextAuth( signIn: async (provider, options, authorizationParams) => { const _config = await config(undefined) setEnvDefaults(_config) + console.log("BLABLA3:", { provider, options, authorizationParams }) return signIn(provider, options, authorizationParams, _config) }, signOut: async (options) => { diff --git a/packages/next-auth/src/lib/actions.ts b/packages/next-auth/src/lib/actions.ts index 80081fa2e0..bae008f26b 100644 --- a/packages/next-auth/src/lib/actions.ts +++ b/packages/next-auth/src/lib/actions.ts @@ -62,7 +62,11 @@ export async function signIn( return url } - if (foundProvider.type === "credentials") { + if ( + foundProvider.type === "credentials" || + (foundProvider.type === "sms" && rest.token) || + foundProvider.type === "anonymous" + ) { url = url.replace("signin", "callback") } diff --git a/packages/next-auth/src/react.tsx b/packages/next-auth/src/react.tsx index 4110e2bf81..37ffc56f47 100644 --- a/packages/next-auth/src/react.tsx +++ b/packages/next-auth/src/react.tsx @@ -13,17 +13,6 @@ "use client" import * as React from "react" -import { - apiBaseUrl, - ClientSessionError, - fetchData, - now, - parseUrl, - useOnline, -} from "./lib/client.js" - -import type { ProviderId } from "@auth/core/providers" -import type { LoggerInstance, Session } from "@auth/core/types" import type { AuthClientConfig, ClientSafeProvider, @@ -35,6 +24,17 @@ import type { SignOutResponse, UseSessionOptions, } from "./lib/client.js" +import { + apiBaseUrl, + ClientSessionError, + fetchData, + now, + parseUrl, + useOnline, +} from "./lib/client.js" + +import type { ProviderId } from "@auth/core/providers" +import type { LoggerInstance, Session } from "@auth/core/types" // TODO: Remove/move to core? export type { @@ -277,7 +277,11 @@ export async function signIn( } const signInUrl = `${baseUrl}/${ - providerType === "credentials" ? "callback" : "signin" + providerType === "credentials" || + (providerType === "sms" && signInParams.token) || + (providerType === "email" && signInParams.token) + ? "callback" + : "signin" }/${provider}` const csrfToken = await getCsrfToken() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8dc49879f..f0ca38be89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,10 +246,10 @@ importers: version: 0.4.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docsearch/react': specifier: '3' - version: 3.8.3(@algolia/client-search@5.20.0)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2) + version: 3.9.0(@algolia/client-search@5.20.3)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2) '@inkeep/widgets': specifier: ^0.2.289 - version: 0.2.289(@internationalized/date@3.5.6)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 0.2.289(@internationalized/date@3.5.2)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@next/third-parties': specifier: ^14.2.15 version: 14.2.15(next@14.2.21(@opentelemetry/api@1.7.0)(@playwright/test@1.41.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.70.0))(react@18.3.1) @@ -935,8 +935,8 @@ packages: '@algolia/cache-in-memory@4.24.0': resolution: {integrity: sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==} - '@algolia/client-abtesting@5.20.0': - resolution: {integrity: sha512-YaEoNc1Xf2Yk6oCfXXkZ4+dIPLulCx8Ivqj0OsdkHWnsI3aOJChY5qsfyHhDBNSOhqn2ilgHWxSfyZrjxBcAww==} + '@algolia/client-abtesting@5.20.3': + resolution: {integrity: sha512-wPOzHYSsW+H97JkBLmnlOdJSpbb9mIiuNPycUCV5DgzSkJFaI/OFxXfZXAh1gqxK+hf0miKue1C9bltjWljrNA==} engines: {node: '>= 14.0.0'} '@algolia/client-account@4.24.0': @@ -945,41 +945,41 @@ packages: '@algolia/client-analytics@4.24.0': resolution: {integrity: sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==} - '@algolia/client-analytics@5.20.0': - resolution: {integrity: sha512-CIT9ni0+5sYwqehw+t5cesjho3ugKQjPVy/iPiJvtJX4g8Cdb6je6SPt2uX72cf2ISiXCAX9U3cY0nN0efnRDw==} + '@algolia/client-analytics@5.20.3': + resolution: {integrity: sha512-XE3iduH9lA7iTQacDGofBQyIyIgaX8qbTRRdj1bOCmfzc9b98CoiMwhNwdTifmmMewmN0EhVF3hP8KjKWwX7Yw==} engines: {node: '>= 14.0.0'} '@algolia/client-common@4.24.0': resolution: {integrity: sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==} - '@algolia/client-common@5.20.0': - resolution: {integrity: sha512-iSTFT3IU8KNpbAHcBUJw2HUrPnMXeXLyGajmCL7gIzWOsYM4GabZDHXOFx93WGiXMti1dymz8k8R+bfHv1YZmA==} + '@algolia/client-common@5.20.3': + resolution: {integrity: sha512-IYRd/A/R3BXeaQVT2805lZEdWo54v39Lqa7ABOxIYnUvX2vvOMW1AyzCuT0U7Q+uPdD4UW48zksUKRixShcWxA==} engines: {node: '>= 14.0.0'} - '@algolia/client-insights@5.20.0': - resolution: {integrity: sha512-w9RIojD45z1csvW1vZmAko82fqE/Dm+Ovsy2ElTsjFDB0HMAiLh2FO86hMHbEXDPz6GhHKgGNmBRiRP8dDPgJg==} + '@algolia/client-insights@5.20.3': + resolution: {integrity: sha512-QGc/bmDUBgzB71rDL6kihI2e1Mx6G6PxYO5Ks84iL3tDcIel1aFuxtRF14P8saGgdIe1B6I6QkpkeIddZ6vWQw==} engines: {node: '>= 14.0.0'} '@algolia/client-personalization@4.24.0': resolution: {integrity: sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==} - '@algolia/client-personalization@5.20.0': - resolution: {integrity: sha512-p/hftHhrbiHaEcxubYOzqVV4gUqYWLpTwK+nl2xN3eTrSW9SNuFlAvUBFqPXSVBqc6J5XL9dNKn3y8OA1KElSQ==} + '@algolia/client-personalization@5.20.3': + resolution: {integrity: sha512-zuM31VNPDJ1LBIwKbYGz/7+CSm+M8EhlljDamTg8AnDilnCpKjBebWZR5Tftv/FdWSro4tnYGOIz1AURQgZ+tQ==} engines: {node: '>= 14.0.0'} - '@algolia/client-query-suggestions@5.20.0': - resolution: {integrity: sha512-m4aAuis5vZi7P4gTfiEs6YPrk/9hNTESj3gEmGFgfJw3hO2ubdS4jSId1URd6dGdt0ax2QuapXufcrN58hPUcw==} + '@algolia/client-query-suggestions@5.20.3': + resolution: {integrity: sha512-Nn872PuOI8qzi1bxMMhJ0t2AzVBqN01jbymBQOkypvZHrrjZPso3iTpuuLLo9gi3yc/08vaaWTAwJfPhxPwJUw==} engines: {node: '>= 14.0.0'} '@algolia/client-search@4.24.0': resolution: {integrity: sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==} - '@algolia/client-search@5.20.0': - resolution: {integrity: sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ==} + '@algolia/client-search@5.20.3': + resolution: {integrity: sha512-9+Fm1ahV8/2goSIPIqZnVitV5yHW5E5xTdKy33xnqGd45A9yVv5tTkudWzEXsbfBB47j9Xb3uYPZjAvV5RHbKA==} engines: {node: '>= 14.0.0'} - '@algolia/ingestion@1.20.0': - resolution: {integrity: sha512-shj2lTdzl9un4XJblrgqg54DoK6JeKFO8K8qInMu4XhE2JuB8De6PUuXAQwiRigZupbI0xq8aM0LKdc9+qiLQA==} + '@algolia/ingestion@1.20.3': + resolution: {integrity: sha512-5GHNTiZ3saLjTNyr6WkP5hzDg2eFFAYWomvPcm9eHWskjzXt8R0IOiW9kkTS6I6hXBwN5H9Zna5mZDSqqJdg+g==} engines: {node: '>= 14.0.0'} '@algolia/logger-common@4.24.0': @@ -988,36 +988,36 @@ packages: '@algolia/logger-console@4.24.0': resolution: {integrity: sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==} - '@algolia/monitoring@1.20.0': - resolution: {integrity: sha512-aF9blPwOhKtWvkjyyXh9P5peqmhCA1XxLBRgItT+K6pbT0q4hBDQrCid+pQZJYy4HFUKjB/NDDwyzFhj/rwKhw==} + '@algolia/monitoring@1.20.3': + resolution: {integrity: sha512-KUWQbTPoRjP37ivXSQ1+lWMfaifCCMzTnEcEnXwAmherS5Tp7us6BAqQDMGOD4E7xyaS2I8pto6WlOzxH+CxmA==} engines: {node: '>= 14.0.0'} '@algolia/recommend@4.24.0': resolution: {integrity: sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==} - '@algolia/recommend@5.20.0': - resolution: {integrity: sha512-T6B/WPdZR3b89/F9Vvk6QCbt/wrLAtrGoL8z4qPXDFApQ8MuTFWbleN/4rHn6APWO3ps+BUePIEbue2rY5MlRw==} + '@algolia/recommend@5.20.3': + resolution: {integrity: sha512-oo/gG77xTTTclkrdFem0Kmx5+iSRFiwuRRdxZETDjwzCI7svutdbwBgV/Vy4D4QpYaX4nhY/P43k84uEowCE4Q==} engines: {node: '>= 14.0.0'} '@algolia/requester-browser-xhr@4.24.0': resolution: {integrity: sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==} - '@algolia/requester-browser-xhr@5.20.0': - resolution: {integrity: sha512-t6//lXsq8E85JMenHrI6mhViipUT5riNhEfCcvtRsTV+KIBpC6Od18eK864dmBhoc5MubM0f+sGpKOqJIlBSCg==} + '@algolia/requester-browser-xhr@5.20.3': + resolution: {integrity: sha512-BkkW7otbiI/Er1AiEPZs1h7lxbtSO9p09jFhv3/iT8/0Yz0CY79VJ9iq+Wv1+dq/l0OxnMpBy8mozrieGA3mXQ==} engines: {node: '>= 14.0.0'} '@algolia/requester-common@4.24.0': resolution: {integrity: sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==} - '@algolia/requester-fetch@5.20.0': - resolution: {integrity: sha512-FHxYGqRY+6bgjKsK4aUsTAg6xMs2S21elPe4Y50GB0Y041ihvw41Vlwy2QS6K9ldoftX4JvXodbKTcmuQxywdQ==} + '@algolia/requester-fetch@5.20.3': + resolution: {integrity: sha512-eAVlXz7UNzTsA1EDr+p0nlIH7WFxo7k3NMxYe8p38DH8YVWLgm2MgOVFUMNg9HCi6ZNOi/A2w/id2ZZ4sKgUOw==} engines: {node: '>= 14.0.0'} '@algolia/requester-node-http@4.24.0': resolution: {integrity: sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==} - '@algolia/requester-node-http@5.20.0': - resolution: {integrity: sha512-kmtQClq/w3vtPteDSPvaW9SPZL/xrIgMrxZyAgsFwrJk0vJxqyC5/hwHmrCraDnStnGSADnLpBf4SpZnwnkwWw==} + '@algolia/requester-node-http@5.20.3': + resolution: {integrity: sha512-FqR3pQPfHfQyX1wgcdK6iyqu86yP76MZd4Pzj1y/YLMj9rRmRCY0E0AffKr//nrOFEwv6uY8BQY4fd9/6b0ZCg==} engines: {node: '>= 14.0.0'} '@algolia/transporter@4.24.0': @@ -2060,15 +2060,15 @@ packages: peerDependencies: postcss-selector-parser: ^6.0.13 - '@docsearch/css@3.8.3': - resolution: {integrity: sha512-1nELpMV40JDLJ6rpVVFX48R1jsBFIQ6RnEQDsLFGmzOjPWTOMlZqUcXcvRx8VmYV/TqnS1l784Ofz+ZEb+wEOQ==} + '@docsearch/css@3.9.0': + resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} - '@docsearch/react@3.8.3': - resolution: {integrity: sha512-6UNrg88K7lJWmuS6zFPL/xgL+n326qXqZ7Ybyy4E8P/6Rcblk3GE8RXxeol4Pd5pFpKMhOhBhzABKKwHtbJCIg==} + '@docsearch/react@3.9.0': + resolution: {integrity: sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==} peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' search-insights: '>= 1 < 3' peerDependenciesMeta: '@types/react': @@ -4189,6 +4189,7 @@ packages: '@playwright/test@1.41.2': resolution: {integrity: sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==} engines: {node: '>=16'} + deprecated: Please update to the latest version of Playwright to test up-to-date browsers. hasBin: true '@polka/url@1.0.0-next.24': @@ -6267,8 +6268,8 @@ packages: algoliasearch@4.24.0: resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} - algoliasearch@5.20.0: - resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==} + algoliasearch@5.20.3: + resolution: {integrity: sha512-iNC6BGvipaalFfDfDnXUje8GUlW5asj0cTMsZJwO/0rhsyLx1L7GZFAY8wW+eQ6AM4Yge2p5GSE5hrBlfSD90Q==} engines: {node: '>= 14.0.0'} ansi-align@3.0.1: @@ -13941,33 +13942,33 @@ snapshots: tunnel: 0.0.6 undici: 5.28.2 - '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.2)': + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3)(search-insights@2.17.2)': dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.2) - '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) + '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3)(search-insights@2.17.2) + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3) transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - search-insights - '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.2)': + '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3)(search-insights@2.17.2)': dependencies: - '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3) search-insights: 2.17.2 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)': + '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3)': dependencies: - '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) - '@algolia/client-search': 5.20.0 - algoliasearch: 5.20.0 + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3) + '@algolia/client-search': 5.20.3 + algoliasearch: 5.20.3 - '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)': + '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3)': dependencies: - '@algolia/client-search': 5.20.0 - algoliasearch: 5.20.0 + '@algolia/client-search': 5.20.3 + algoliasearch: 5.20.3 '@algolia/cache-browser-local-storage@4.24.0': dependencies: @@ -13979,12 +13980,12 @@ snapshots: dependencies: '@algolia/cache-common': 4.24.0 - '@algolia/client-abtesting@5.20.0': + '@algolia/client-abtesting@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/client-account@4.24.0': dependencies: @@ -13999,26 +14000,26 @@ snapshots: '@algolia/requester-common': 4.24.0 '@algolia/transporter': 4.24.0 - '@algolia/client-analytics@5.20.0': + '@algolia/client-analytics@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/client-common@4.24.0': dependencies: '@algolia/requester-common': 4.24.0 '@algolia/transporter': 4.24.0 - '@algolia/client-common@5.20.0': {} + '@algolia/client-common@5.20.3': {} - '@algolia/client-insights@5.20.0': + '@algolia/client-insights@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/client-personalization@4.24.0': dependencies: @@ -14026,19 +14027,19 @@ snapshots: '@algolia/requester-common': 4.24.0 '@algolia/transporter': 4.24.0 - '@algolia/client-personalization@5.20.0': + '@algolia/client-personalization@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 - '@algolia/client-query-suggestions@5.20.0': + '@algolia/client-query-suggestions@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/client-search@4.24.0': dependencies: @@ -14046,19 +14047,19 @@ snapshots: '@algolia/requester-common': 4.24.0 '@algolia/transporter': 4.24.0 - '@algolia/client-search@5.20.0': + '@algolia/client-search@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 - '@algolia/ingestion@1.20.0': + '@algolia/ingestion@1.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/logger-common@4.24.0': {} @@ -14066,12 +14067,12 @@ snapshots: dependencies: '@algolia/logger-common': 4.24.0 - '@algolia/monitoring@1.20.0': + '@algolia/monitoring@1.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/recommend@4.24.0': dependencies: @@ -14087,34 +14088,34 @@ snapshots: '@algolia/requester-node-http': 4.24.0 '@algolia/transporter': 4.24.0 - '@algolia/recommend@5.20.0': + '@algolia/recommend@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + '@algolia/client-common': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 '@algolia/requester-browser-xhr@4.24.0': dependencies: '@algolia/requester-common': 4.24.0 - '@algolia/requester-browser-xhr@5.20.0': + '@algolia/requester-browser-xhr@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 + '@algolia/client-common': 5.20.3 '@algolia/requester-common@4.24.0': {} - '@algolia/requester-fetch@5.20.0': + '@algolia/requester-fetch@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 + '@algolia/client-common': 5.20.3 '@algolia/requester-node-http@4.24.0': dependencies: '@algolia/requester-common': 4.24.0 - '@algolia/requester-node-http@5.20.0': + '@algolia/requester-node-http@5.20.3': dependencies: - '@algolia/client-common': 5.20.0 + '@algolia/client-common': 5.20.3 '@algolia/transporter@4.24.0': dependencies: @@ -14206,7 +14207,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@ark-ui/anatomy@0.1.0(@internationalized/date@3.5.6)': + '@ark-ui/anatomy@0.1.0(@internationalized/date@3.5.2)': dependencies: '@zag-js/accordion': 0.20.0 '@zag-js/anatomy': 0.20.0 @@ -14217,7 +14218,7 @@ snapshots: '@zag-js/color-utils': 0.20.0 '@zag-js/combobox': 0.20.0 '@zag-js/date-picker': 0.20.0 - '@zag-js/date-utils': 0.20.0(@internationalized/date@3.5.6) + '@zag-js/date-utils': 0.20.0(@internationalized/date@3.5.2) '@zag-js/dialog': 0.20.0 '@zag-js/editable': 0.20.0 '@zag-js/hover-card': 0.20.0 @@ -14243,7 +14244,7 @@ snapshots: transitivePeerDependencies: - '@internationalized/date' - '@ark-ui/react@0.15.0(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ark-ui/react@0.15.0(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@zag-js/accordion': 0.19.1 '@zag-js/anatomy': 0.19.1 @@ -14255,7 +14256,7 @@ snapshots: '@zag-js/combobox': 0.19.1 '@zag-js/core': 0.19.1 '@zag-js/date-picker': 0.19.1 - '@zag-js/date-utils': 0.19.1(@internationalized/date@3.5.6) + '@zag-js/date-utils': 0.19.1(@internationalized/date@3.5.2) '@zag-js/dialog': 0.19.1 '@zag-js/editable': 0.19.1 '@zag-js/hover-card': 0.19.1 @@ -15891,14 +15892,14 @@ snapshots: dependencies: postcss-selector-parser: 6.1.2 - '@docsearch/css@3.8.3': {} + '@docsearch/css@3.9.0': {} - '@docsearch/react@3.8.3(@algolia/client-search@5.20.0)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)': + '@docsearch/react@3.9.0(@algolia/client-search@5.20.3)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)': dependencies: - '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.2) - '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) - '@docsearch/css': 3.8.3 - algoliasearch: 5.20.0 + '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3)(search-insights@2.17.2) + '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.20.3)(algoliasearch@5.20.3) + '@docsearch/css': 3.9.0 + algoliasearch: 5.20.3 optionalDependencies: '@types/react': 18.2.78 react: 18.3.1 @@ -17134,11 +17135,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@inkeep/components@0.0.24(@ark-ui/react@0.15.0(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + '@inkeep/components@0.0.24(@ark-ui/react@0.15.0(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': dependencies: - '@ark-ui/react': 0.15.0(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@inkeep/preset': 0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3) - '@inkeep/preset-chakra': 0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3) + '@ark-ui/react': 0.15.0(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@inkeep/preset': 0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3) + '@inkeep/preset-chakra': 0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3) '@inkeep/shared': 0.0.25 '@inkeep/styled-system': 0.0.44 '@pandacss/dev': 0.22.1(typescript@5.6.3) @@ -17150,9 +17151,9 @@ snapshots: - jsdom - typescript - '@inkeep/preset-chakra@0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3)': + '@inkeep/preset-chakra@0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3)': dependencies: - '@ark-ui/anatomy': 0.1.0(@internationalized/date@3.5.6) + '@ark-ui/anatomy': 0.1.0(@internationalized/date@3.5.2) '@inkeep/shared': 0.0.25 '@pandacss/dev': 0.22.1(typescript@5.6.3) transitivePeerDependencies: @@ -17160,10 +17161,10 @@ snapshots: - jsdom - typescript - '@inkeep/preset@0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3)': + '@inkeep/preset@0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3)': dependencies: - '@ark-ui/anatomy': 0.1.0(@internationalized/date@3.5.6) - '@inkeep/preset-chakra': 0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3) + '@ark-ui/anatomy': 0.1.0(@internationalized/date@3.5.2) + '@inkeep/preset-chakra': 0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3) '@inkeep/shared': 0.0.25 '@pandacss/dev': 0.22.1(typescript@5.6.3) colorjs.io: 0.4.5 @@ -17178,14 +17179,14 @@ snapshots: '@inkeep/styled-system@0.0.46': {} - '@inkeep/widgets@0.2.289(@internationalized/date@3.5.6)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + '@inkeep/widgets@0.2.289(@internationalized/date@3.5.2)(@types/react@18.2.78)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': dependencies: '@apollo/client': 3.9.5(@types/react@18.2.78)(graphql-ws@5.14.3(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@ark-ui/react': 0.15.0(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ark-ui/react': 0.15.0(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@inkeep/color-mode': 0.0.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@inkeep/components': 0.0.24(@ark-ui/react@0.15.0(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@internationalized/date@3.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) - '@inkeep/preset': 0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3) - '@inkeep/preset-chakra': 0.0.24(@internationalized/date@3.5.6)(typescript@5.6.3) + '@inkeep/components': 0.0.24(@ark-ui/react@0.15.0(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@internationalized/date@3.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@inkeep/preset': 0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3) + '@inkeep/preset-chakra': 0.0.24(@internationalized/date@3.5.2)(typescript@5.6.3) '@inkeep/shared': 0.0.25 '@inkeep/styled-system': 0.0.46 '@types/lodash.isequal': 4.5.8 @@ -20532,9 +20533,9 @@ snapshots: dependencies: '@internationalized/date': 3.5.2 - '@zag-js/date-utils@0.19.1(@internationalized/date@3.5.6)': + '@zag-js/date-utils@0.20.0(@internationalized/date@3.5.2)': dependencies: - '@internationalized/date': 3.5.6 + '@internationalized/date': 3.5.2 '@zag-js/date-utils@0.20.0(@internationalized/date@3.5.6)': dependencies: @@ -21281,21 +21282,21 @@ snapshots: '@algolia/requester-node-http': 4.24.0 '@algolia/transporter': 4.24.0 - algoliasearch@5.20.0: - dependencies: - '@algolia/client-abtesting': 5.20.0 - '@algolia/client-analytics': 5.20.0 - '@algolia/client-common': 5.20.0 - '@algolia/client-insights': 5.20.0 - '@algolia/client-personalization': 5.20.0 - '@algolia/client-query-suggestions': 5.20.0 - '@algolia/client-search': 5.20.0 - '@algolia/ingestion': 1.20.0 - '@algolia/monitoring': 1.20.0 - '@algolia/recommend': 5.20.0 - '@algolia/requester-browser-xhr': 5.20.0 - '@algolia/requester-fetch': 5.20.0 - '@algolia/requester-node-http': 5.20.0 + algoliasearch@5.20.3: + dependencies: + '@algolia/client-abtesting': 5.20.3 + '@algolia/client-analytics': 5.20.3 + '@algolia/client-common': 5.20.3 + '@algolia/client-insights': 5.20.3 + '@algolia/client-personalization': 5.20.3 + '@algolia/client-query-suggestions': 5.20.3 + '@algolia/client-search': 5.20.3 + '@algolia/ingestion': 1.20.3 + '@algolia/monitoring': 1.20.3 + '@algolia/recommend': 5.20.3 + '@algolia/requester-browser-xhr': 5.20.3 + '@algolia/requester-fetch': 5.20.3 + '@algolia/requester-node-http': 5.20.3 ansi-align@3.0.1: dependencies: @@ -30131,7 +30132,7 @@ snapshots: error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 open: 9.1.0 - picocolors: 1.0.0 + picocolors: 1.1.0 sirv: 2.0.4 vite: 5.3.1(@types/node@18.11.10)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: