diff --git a/AGENTS.md b/AGENTS.md index 9efaa591c..232173a69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ - Use **Batching** with `bulkWrite` (e.g., batches of 500) to maximize performance and minimize network roundtrips. - Ensure **Idempotency** (safe to re-run) by using upserts or `$setOnInsert` where applicable. - When making changes to the structure of the Course, consider how it affects its representation on its public page (`apps/web/app/(with-contexts)/(with-layout)/p/[id]/page.tsx`) and the course viewer (`apps/web/app/(with-contexts)/course/[slug]/[id]/page.tsx`). +- `apps/web` is a multi-tenant app. ### Workspace map (core modules): diff --git a/apps/docs/public/assets/schools/google-login-button.png b/apps/docs/public/assets/schools/google-login-button.png new file mode 100644 index 000000000..ec900a86b Binary files /dev/null and b/apps/docs/public/assets/schools/google-login-button.png differ diff --git a/apps/docs/public/assets/schools/google-login-checkbox.png b/apps/docs/public/assets/schools/google-login-checkbox.png new file mode 100644 index 000000000..1cfce3f8a Binary files /dev/null and b/apps/docs/public/assets/schools/google-login-checkbox.png differ diff --git a/apps/docs/public/assets/schools/google-project-create-client.png b/apps/docs/public/assets/schools/google-project-create-client.png new file mode 100644 index 000000000..d1a88de77 Binary files /dev/null and b/apps/docs/public/assets/schools/google-project-create-client.png differ diff --git a/apps/docs/public/assets/schools/idp/entra-id/sso-entra-idp-cert-and-metadata.png b/apps/docs/public/assets/schools/idp/entra-id/sso-entra-idp-cert-and-metadata.png new file mode 100644 index 000000000..06cfaf3fa Binary files /dev/null and b/apps/docs/public/assets/schools/idp/entra-id/sso-entra-idp-cert-and-metadata.png differ diff --git a/apps/docs/public/assets/schools/login-providers-area.png b/apps/docs/public/assets/schools/login-providers-area.png index 4f2564b41..5c7b0ec9a 100644 Binary files a/apps/docs/public/assets/schools/login-providers-area.png and b/apps/docs/public/assets/schools/login-providers-area.png differ diff --git a/apps/docs/src/config.ts b/apps/docs/src/config.ts index 978d4b23e..e03391336 100644 --- a/apps/docs/src/config.ts +++ b/apps/docs/src/config.ts @@ -123,6 +123,7 @@ export const SIDEBAR: Sidebar = { { text: "Use custom domain", link: "en/schools/add-custom-domain" }, { text: "Set up payments", link: "en/schools/set-up-payments" }, { text: "Single Sign-On", link: "en/schools/sso" }, + { text: "Sign in with Google", link: "en/schools/google-sign-in" }, { text: "Delete a school", link: "en/schools/delete" }, ], Users: [ diff --git a/apps/docs/src/pages/en/schools/google-sign-in.md b/apps/docs/src/pages/en/schools/google-sign-in.md new file mode 100644 index 000000000..59a73b127 --- /dev/null +++ b/apps/docs/src/pages/en/schools/google-sign-in.md @@ -0,0 +1,79 @@ +--- +title: Set up Sign in with Google +description: Learn how to let your customers sign in with their Google accounts +layout: ../../../layouts/MainLayout.astro +--- + +Using Google sign-in, you can let customers authenticate with their Google accounts on your login page and checkout page. + +## Steps to set up Google sign-in + +1. In the CourseLit dashboard, go to `Settings` -> `Miscellaneous` -> `Login providers`. + + ![Login providers area](/assets/schools/login-providers-area.png) + +2. Click on the Cog icon next to the Google provider to open its configuration screen. + +3. Keep the `School Settings` card open. You will use these values while creating the Google OAuth app: + + - **Authorized redirect URI**: Usually `https:///api/auth/sso/callback/google` + - **Authorized JavaScript origin**: Usually `https://` + +4. Open the Google Cloud Console and create or select the project you want to use. + +5. If prompted, configure the OAuth consent screen first: + + - Choose the appropriate user type for your use case. + - Add the app name, support email, and authorized domain details requested by Google. + - If your app is in testing mode, add the Google accounts you want to use as test users. + +6. In Google Auth Platform, go to `Clients`. Click on `Create client`. + + ![Google Auth app](/assets/schools/google-project-create-client.png) + +7. Select `Web application` as the application type. + +8. In the OAuth client configuration screen, use the values from CourseLit: + + - Add the `Authorized JavaScript origin` shown in CourseLit. + - Add the `Authorized redirect URI` shown in CourseLit. + +9. Click `Create`, then copy the generated `Client ID` and `Client secret`. + +10. Return to CourseLit and paste those values into the `Google App Configuration` card. + +11. Click `Save`. + +12. Go back to the `Login providers` screen and enable the Google provider. + +![CourseLit google login provider](/assets/schools/google-login-checkbox.png) + +## Customer's experience + +When Google login is configured and enabled, customers will see a `Continue with Google` button anywhere external login providers are shown, such as the login page and checkout page. + +![Google login button](/assets/schools/google-login-button.png) + +## Before you disable Google sign-in + +Disabling the Google provider does not delete the users who previously signed up with Google. Their CourseLit account, purchases, and progress remain intact. + +However, they will no longer be able to use `Continue with Google` to access that account. To keep signing in, they need another enabled login method that maps to the same email address, such as email login. + +## Troubleshooting + +### 1. I get a redirect URI mismatch error + +Make sure the `Authorized redirect URI` in Google Cloud Console exactly matches the value shown in CourseLit, including the protocol (`https://`) and the full path. + +### 2. I get an origin mismatch error + +Make sure the `Authorized JavaScript origin` in Google Cloud Console exactly matches the value shown in CourseLit. + +### 3. Only some Google accounts can sign in + +If your OAuth app is still in testing mode, Google only allows the accounts listed as test users to sign in. Add the required accounts as test users or publish the app when you are ready. + +## Stuck somewhere? + +We are always here for you. Come chat with us in our Discord channel or send a tweet at @CourseLit. diff --git a/apps/docs/src/pages/en/schools/sso.md b/apps/docs/src/pages/en/schools/sso.md index 1e75f3e47..fb189f5f5 100644 --- a/apps/docs/src/pages/en/schools/sso.md +++ b/apps/docs/src/pages/en/schools/sso.md @@ -76,7 +76,19 @@ To use this feature on [courseLit.app](https://courselit.app), you need to be on ![Okta IdP metadata](/assets/schools/idp/okta/saml-signing-certificates.png) 9. Enter the values obtained in the `IDP Configuration` panel. -10. The Okta IdP is now configured. +10. The Okta IdP is now configured. Test the integration. + +### Microsoft Entra ID + +1. Go to `Azure` portal, search for `Enterprise applications` and click on it to go to the apps. +2. Click `New application` to create a new app. +3. In the `Browse Microsoft Entra App Gallery` screen, click `Create your own application`. In the settings pane, enter the app's name and click `Create`. +4. After the application is created, select `Set up single sign on` and choose `SAML` as the single sign-on method. +5. In the SAML setup screen, click the pencil icon in the `Basic SAML Configuration` card and enter CourseLit's `Audience URI (SP Entity ID)` in `Identifier (Entity ID)` and the `SAML ACS URL` in `Reply URL (Assertion Consumer Service URL)`. +6. Next, obtain the IdP details from the `SAML Certificates` pane (on the same page). Click the pencil icon to open the settings pane, download the PEM certificate and the Federated certificate XML, and paste them into CourseLit's `Certificate` and `IDP Metadata` fields respectively. + ![Microsoft Entra ID's certificates](/assets/schools/idp/entra-id/sso-entra-idp-cert-and-metadata.png) +7. Obtain the `Login URL` from the same screen and enter it into CourseLit's `Entry point` field. +8. The Entra IdP is now configured — test the integration. ## Customer's experience diff --git a/apps/web/.env b/apps/web/.env index f276d4e1d..a93bfae81 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -27,4 +27,7 @@ # SEQUENCE_DELAY_BETWEEN_MAILS = 86400000 # 1 day in milliseconds # Cache directory -# CACHE_DIR=/tmp \ No newline at end of file +# CACHE_DIR=/tmp + +# If using standalone Mongo (no replica set) +# DB_TRANSACTION=false \ No newline at end of file diff --git a/apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx b/apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx index 9a90ddd37..7c218198c 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx @@ -9,7 +9,7 @@ import { FetchBuilder } from "@courselit/utils"; import { TOAST_TITLE_ERROR } from "@ui-config/strings"; import { useSearchParams } from "next/navigation"; import { useCallback, useContext, useEffect, useState } from "react"; -import type { SSOProvider } from "../login/page"; +import type { RuntimeLoginProvider } from "@/lib/login-providers"; const { MembershipEntityType } = Constants; @@ -23,7 +23,9 @@ export default function ProductCheckout() { const [product, setProduct] = useState(null); const [paymentPlans, setPaymentPlans] = useState([]); const [includedProducts, setIncludedProducts] = useState([]); - const [ssoProvider, setSSOProvider] = useState(); + const [loginProviders, setLoginProviders] = useState< + RuntimeLoginProvider[] + >([]); const getIncludedProducts = useCallback(async () => { const query = ` @@ -97,9 +99,12 @@ export default function ProductCheckout() { } defaultPaymentPlan } - ssoProvider: getSSOProvider { + loginProviders: getExternalLoginProviders { + key providerId - domain + label + buttonText + authType } } `; @@ -126,9 +131,7 @@ export default function ProductCheckout() { description: "Course not found", }); } - if (response.ssoProvider) { - setSSOProvider(response.ssoProvider); - } + setLoginProviders(response.loginProviders || []); } catch (err: any) { toast({ title: TOAST_TITLE_ERROR, @@ -164,9 +167,12 @@ export default function ProductCheckout() { joiningReasonText defaultPaymentPlan } - ssoProvider: getSSOProvider { + loginProviders: getExternalLoginProviders { + key providerId - domain + label + buttonText + authType } } `; @@ -194,9 +200,7 @@ export default function ProductCheckout() { description: "Community not found", }); } - if (response.ssoProvider) { - setSSOProvider(response.ssoProvider); - } + setLoginProviders(response.loginProviders || []); } catch (err: any) { toast({ title: TOAST_TITLE_ERROR, @@ -231,7 +235,7 @@ export default function ProductCheckout() { product={product} paymentPlans={paymentPlans} includedProducts={includedProducts} - ssoProvider={ssoProvider} + loginProviders={loginProviders} type={entityType as MembershipEntityType | undefined} id={entityId as string | undefined} /> diff --git a/apps/web/app/(with-contexts)/(with-layout)/login/login-form.tsx b/apps/web/app/(with-contexts)/(with-layout)/login/login-form.tsx index 3d7268023..23e1ca455 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/login/login-form.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/login/login-form.tsx @@ -37,16 +37,15 @@ import { Constants, Profile } from "@courselit/common-models"; import { getUserProfile } from "../../helpers"; import { ADMIN_PERMISSIONS } from "@ui-config/constants"; import { authClient } from "@/lib/auth-client"; +import type { RuntimeLoginProvider } from "@/lib/login-providers"; +import ExternalLoginButton from "@/components/auth/external-login-button"; export default function LoginForm({ redirectTo, - ssoProvider, + loginProviders = [], }: { redirectTo?: string; - ssoProvider?: { - providerId: string; - domain: string; - }; + loginProviders?: RuntimeLoginProvider[]; }) { const { theme } = useContext(ThemeContext); const [showCode, setShowCode] = useState(false); @@ -312,23 +311,20 @@ export default function LoginForm({ )} )} - {siteinfo.logins?.includes( - Constants.LoginProvider.SSO, - ) && - ssoProvider && ( - - )} + {loginProviders.map((provider) => ( + { + await authClient.signIn.sso({ + providerId: provider.providerId, + callbackURL: "/dashboard", + }); + }} + /> + ))} {LOGIN_FORM_DISCLAIMER} diff --git a/apps/web/app/(with-contexts)/(with-layout)/login/page.tsx b/apps/web/app/(with-contexts)/(with-layout)/login/page.tsx index f67ed8d51..db7e79d14 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/login/page.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/login/page.tsx @@ -5,6 +5,7 @@ import { headers } from "next/headers"; import { getAddressFromHeaders } from "@/app/actions"; import { FetchBuilder } from "@courselit/utils"; import { error } from "@/services/logger"; +import type { RuntimeLoginProvider } from "@/lib/login-providers"; export default async function LoginPage({ searchParams, @@ -26,24 +27,22 @@ export default async function LoginPage({ return ( ); } -export type SSOProvider = { - providerId: string; - domain: string; -}; - -export const getSSOProvider = async ( +export const getExternalLoginProviders = async ( backend: string, -): Promise => { +): Promise => { const query = ` query { - ssoProvider: getSSOProvider { + loginProviders: getExternalLoginProviders { + key providerId - domain + label + buttonText + authType } } `; @@ -55,10 +54,11 @@ export const getSSOProvider = async ( try { const response = await fetch.exec(); - return response.ssoProvider; + return response.loginProviders || []; } catch (e: any) { - error(`Error in fetching SSO provider`, { + error(`Error in fetching login providers`, { stack: e.stack, }); + return []; } }; diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/mails/template/[id]/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/mails/template/[id]/page.tsx index bd5b2f2d4..6b46d0a60 100644 --- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/mails/template/[id]/page.tsx +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/mails/template/[id]/page.tsx @@ -78,7 +78,7 @@ export default function Page(props: { const currentValuesRef = useRef({ title: "" }); const form = useForm({ - resolver: zodResolver(formSchema), + resolver: zodResolver(formSchema as any), defaultValues: { title: "", }, diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/google/layout.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/google/layout.tsx new file mode 100644 index 000000000..032fec8c5 --- /dev/null +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/google/layout.tsx @@ -0,0 +1,16 @@ +import { GOOGLE_PROVIDER_HEADER } from "@ui-config/strings"; +import type { Metadata, ResolvingMetadata } from "next"; +import { ReactNode } from "react"; + +export async function generateMetadata( + _: any, + parent: ResolvingMetadata, +): Promise { + return { + title: `${GOOGLE_PROVIDER_HEADER} | ${(await parent)?.title?.absolute}`, + }; +} + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/google/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/google/page.tsx new file mode 100644 index 000000000..9f44ecf5e --- /dev/null +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/google/page.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useContext } from "react"; +import LoadingScreen from "@components/admin/loading-screen"; +import { AddressContext, ProfileContext } from "@components/contexts"; +import { UIConstants } from "@courselit/common-models"; +import DashboardContent from "@components/admin/dashboard-content"; +import { + GOOGLE_PROVIDER_HEADER, + SITE_MISCELLANEOUS_SETTING_HEADER, + SITE_SETTINGS_PAGE_HEADING, +} from "@ui-config/strings"; +import dynamic from "next/dynamic"; + +const { permissions } = UIConstants; +const GoogleProvider = dynamic( + () => import("@/components/admin/settings/google"), +); + +const breadcrumbs = [ + { + label: SITE_SETTINGS_PAGE_HEADING, + href: `/dashboard/settings?tab=${SITE_MISCELLANEOUS_SETTING_HEADER}`, + }, + { label: GOOGLE_PROVIDER_HEADER, href: "#" }, +]; + +export default function Page() { + const address = useContext(AddressContext); + const { profile } = useContext(ProfileContext); + + if (!profile) { + return ; + } + + return ( + + + + ); +} diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/sso/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/sso/page.tsx index a8d9f1d5f..fa4e86afd 100644 --- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/sso/page.tsx +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/login-provider/sso/page.tsx @@ -2,7 +2,6 @@ import { useContext } from "react"; import LoadingScreen from "@components/admin/loading-screen"; -import { checkPermission } from "@courselit/utils"; import { AddressContext, FeaturesContext, @@ -40,15 +39,15 @@ export default function Page() { ); } - if ( - !profile || - !checkPermission(profile.permissions!, [permissions.manageSettings]) - ) { + if (!profile) { return ; } return ( - + ); diff --git a/apps/web/app/(with-contexts)/dashboard/page.tsx b/apps/web/app/(with-contexts)/dashboard/page.tsx index accf2a30a..c792708c4 100644 --- a/apps/web/app/(with-contexts)/dashboard/page.tsx +++ b/apps/web/app/(with-contexts)/dashboard/page.tsx @@ -3,13 +3,12 @@ import { getProfile } from "../action"; import { Profile } from "@courselit/common-models"; import { checkPermission } from "@courselit/utils"; import { ADMIN_PERMISSIONS } from "@ui-config/constants"; -import { auth } from "@/auth"; export default async function Page() { const profile = (await getProfile()) as Profile; if (!profile) { - await auth.api.signOut(); + redirect("/login"); } if (checkPermission(profile?.permissions, ADMIN_PERMISSIONS)) { diff --git a/apps/web/app/api/auth/[...all]/route.ts b/apps/web/app/api/auth/[...all]/route.ts index 535458328..7ae3b79a6 100644 --- a/apps/web/app/api/auth/[...all]/route.ts +++ b/apps/web/app/api/auth/[...all]/route.ts @@ -1,23 +1,41 @@ import { als } from "@/async-local-storage"; -import { auth } from "@/auth"; +import { getBackendAddress } from "@/app/actions"; +import { getAuth } from "@/auth"; import { toNextJsHandler } from "better-auth/next-js"; -const handlers = toNextJsHandler(auth); +const getHandlers = (baseURL: string) => toNextJsHandler(getAuth(baseURL)); + +// This is needed to prevent creating URLs like https://0.0.0.0:3000/api/auth/sign-in/sso +export const rewriteAuthRequestOrigin = async (req: Request) => { + const publicOrigin = await getBackendAddress(req.headers); + const currentUrl = new URL(req.url); + + if (currentUrl.origin === publicOrigin) { + return req; + } + + const rewrittenUrl = new URL( + `${currentUrl.pathname}${currentUrl.search}`, + publicOrigin, + ); + + return new Request(rewrittenUrl, req); +}; export const POST = async (req: Request) => { + const rewrittenReq = await rewriteAuthRequestOrigin(req); + const handlers = getHandlers(new URL(rewrittenReq.url).origin); const map = new Map(); map.set("domain", req.headers.get("domain")); map.set("domainId", req.headers.get("domainId")); - als.enterWith(map); - - return handlers.POST(req); + return als.run(map, () => handlers.POST(rewrittenReq)); }; -export const GET = async (req: Request, ...rest: any[]) => { +export const GET = async (req: Request) => { + const rewrittenReq = await rewriteAuthRequestOrigin(req); + const handlers = getHandlers(new URL(rewrittenReq.url).origin); const map = new Map(); map.set("domain", req.headers.get("domain")); map.set("domainId", req.headers.get("domainId")); - als.enterWith(map); - - return handlers.GET(req); + return als.run(map, () => handlers.GET(rewrittenReq)); }; diff --git a/apps/web/app/api/auth/__tests__/route.test.ts b/apps/web/app/api/auth/__tests__/route.test.ts new file mode 100644 index 000000000..19c3dd47b --- /dev/null +++ b/apps/web/app/api/auth/__tests__/route.test.ts @@ -0,0 +1,148 @@ +/** + * @jest-environment node + */ + +const mockGetAuth = jest.fn(() => ({})); +const mockHandlerGet = jest.fn(); +const mockHandlerPost = jest.fn(); + +jest.mock("@/app/actions", () => ({ + getBackendAddress: jest.fn(), +})); + +jest.mock( + "@/async-local-storage", + () => ({ + als: { + run: jest.fn((_: unknown, fn: () => unknown) => fn()), + }, + }), + { virtual: true }, +); + +jest.mock("@/auth", () => ({ + auth: {}, + getAuth: mockGetAuth, +})); + +jest.mock("better-auth/next-js", () => ({ + toNextJsHandler: () => ({ + GET: mockHandlerGet, + POST: mockHandlerPost, + }), +})); + +import { getBackendAddress } from "@/app/actions"; +import { getAuth } from "@/auth"; +import { POST, rewriteAuthRequestOrigin } from "../[...all]/route"; + +describe("Auth Route Origin Rewrite", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("rewrites auth requests to the forwarded school origin", async () => { + (getBackendAddress as jest.Mock).mockResolvedValue( + "https://domain1.clqa.site", + ); + + const req = new Request( + "http://0.0.0.0:3000/api/auth/sso/callback/google?foo=bar", + { + headers: { + host: "0.0.0.0:3000", + "x-forwarded-host": "domain1.clqa.site", + "x-forwarded-proto": "https", + domain: "domain1", + domainId: "domain-id-1", + }, + }, + ); + + const rewritten = await rewriteAuthRequestOrigin(req); + + expect(rewritten).not.toBe(req); + expect(rewritten.url).toBe( + "https://domain1.clqa.site/api/auth/sso/callback/google?foo=bar", + ); + expect(rewritten.headers.get("domain")).toBe("domain1"); + expect(rewritten.headers.get("domainId")).toBe("domain-id-1"); + }); + + it("returns the original request when the origin already matches", async () => { + (getBackendAddress as jest.Mock).mockResolvedValue( + "https://domain1.clqa.site", + ); + + const req = new Request( + "https://domain1.clqa.site/api/auth/sso/callback/google", + ); + + const rewritten = await rewriteAuthRequestOrigin(req); + + expect(rewritten).toBe(req); + }); + + it("preserves method, body, and headers for post requests", async () => { + (getBackendAddress as jest.Mock).mockResolvedValue( + "https://domain1.clqa.site", + ); + + const req = new Request("http://0.0.0.0:3000/api/auth/sign-in/sso", { + method: "POST", + headers: { + "content-type": "application/json", + domain: "domain1", + domainId: "domain-id-1", + }, + body: JSON.stringify({ + providerId: "google", + callbackURL: "/checkout?id=123", + }), + }); + + const rewritten = await rewriteAuthRequestOrigin(req); + + expect(rewritten.method).toBe("POST"); + expect(rewritten.url).toBe( + "https://domain1.clqa.site/api/auth/sign-in/sso", + ); + expect(rewritten.headers.get("content-type")).toBe("application/json"); + expect(rewritten.headers.get("domainId")).toBe("domain-id-1"); + await expect(rewritten.text()).resolves.toBe( + JSON.stringify({ + providerId: "google", + callbackURL: "/checkout?id=123", + }), + ); + }); + + it("creates request-scoped auth handlers per rewritten origin", async () => { + (getBackendAddress as jest.Mock).mockResolvedValue( + "https://domain1.clqa.site", + ); + mockHandlerPost.mockResolvedValue(new Response(null, { status: 200 })); + + const req = new Request("https://0.0.0.0:3000/api/auth/sign-in/sso", { + method: "POST", + headers: { + host: "domain1.clqa.site", + domain: "domain1", + domainId: "domain-id-1", + }, + body: JSON.stringify({ + providerId: "google", + callbackURL: "/dashboard", + }), + }); + + await POST(req); + + expect(getAuth).toHaveBeenCalledWith("https://domain1.clqa.site"); + expect(mockHandlerPost).toHaveBeenCalledTimes(1); + const rewrittenRequest = mockHandlerPost.mock.calls[0][0] as Request; + expect(rewrittenRequest.url).toBe( + "https://domain1.clqa.site/api/auth/sign-in/sso", + ); + }); +}); diff --git a/apps/web/app/api/auth/sso/saml2/callback/[providerId]/route.ts b/apps/web/app/api/auth/sso/saml2/callback/[providerId]/route.ts deleted file mode 100644 index 6ad530677..000000000 --- a/apps/web/app/api/auth/sso/saml2/callback/[providerId]/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { als } from "@/async-local-storage"; -import { auth } from "@/auth"; -import { NextResponse } from "next/server"; -import { toNextJsHandler } from "better-auth/next-js"; - -const handlers = toNextJsHandler(auth); - -export async function POST(req: Request) { - const map = new Map(); - map.set("domain", req.headers.get("domain")); - map.set("domainId", req.headers.get("domainId")); - als.enterWith(map); - - return handlers.POST(req); -} - -// Required: IdP-initiated flows redirect to this URL after POST -export async function GET(req: Request) { - const map = new Map(); - map.set("domain", req.headers.get("domain")); - map.set("domainId", req.headers.get("domainId")); - als.enterWith(map); - - const url = new URL(req.url); - url.host = req.headers.get("x-forwarded-host") || ""; - url.protocol = req.headers.get("x-forwarded-proto") || ""; - url.port = - process.env.NODE_ENV === "production" - ? "" - : req.headers.get("x-forwarded-port") || ""; - - return NextResponse.redirect(new URL("/dashboard", url)); -} diff --git a/apps/web/auth.ts b/apps/web/auth.ts index 1fa480fcc..635dce28f 100644 --- a/apps/web/auth.ts +++ b/apps/web/auth.ts @@ -1,38 +1,182 @@ import { APIError, betterAuth } from "better-auth"; import { customSession, emailOTP } from "better-auth/plugins"; import { MongoClient } from "mongodb"; -import DomainModel, { Domain } from "@models/Domain"; -import { getEmailFrom } from "@courselit/utils"; +import { InternalUser } from "@courselit/orm-models"; +import { generateUniqueId, getEmailFrom } from "@courselit/utils"; import { addMailJob } from "@/services/queue"; import pug from "pug"; import MagicCodeEmailTemplate from "@/templates/magic-code-email"; import { responses } from "@/config/strings"; import { mongodbAdapter } from "@/ba-multitenant-adapter"; -import { updateUserAfterCreationViaAuth } from "./graphql/users/logic"; -import UserModel from "@models/User"; import { getBackendAddress } from "./app/actions"; import { sso } from "@better-auth/sso"; +import constants from "@/config/constants"; +import { finalizeUserCreation } from "./graphql/users/logic"; +import { sanitizeEmail } from "./lib/sanitize-email"; +import DomainModel, { Domain } from "@models/Domain"; +import UserModel from "@models/User"; +import { als } from "./async-local-storage"; const client = new MongoClient( process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017", ); const db = client.db(); -const config: any = { +const toDomainId = (value: unknown) => { + if (typeof value === "string" && value) { + return value; + } + + if ( + value && + typeof value === "object" && + "toString" in value && + typeof value.toString === "function" + ) { + const serialized = value.toString(); + return serialized ? serialized : undefined; + } + + return undefined; +}; + +const getAuthDomain = async ({ + user, + ctx, +}: { + user?: Record; + ctx?: { headers?: Headers }; +}): Promise => { + const domainId = + toDomainId(user?.domain) || + ctx?.headers?.get("domainId") || + als.getStore()?.get("domainId"); + const domainName = + ctx?.headers?.get("domain") || als.getStore()?.get("domain"); + + const domain = (domainId + ? await DomainModel.findById(domainId).lean() + : await DomainModel.findOne({ + name: domainName, + }).lean()) as unknown as Domain; + + if (!domain) { + throw new APIError("NOT_FOUND", { + message: "Domain not found", + }); + } + + return domain; +}; + +const getSessionUserId = async ( + user: Partial & { id?: string }, +) => { + if (user.userId) { + return user.userId; + } + + if (!user.id) { + return undefined; + } + + const authUser = await UserModel.findOne({ _id: user.id }) + .select("userId") + .lean(); + + return (authUser as { userId?: string } | null)?.userId; +}; + +const getSessionConfig = () => { + if (process.env.SESSION_COOKIE_CACHE_MAX_AGE) { + const configuredMaxAge = parseInt( + process.env.SESSION_COOKIE_CACHE_MAX_AGE, + ); + + if (configuredMaxAge > 0) { + return { + cookieCache: { + enabled: true, + maxAge: configuredMaxAge * 60, + }, + }; + } + } + + return { + cookieCache: { + enabled: true, + maxAge: 5 * 60, // 5 minutes + }, + }; +}; + +const createAuthConfig = (baseURL = ""): any => ({ appName: "CourseLit", secret: process.env.AUTH_SECRET, + ...(baseURL ? { baseURL } : {}), + user: { + additionalFields: { + userId: { + type: "string", + required: false, + }, + active: { + type: "boolean", + required: false, + }, + purchases: { + type: "json", + required: false, + }, + permissions: { + type: "string[]", + required: false, + }, + lead: { + type: "string", + required: false, + }, + subscribedToUpdates: { + type: "boolean", + required: false, + }, + tags: { + type: "string[]", + required: false, + }, + unsubscribeToken: { + type: "string", + required: false, + }, + }, + }, account: { + storeStateStrategy: "cookie", accountLinking: { enabled: true, - trustedProviders: ["sso", "email-otp"], + trustedProviders: ["sso", "google"], }, }, advanced: { cookiePrefix: "courselit", + cookies: { + relay_state: { + attributes: { + sameSite: "none", + secure: true, + }, + }, + }, }, database: mongodbAdapter(db, { client, usePlural: true, + // Enable transactions by default; set DB_TRANSACTIONS=false to opt out. + transaction: + process.env.DB_TRANSACTIONS === undefined + ? true + : process.env.DB_TRANSACTIONS.toLowerCase() !== "false", }), plugins: [ emailOTP({ @@ -66,11 +210,9 @@ const config: any = { return { user: { ...user, - userId: ( - (await UserModel.findOne({ _id: user.id }) - .select("userId") - .lean()) as unknown as any - )?.userId, + userId: await getSessionUserId( + user as Partial & { id?: string }, + ), }, session: { ...session, @@ -93,47 +235,66 @@ const config: any = { databaseHooks: { user: { create: { + before: async (user) => { + return { + data: { + email: sanitizeEmail(user.email), + active: true, + userId: generateUniqueId(), + purchases: [], + permissions: [ + constants.permissions.enrollInCourse, + constants.permissions.manageMedia, + ], + lead: constants.leadWebsite, + subscribedToUpdates: true, + tags: [], + unsubscribeToken: generateUniqueId(), + }, + }; + }, after: async (user, ctx) => { - const domainName = ctx!.headers?.get("domain"); - const domain = (await DomainModel.findOne({ - name: domainName, - }).lean()) as unknown as Domain; - if (!domain) { - throw new APIError("NOT_FOUND", { - message: "Domain not found", - }); - } - - await updateUserAfterCreationViaAuth(user.id, domain); + const domain = await getAuthDomain({ + user: user as Record, + ctx, + }); + await finalizeUserCreation( + user as InternalUser, + domain._id.toString(), + ); }, }, }, }, - trustedOrigins: async (request: Request) => { - const origins: string[] = [await getBackendAddress(request.headers)]; + trustedOrigins: async (request?: Request) => { + // Better Auth may invoke this during initialization/auth.api calls without a request. + if (!request) { + return []; + } + + const origins: string[] = [ + await getBackendAddress(request.headers), + "https://accounts.google.com", + "https://oauth2.googleapis.com", + "https://openidconnect.googleapis.com", + "https://www.googleapis.com", + ]; if (request.headers.get("ssotrusteddomain")) { origins.push(request.headers.get("ssotrusteddomain")!); } return origins; }, -}; + session: getSessionConfig(), +}); -if (process.env.SESSION_COOKIE_CACHE_MAX_AGE) { - if (parseInt(process.env.SESSION_COOKIE_CACHE_MAX_AGE) > 0) { - config.session = { - cookieCache: { - enabled: true, - maxAge: parseInt(process.env.SESSION_COOKIE_CACHE_MAX_AGE) * 60, - }, - }; +const authInstances = new Map>(); + +export const getAuth = (baseURL = "") => { + if (!authInstances.has(baseURL)) { + authInstances.set(baseURL, betterAuth(createAuthConfig(baseURL))); } -} else { - config.session = { - cookieCache: { - enabled: true, - maxAge: 5 * 60, // 5 minutes - }, - }; -} -export const auth = betterAuth(config); + return authInstances.get(baseURL)!; +}; + +export const auth = getAuth(); diff --git a/apps/web/ba-multitenant-adapter.ts b/apps/web/ba-multitenant-adapter.ts index 815578b49..281d27d41 100644 --- a/apps/web/ba-multitenant-adapter.ts +++ b/apps/web/ba-multitenant-adapter.ts @@ -313,9 +313,14 @@ export const mongodbAdapter = ( return { async create({ model, data: values }) { - (values as any).domain = new ObjectId( - als.getStore()?.get("domainId") ?? "", - ); + const domainId = als.getStore()?.get("domainId"); + if (!domainId) { + throw new Error( + `Missing tenant context while creating ${model}`, + ); + } + + (values as any).domain = new ObjectId(domainId); const res = await db .collection(model) .insertOne(values, { session }); @@ -714,6 +719,20 @@ export const mongodbAdapter = ( const oid = new ObjectId(); return oid; } + + if ( + (fieldAttributes.type === "json" || + fieldAttributes.type === "string[]" || + fieldAttributes.type === "number[]") && + typeof data === "string" + ) { + try { + return JSON.parse(data); + } catch { + return data; + } + } + return data; }, customTransformOutput({ data, field, fieldAttributes }) { @@ -734,6 +753,20 @@ export const mongodbAdapter = ( } return data; } + + if ( + (fieldAttributes.type === "json" || + fieldAttributes.type === "string[]" || + fieldAttributes.type === "number[]") && + typeof data === "string" + ) { + try { + return JSON.parse(data); + } catch { + return data; + } + } + return data; }, customIdGenerator() { diff --git a/apps/web/components/admin/settings/google.tsx b/apps/web/components/admin/settings/google.tsx new file mode 100644 index 000000000..84b1563ce --- /dev/null +++ b/apps/web/components/admin/settings/google.tsx @@ -0,0 +1,462 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useForm, FormProvider } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { type Address } from "@courselit/common-models"; +import { useToast } from "@courselit/components-library"; +import { FetchBuilder } from "@courselit/utils"; +import { + BTN_RESET, + BUTTON_SAVE, + GOOGLE_PROVIDER_CARD_DESCRIPTION, + GOOGLE_PROVIDER_CARD_HEADER, + GOOGLE_PROVIDER_CLIENT_ID_LABEL, + GOOGLE_PROVIDER_CLIENT_SECRET_LABEL, + GOOGLE_PROVIDER_HEADER, + GOOGLE_PROVIDER_ORIGIN_LABEL, + GOOGLE_PROVIDER_REDIRECT_URI_LABEL, + GOOGLE_PROVIDER_REMOVE_DIALOG_HEADER, + GOOGLE_PROVIDER_SECRET_HELPER, + GOOGLE_PROVIDER_SECRET_SAVED, + GOOGLE_PROVIDER_SETTINGS_DESCRIPTION, + GOOGLE_PROVIDER_SETTINGS_HEADER, + GOOGLE_PROVIDER_SUCCESS_MESSAGE, + PROVIDER_RESET_SUCCESS_MESSAGE, + SITE_MISCELLANEOUS_SETTING_HEADER, + TOAST_TITLE_ERROR, + TOAST_TITLE_SUCCESS, + URL_COPIED_TO_CLIPBOARD, +} from "@ui-config/strings"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@components/ui/button"; +import Resources from "@components/resources"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@components/ui/alert-dialog"; +import { Copy, Loader2, Save, Trash2 } from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@components/ui/card"; +import { Label } from "@components/ui/label"; + +const formSchema = z.object({ + clientId: z.string().trim().min(1, "Client ID is required"), + clientSecret: z.string().optional(), +}); + +type FormData = z.infer; + +interface GoogleProviderProps { + address: Address; +} + +export default function GoogleProvider({ address }: GoogleProviderProps) { + const [loading, setLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [canSubmit, setCanSubmit] = useState(false); + const [hasSavedSecret, setHasSavedSecret] = useState(false); + const [isGoogleProviderSet, setIsGoogleProviderSet] = useState(false); + const { toast } = useToast(); + const valuesRef = useRef({ + clientId: "", + clientSecret: "", + }); + + const form = useForm({ + resolver: zodResolver(formSchema as any), + defaultValues: { + clientId: "", + clientSecret: "", + }, + }); + + const updateCanSubmit = useCallback(() => { + const clientId = valuesRef.current.clientId.trim(); + const clientSecret = valuesRef.current.clientSecret?.trim() || ""; + + setCanSubmit( + !loading && !!clientId && (!!clientSecret || hasSavedSecret), + ); + }, [hasSavedSecret, loading]); + + useEffect(() => { + const subscription = form.watch((values) => { + valuesRef.current = { + clientId: values.clientId || "", + clientSecret: values.clientSecret || "", + }; + updateCanSubmit(); + }); + + return () => subscription.unsubscribe(); + }, [form, updateCanSubmit]); + + useEffect(() => { + updateCanSubmit(); + }, [updateCanSubmit]); + + useEffect(() => { + const fetchGoogleProvider = async () => { + const query = ` + query { + googleProvider: getGoogleProviderSettings { + clientId + hasClientSecret + } + } + `; + const fetcher = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ query }) + .setIsGraphQLEndpoint(true) + .build(); + + try { + const response = await fetcher.exec(); + const { googleProvider } = response; + + if (googleProvider) { + form.reset({ + clientId: googleProvider.clientId, + clientSecret: "", + }); + valuesRef.current = { + clientId: googleProvider.clientId, + clientSecret: "", + }; + setHasSavedSecret(googleProvider.hasClientSecret); + setIsGoogleProviderSet(true); + } + } catch (err: any) { + toast({ + title: TOAST_TITLE_ERROR, + description: err.message, + variant: "destructive", + }); + } + }; + + fetchGoogleProvider(); + }, [address.backend, form, toast]); + + const updateGoogleProvider = async (values: FormData) => { + const query = ` + mutation ( + $clientId: String!, + $clientSecret: String, + $backend: String! + ) { + googleProvider: updateGoogleProvider( + clientId: $clientId, + clientSecret: $clientSecret, + backend: $backend, + ) { + providerId + } + } + `; + const nextClientSecret = values.clientSecret?.trim() || undefined; + + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query, + variables: { + clientId: values.clientId.trim(), + clientSecret: nextClientSecret, + backend: address.backend, + }, + }) + .setIsGraphQLEndpoint(true) + .build(); + + try { + setLoading(true); + const response = await fetch.exec(); + + if (response.googleProvider) { + toast({ + title: TOAST_TITLE_SUCCESS, + description: GOOGLE_PROVIDER_SUCCESS_MESSAGE, + }); + form.setValue("clientSecret", ""); + valuesRef.current = { + clientId: values.clientId.trim(), + clientSecret: "", + }; + setHasSavedSecret(true); + setIsGoogleProviderSet(true); + updateCanSubmit(); + } else { + toast({ + title: TOAST_TITLE_ERROR, + description: response.error, + variant: "destructive", + }); + } + } catch (err: any) { + toast({ + title: TOAST_TITLE_ERROR, + description: err.message, + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const resetProvider = async () => { + const query = ` + mutation { + removeGoogleProvider + } + `; + + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload(query) + .setIsGraphQLEndpoint(true) + .build(); + + try { + setIsDeleting(true); + const response = await fetch.exec(); + + if (response.removeGoogleProvider) { + toast({ + title: TOAST_TITLE_SUCCESS, + description: PROVIDER_RESET_SUCCESS_MESSAGE, + }); + window.location.href = `/dashboard/settings?tab=${SITE_MISCELLANEOUS_SETTING_HEADER}`; + } + } catch (err: any) { + toast({ + title: TOAST_TITLE_ERROR, + description: err.message, + variant: "destructive", + }); + } finally { + setIsDeleting(false); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast({ + title: TOAST_TITLE_SUCCESS, + description: URL_COPIED_TO_CLIPBOARD, + }); + }; + + const redirectUri = `${address.backend}/api/auth/sso/callback/google`; + + return ( +
+

+ {GOOGLE_PROVIDER_HEADER} +

+
+ + + {GOOGLE_PROVIDER_CARD_HEADER} + + {GOOGLE_PROVIDER_CARD_DESCRIPTION} + + + + +
+ ( + + + { + GOOGLE_PROVIDER_CLIENT_ID_LABEL + } + + + + + + + )} + /> + ( + + + { + GOOGLE_PROVIDER_CLIENT_SECRET_LABEL + } + + + + + + {hasSavedSecret + ? `${GOOGLE_PROVIDER_SECRET_SAVED} ${GOOGLE_PROVIDER_SECRET_HELPER}` + : GOOGLE_PROVIDER_SECRET_HELPER} + + + + )} + /> +
+ +
+ +
+
+ + + + + {isGoogleProviderSet && ( + + !open && setIsDeleting(false) + } + > + + + + + + + { + GOOGLE_PROVIDER_REMOVE_DIALOG_HEADER + } + + + This action is irreversible. The + current Google app configuration + will be removed. + + + + + Cancel + + + {isDeleting ? ( + <> + + Resetting... + + ) : ( + "Reset" + )} + + + + + )} + +
+ + + {GOOGLE_PROVIDER_SETTINGS_HEADER} + + {GOOGLE_PROVIDER_SETTINGS_DESCRIPTION} + + + +
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ ); +} diff --git a/apps/web/components/admin/settings/sso.tsx b/apps/web/components/admin/settings/sso.tsx index 6006e1274..a98b33c4c 100644 --- a/apps/web/components/admin/settings/sso.tsx +++ b/apps/web/components/admin/settings/sso.tsx @@ -80,7 +80,7 @@ export default function SSOProvider({ address }: NewSSOProviderProps) { .setIsGraphQLEndpoint(true); const form = useForm({ - resolver: zodResolver(formSchema), + resolver: zodResolver(formSchema as any), defaultValues: { idpMetadata: "", entryPoint: "", diff --git a/apps/web/components/admin/settings/tabs/miscellaneous.tsx b/apps/web/components/admin/settings/tabs/miscellaneous.tsx index cdb5334f7..dd0cd6d29 100644 --- a/apps/web/components/admin/settings/tabs/miscellaneous.tsx +++ b/apps/web/components/admin/settings/tabs/miscellaneous.tsx @@ -7,7 +7,7 @@ import { CardFooter, } from "@components/ui/card"; import { Checkbox } from "@components/ui/checkbox"; -import { Constants, LoginProvider } from "@courselit/common-models"; +import { Constants, Features, LoginProvider } from "@courselit/common-models"; import { ALPHA_LABEL, APIKEY_CARD_DESCRIPTION, @@ -18,6 +18,7 @@ import { APIKEY_REMOVE_BTN, APIKEY_REMOVE_DIALOG_DESC, APIKEY_REMOVE_DIALOG_HEADER, + BETA_LABEL, LOGIN_METHODS_CARD_DESCRIPTION, LOGIN_METHODS_HEADER, TOAST_TITLE_ERROR, @@ -28,7 +29,7 @@ import { FeaturesContext, SiteInfoContext, } from "@components/contexts"; -import { capitalize, FetchBuilder } from "@courselit/utils"; +import { FetchBuilder } from "@courselit/utils"; import { Chip, useToast, @@ -41,6 +42,7 @@ import { import { Button } from "@components/ui/button"; import { CogIcon, Key, Trash2 } from "lucide-react"; import Link from "next/link"; +import { LOGIN_PROVIDER_REGISTRY } from "@/lib/login-providers"; type ApiKeyListItem = { name: string; @@ -165,24 +167,20 @@ export default function MiscellaneousTab() {
- {[ - Constants.LoginProvider.EMAIL, - Constants.LoginProvider.SSO, - ].map((provider) => ( + {LOGIN_PROVIDER_REGISTRY.map((provider) => (
{ toggleLoginProvider( - provider, + provider.key, value, ); }} />
- {provider === - Constants.LoginProvider.SSO - ? provider.toUpperCase() - : capitalize(provider)} + {provider.label} - {provider === + {provider.key === Constants.LoginProvider.SSO && ( <> - {!features.includes( - Constants.Features.SSO, - ) && Upgrade} - {{ALPHA_LABEL}} + {!!provider.featureFlag && + !features.includes( + provider.featureFlag as Features, + ) && Upgrade} + {{BETA_LABEL}} )} + {provider.key === + Constants.LoginProvider.GOOGLE && ( + {ALPHA_LABEL} + )}
- {provider !== Constants.LoginProvider.EMAIL && ( - + {provider.settingsRoute && ( + + ); +} diff --git a/apps/web/components/public/payments/__tests__/login-form.test.tsx b/apps/web/components/public/payments/__tests__/login-form.test.tsx new file mode 100644 index 000000000..bba79325d --- /dev/null +++ b/apps/web/components/public/payments/__tests__/login-form.test.tsx @@ -0,0 +1,234 @@ +import type { ReactNode } from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { Constants } from "@courselit/common-models"; +import { LOGIN_PROVIDER_AUTH_TYPE } from "@/lib/login-providers"; +import { + LOGIN_PROVIDER_SSO_BUTTON, + LOGIN_PROVIDER_SSO_LABEL, +} from "@/ui-config/strings"; + +jest.mock("@components/contexts", () => { + const React = require("react"); + + return { + AddressContext: React.createContext({ + backend: "", + frontend: "", + }), + ProfileContext: React.createContext({ + profile: null, + setProfile: jest.fn(), + }), + ServerConfigContext: React.createContext({ + recaptchaSiteKey: "", + turnstileSiteKey: "", + queueServer: "", + cacheEnabled: false, + }), + SiteInfoContext: React.createContext({ + logins: [], + }), + ThemeContext: React.createContext({ + theme: { + id: "", + name: "", + theme: {}, + }, + setTheme: jest.fn(), + }), + }; +}); + +jest.mock("@/lib/auth-client", () => ({ + authClient: { + signIn: { + sso: jest.fn(), + emailOtp: jest.fn(), + }, + emailOtp: { + sendVerificationOtp: jest.fn(), + }, + }, +})); + +jest.mock("@/app/(with-contexts)/helpers", () => ({ + getUserProfile: jest.fn(), +})); + +jest.mock("@/hooks/use-recaptcha", () => ({ + useRecaptcha: () => ({ + executeRecaptcha: null, + }), +})); + +jest.mock("@courselit/components-library", () => ({ + useToast: () => ({ + toast: jest.fn(), + }), +})); + +jest.mock("@components/recaptcha-script-loader", () => () => null, { + virtual: true, +}); + +jest.mock( + "@/components/ui/form", + () => ({ + FormControl: ({ children }: { children: ReactNode }) => children, + FormField: ({ + name, + render, + }: { + name: string; + render: (props: { field: Record }) => ReactNode; + }) => + render({ + field: { + name, + value: "", + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + }, + }), + FormItem: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + FormMessage: () => null, + }), + { virtual: true }, +); + +jest.mock("@courselit/page-primitives", () => ({ + Button: ({ + children, + onClick, + ...props + }: React.ButtonHTMLAttributes) => ( + + ), + Caption: ({ children }: { children: ReactNode }) =>
{children}
, + Input: (props: React.InputHTMLAttributes) => ( + + ), + Text1: ({ children }: { children: ReactNode }) =>
{children}
, +})); + +jest.mock("next/link", () => ({ + __esModule: true, + default: ({ children, href }: { children: ReactNode; href: string }) => ( + {children} + ), +})); + +import { LoginForm } from "../login-form"; +import { authClient } from "@/lib/auth-client"; +import { + AddressContext, + ProfileContext, + ServerConfigContext, + SiteInfoContext, + ThemeContext, +} from "@components/contexts"; + +function renderLoginForm() { + return render( + + + + + + + + + + + , + ); +} + +describe("Checkout LoginForm", () => { + beforeEach(() => { + jest.clearAllMocks(); + (authClient.signIn.sso as jest.Mock).mockResolvedValue({}); + }); + + it("starts SSO with the checkout callback URL", async () => { + renderLoginForm(); + + fireEvent.click( + screen.getByRole("button", { name: "Continue with SSO" }), + ); + + await waitFor(() => { + expect(authClient.signIn.sso).toHaveBeenCalledWith({ + providerId: "sso", + callbackURL: "/checkout?type=course&id=course-123", + }); + }); + }); +}); diff --git a/apps/web/components/public/payments/checkout.tsx b/apps/web/components/public/payments/checkout.tsx index 8888ef922..10da991ab 100644 --- a/apps/web/components/public/payments/checkout.tsx +++ b/apps/web/components/public/payments/checkout.tsx @@ -36,7 +36,8 @@ import Script from "next/script"; import { Button, Header3, Text1 } from "@courselit/page-primitives"; import { PaymentPlanCard } from "./payment-plan-card"; import { MobileOrderSummary, DesktopOrderSummary } from "./order-summary"; -import { SSOProvider } from "@/app/(with-contexts)/(with-layout)/login/page"; +import type { RuntimeLoginProvider } from "@/lib/login-providers"; +import { LOGIN_FORM_PERSONAL_INFORMATION_LABEL } from "@ui-config/strings"; const { PaymentPlanType: paymentPlanType } = Constants; export interface Product { @@ -55,7 +56,7 @@ export interface CheckoutScreenProps { product: Product; paymentPlans: PaymentPlan[]; includedProducts: Course[]; - ssoProvider?: SSOProvider; + loginProviders?: RuntimeLoginProvider[]; type?: MembershipEntityType; id?: string; } @@ -69,7 +70,7 @@ export default function Checkout({ product, paymentPlans, includedProducts, - ssoProvider, + loginProviders = [], type, id, }: CheckoutScreenProps) { @@ -397,14 +398,14 @@ export default function Checkout({ <>
- Personal Information + {LOGIN_FORM_PERSONAL_INFORMATION_LABEL} {!isLoggedIn ? ( diff --git a/apps/web/components/public/payments/login-form.tsx b/apps/web/components/public/payments/login-form.tsx index 048f688c3..ff800b2c4 100644 --- a/apps/web/components/public/payments/login-form.tsx +++ b/apps/web/components/public/payments/login-form.tsx @@ -29,8 +29,9 @@ import { authClient } from "@/lib/auth-client"; import Link from "next/link"; import { Constants, MembershipEntityType } from "@courselit/common-models"; import { useRecaptcha } from "@/hooks/use-recaptcha"; -import type { SSOProvider } from "@/app/(with-contexts)/(with-layout)/login/page"; +import type { RuntimeLoginProvider } from "@/lib/login-providers"; import RecaptchaScriptLoader from "@components/recaptcha-script-loader"; +import ExternalLoginButton from "@/components/auth/external-login-button"; const loginFormSchema = z.object({ email: z.string().email("Invalid email address"), @@ -41,14 +42,14 @@ type LoginStep = "email" | "otp" | "complete"; interface LoginFormProps { onLoginComplete: (email: string, name: string) => void; - ssoProvider?: SSOProvider; + loginProviders?: RuntimeLoginProvider[]; type?: MembershipEntityType; id?: string; } export function LoginForm({ onLoginComplete, - ssoProvider, + loginProviders = [], type, id, }: LoginFormProps) { @@ -314,21 +315,20 @@ export function LoginForm({ )} - {siteinfo.logins?.includes(Constants.LoginProvider.SSO) && - ssoProvider && ( - - )} + {loginProviders.map((provider) => ( + { + await authClient.signIn.sso({ + providerId: provider.providerId, + callbackURL: `/checkout?type=${type}&id=${id}`, + }); + }} + /> + ))} {LOGIN_FORM_DISCLAIMER} diff --git a/apps/web/graphql/settings/__tests__/sso.test.ts b/apps/web/graphql/settings/__tests__/sso.test.ts index e31b07d9d..30e60084d 100644 --- a/apps/web/graphql/settings/__tests__/sso.test.ts +++ b/apps/web/graphql/settings/__tests__/sso.test.ts @@ -1,26 +1,48 @@ import { - updateSSOProvider, + getGoogleProviderSettings, + getExternalLoginProviders, getSSOProviderSettings, getSSOProvider, + removeGoogleProvider, removeSSOProvider, getFeatures, toggleLoginProvider, + updateGoogleProvider, + updateSSOProvider, } from "../logic"; import DomainModel from "@models/Domain"; import UserModel from "@models/User"; import SSOProviderModel from "@models/SSOProvider"; import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; +import { invalidateDomainCache } from "@/lib/domain-cache"; +import { LOGIN_PROVIDER_AUTH_TYPE } from "../../../lib/login-providers"; +import { + LOGIN_PROVIDER_GOOGLE_BUTTON, + LOGIN_PROVIDER_GOOGLE_LABEL, + LOGIN_PROVIDER_SSO_BUTTON, + LOGIN_PROVIDER_SSO_LABEL, +} from "../../../ui-config/strings"; + +jest.mock("@/lib/domain-cache", () => ({ + invalidateDomainCache: jest.fn(), +})); const SUITE_PREFIX = `sso-tests-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; const email = (suffix: string) => `${suffix}-${SUITE_PREFIX}@example.com`; +const validSamlConfig = JSON.stringify({ + entryPoint: "https://test-idp.com", + cert: "test-cert", + idpMetadata: { metadata: "test-metadata" }, +}); describe("SSO Logic Tests", () => { let testDomain: any; let adminUser: any; let regularUser: any; let mockCtx: any; + const invalidateDomainCacheMock = invalidateDomainCache as jest.Mock; beforeAll(async () => { // Create test domain with SSO feature enabled @@ -65,6 +87,7 @@ describe("SSO Logic Tests", () => { afterEach(async () => { await SSOProviderModel.deleteMany({ domain: testDomain._id }); + invalidateDomainCacheMock.mockClear(); // Reset domain settings await DomainModel.updateOne( { _id: testDomain._id }, @@ -138,14 +161,29 @@ describe("SSO Logic Tests", () => { domain: testDomain._id, }); expect(savedProvider).toBeDefined(); + expect(savedProvider?.issuer).toBe( + "https://backend.example.com/api/auth/sso/saml2/sp/metadata?providerId=sso", + ); const samlConfig = JSON.parse(savedProvider!.samlConfig); + expect(samlConfig.issuer).toBe( + "https://backend.example.com/api/auth/sso/saml2/sp/metadata?providerId=sso", + ); expect(samlConfig.entryPoint).toBe(validConfig.entryPoint); + expect(samlConfig.callbackUrl).toBe( + "https://backend.example.com/api/auth/sso/saml2/sp/acs/sso", + ); + expect(samlConfig.spMetadata.entityID).toBe( + "https://backend.example.com/api/auth/sso/saml2/sp/metadata?providerId=sso", + ); // Check if domain settings updated (refresh context first or check DB) const domain = await DomainModel.findById(testDomain._id); expect(domain!.settings.ssoTrustedDomain).toBe( new URL(validConfig.entryPoint).origin, ); + expect(invalidateDomainCacheMock).toHaveBeenCalledWith( + testDomain.name, + ); }); }); @@ -210,6 +248,85 @@ describe("SSO Logic Tests", () => { }); }); + describe("updateGoogleProvider", () => { + const validConfig = { + clientId: "google-client-id", + clientSecret: "google-client-secret", + backend: "https://backend.example.com", + }; + + it("should create Google provider configuration", async () => { + const result = await updateGoogleProvider({ + ...validConfig, + context: mockCtx, + }); + + expect(result).toBeDefined(); + expect(result.providerId).toBe("google"); + + const savedProvider = await SSOProviderModel.findOne({ + domain: testDomain._id, + providerId: "google", + }); + expect(savedProvider?.issuer).toBe("https://accounts.google.com"); + + const oidcConfig = JSON.parse(savedProvider!.oidcConfig); + expect(oidcConfig.clientId).toBe(validConfig.clientId); + expect(oidcConfig.clientSecret).toBe(validConfig.clientSecret); + expect(oidcConfig.scopes).toEqual(["openid", "email", "profile"]); + expect(invalidateDomainCacheMock).toHaveBeenCalledWith( + testDomain.name, + ); + }); + + it("should preserve existing client secret when not provided again", async () => { + await updateGoogleProvider({ + ...validConfig, + context: mockCtx, + }); + + await updateGoogleProvider({ + clientId: "updated-client-id", + backend: validConfig.backend, + context: mockCtx, + }); + + const savedProvider = await SSOProviderModel.findOne({ + domain: testDomain._id, + providerId: "google", + }); + const oidcConfig = JSON.parse(savedProvider!.oidcConfig); + expect(oidcConfig.clientId).toBe("updated-client-id"); + expect(oidcConfig.clientSecret).toBe(validConfig.clientSecret); + }); + }); + + describe("getGoogleProviderSettings", () => { + it("should return null if no Google provider exists", async () => { + const result = await getGoogleProviderSettings(mockCtx); + expect(result).toBeNull(); + }); + + it("should return saved Google provider settings without exposing secret", async () => { + await SSOProviderModel.create({ + domain: testDomain._id, + providerId: "google", + issuer: "https://accounts.google.com", + oidcConfig: JSON.stringify({ + clientId: "test-client-id", + clientSecret: "test-client-secret", + }), + domain_string: "backend.com", + }); + + const result = await getGoogleProviderSettings(mockCtx); + expect(result).toEqual({ + clientId: "test-client-id", + hasClientSecret: true, + }); + }); + }); + describe("removeSSOProvider", () => { it("should remove provider and disable SSO login", async () => { // Setup @@ -217,7 +334,7 @@ describe("SSO Logic Tests", () => { id: id("sso-3"), domain: testDomain._id, providerId: "sso", - samlConfig: "{}", + samlConfig: validSamlConfig, domain_string: "test", }); @@ -243,17 +360,89 @@ describe("SSO Logic Tests", () => { Constants.LoginProvider.SSO, ); expect(domain!.settings.ssoTrustedDomain).toBeUndefined(); + expect(invalidateDomainCacheMock).toHaveBeenCalledWith( + testDomain.name, + ); + }); + }); + + describe("removeGoogleProvider", () => { + it("should remove provider and disable Google login", async () => { + await SSOProviderModel.create({ + domain: testDomain._id, + providerId: "google", + issuer: "https://accounts.google.com", + oidcConfig: JSON.stringify({ + clientId: "test-client-id", + clientSecret: "test-client-secret", + }), + domain_string: "test-domain", + }); + + await toggleLoginProvider({ + provider: Constants.LoginProvider.GOOGLE, + value: true, + ctx: mockCtx, + }); + + const result = await removeGoogleProvider(mockCtx); + expect(result).toBe(true); + + const provider = await SSOProviderModel.findOne({ + domain: testDomain._id, + providerId: "google", + }); + expect(provider).toBeNull(); + + const domain = await DomainModel.findById(testDomain._id); + expect(domain!.settings.logins).not.toContain( + Constants.LoginProvider.GOOGLE, + ); }); }); describe("toggleLoginProvider", () => { + it("should enable Google login if provider configured", async () => { + await SSOProviderModel.create({ + domain: testDomain._id, + providerId: "google", + issuer: "https://accounts.google.com", + oidcConfig: JSON.stringify({ + clientId: "test-client-id", + clientSecret: "test-client-secret", + }), + domain_string: "test-domain", + }); + + const result = await toggleLoginProvider({ + provider: Constants.LoginProvider.GOOGLE, + value: true, + ctx: mockCtx, + }); + + expect(result).toContain(Constants.LoginProvider.GOOGLE); + expect(invalidateDomainCacheMock).toHaveBeenCalledWith( + testDomain.name, + ); + }); + + it("should throw if enabling Google without provider", async () => { + await expect( + toggleLoginProvider({ + provider: Constants.LoginProvider.GOOGLE, + value: true, + ctx: mockCtx, + }), + ).rejects.toThrow(); + }); + it("should enable SSO login if provider configured", async () => { // Must have provider first await SSOProviderModel.create({ id: id("sso-4"), domain: testDomain._id, providerId: "sso", - samlConfig: "{}", + samlConfig: validSamlConfig, domain_string: "test", }); @@ -266,6 +455,48 @@ describe("SSO Logic Tests", () => { expect(result).toContain(Constants.LoginProvider.SSO); }); + it("should enable Google and SSO together", async () => { + await SSOProviderModel.create({ + domain: testDomain._id, + providerId: "google", + issuer: "https://accounts.google.com", + oidcConfig: JSON.stringify({ + clientId: "test-client-id", + clientSecret: "test-client-secret", + }), + domain_string: "test-domain", + }); + await SSOProviderModel.create({ + domain: testDomain._id, + providerId: "sso", + samlConfig: JSON.stringify({ + entryPoint: "https://test-idp.com", + cert: "test-cert", + idpMetadata: { metadata: "test-metadata" }, + }), + domain_string: "test-domain", + }); + + await toggleLoginProvider({ + provider: Constants.LoginProvider.GOOGLE, + value: true, + ctx: mockCtx, + }); + const result = await toggleLoginProvider({ + provider: Constants.LoginProvider.SSO, + value: true, + ctx: mockCtx, + }); + + expect(result).toEqual( + expect.arrayContaining([ + Constants.LoginProvider.EMAIL, + Constants.LoginProvider.GOOGLE, + Constants.LoginProvider.SSO, + ]), + ); + }); + it("should throw if enabling SSO without provider", async () => { await expect( toggleLoginProvider({ @@ -294,7 +525,7 @@ describe("SSO Logic Tests", () => { id: id("sso-5"), domain: testDomain._id, providerId: "sso", - samlConfig: "{}", + samlConfig: validSamlConfig, domain_string: "test", }); await toggleLoginProvider({ @@ -318,7 +549,7 @@ describe("SSO Logic Tests", () => { id: id("sso-auto-enable"), domain: testDomain._id, providerId: "sso", - samlConfig: "{}", + samlConfig: validSamlConfig, domain_string: "test", }); @@ -356,6 +587,62 @@ describe("SSO Logic Tests", () => { }); }); + describe("getExternalLoginProviders", () => { + it("should return enabled and configured external providers", async () => { + await SSOProviderModel.create([ + { + domain: testDomain._id, + providerId: "google", + issuer: "https://accounts.google.com", + oidcConfig: JSON.stringify({ + clientId: "test-client-id", + clientSecret: "test-client-secret", + }), + domain_string: "test-domain", + }, + { + domain: testDomain._id, + providerId: "sso", + samlConfig: JSON.stringify({ + entryPoint: "https://test-idp.com", + cert: "test-cert", + idpMetadata: { metadata: "test-metadata" }, + }), + domain_string: "test-domain", + }, + ]); + + await toggleLoginProvider({ + provider: Constants.LoginProvider.GOOGLE, + value: true, + ctx: mockCtx, + }); + await toggleLoginProvider({ + provider: Constants.LoginProvider.SSO, + value: true, + ctx: mockCtx, + }); + + const result = await getExternalLoginProviders(mockCtx); + expect(result).toEqual([ + { + key: Constants.LoginProvider.GOOGLE, + providerId: Constants.LoginProvider.GOOGLE, + label: LOGIN_PROVIDER_GOOGLE_LABEL, + buttonText: LOGIN_PROVIDER_GOOGLE_BUTTON, + authType: LOGIN_PROVIDER_AUTH_TYPE.OIDC, + }, + { + key: Constants.LoginProvider.SSO, + providerId: Constants.LoginProvider.SSO, + label: LOGIN_PROVIDER_SSO_LABEL, + buttonText: LOGIN_PROVIDER_SSO_BUTTON, + authType: LOGIN_PROVIDER_AUTH_TYPE.SAML, + }, + ]); + }); + }); + describe("getFeatures", () => { it("should return domain features", async () => { const features = await getFeatures(mockCtx); diff --git a/apps/web/graphql/settings/helpers.ts b/apps/web/graphql/settings/helpers.ts index 389021579..8f23c6a8f 100644 --- a/apps/web/graphql/settings/helpers.ts +++ b/apps/web/graphql/settings/helpers.ts @@ -9,6 +9,7 @@ import { UIConstants, } from "@courselit/common-models"; import GQLContext from "@models/GQLContext"; +import { invalidateDomainCache } from "@/lib/domain-cache"; const currencyISOCodes = currencies.map((currency) => currency.isoCode?.toLowerCase(), @@ -129,4 +130,5 @@ export async function saveLoginProvider({ } ctx.subdomain.settings.logins = logins; await (ctx.subdomain as any).save(); + invalidateDomainCache(ctx.subdomain.name); } diff --git a/apps/web/graphql/settings/logic.ts b/apps/web/graphql/settings/logic.ts index 9dbcd8537..aa39d58e0 100644 --- a/apps/web/graphql/settings/logic.ts +++ b/apps/web/graphql/settings/logic.ts @@ -12,6 +12,7 @@ import DomainModel, { Domain } from "../../models/Domain"; import { checkPermission } from "@courselit/utils"; import { Constants, + Features, LoginProvider, Media, Typeface, @@ -19,9 +20,99 @@ import { import ApikeyModel, { ApiKey } from "@models/ApiKey"; import SSOProviderModel from "@models/SSOProvider"; import { sealMedia } from "@/services/medialit"; +import { + getLoginProviderDefinition, + LOGIN_PROVIDER_REGISTRY, + RuntimeLoginProvider, +} from "@/lib/login-providers"; +import { invalidateDomainCache } from "@/lib/domain-cache"; const { permissions } = constants; +const GOOGLE_PROVIDER_ID = getLoginProviderDefinition( + Constants.LoginProvider.GOOGLE, +)?.providerId; +const SAML_PROVIDER_ID = getLoginProviderDefinition( + Constants.LoginProvider.SSO, +)?.providerId; + +if (!GOOGLE_PROVIDER_ID || !SAML_PROVIDER_ID) { + throw new Error("Login provider registry is missing required provider IDs"); +} + +type StoredOIDCConfig = { + clientId?: string; + clientSecret?: string; + scopes?: string[]; + pkce?: boolean; + mapping?: Record; +}; + +const buildGoogleOIDCConfig = ({ + clientId, + clientSecret, +}: { + clientId: string; + clientSecret: string; +}) => ({ + clientId, + clientSecret, + scopes: ["openid", "email", "profile"], + pkce: true, + mapping: { + id: "sub", + email: "email", + emailVerified: "email_verified", + name: "name", + image: "picture", + }, +}); + +const parseOIDCConfig = (oidcConfig?: string): StoredOIDCConfig => { + if (!oidcConfig) { + return {}; + } + + return JSON.parse(oidcConfig) as StoredOIDCConfig; +}; + +const isSAMLProviderConfigured = (samlConfig?: string) => { + const parsed = JSON.parse(samlConfig || "{}"); + return !!(parsed.entryPoint && parsed.cert && parsed.idpMetadata?.metadata); +}; + +const isGoogleProviderConfigured = (oidcConfig?: string) => { + const parsed = parseOIDCConfig(oidcConfig); + return !!(parsed.clientId && parsed.clientSecret); +}; + +const isExternalProviderConfigured = ({ + provider, + oidcConfig, + samlConfig, +}: { + provider: LoginProvider; + oidcConfig?: string; + samlConfig?: string; +}) => { + switch (provider) { + case Constants.LoginProvider.GOOGLE: + return isGoogleProviderConfigured(oidcConfig); + case Constants.LoginProvider.SSO: + return isSAMLProviderConfigured(samlConfig); + default: + return false; + } +}; + +const assertCanManageSettings = (ctx: GQLContext) => { + checkIfAuthenticated(ctx); + + if (!checkPermission(ctx.user.permissions, [permissions.manageSettings])) { + throw new Error(responses.action_not_allowed); + } +}; + export const getSiteInfo = async (ctx: GQLContext) => { const exclusionProjection: Record = { email: 0, @@ -272,11 +363,7 @@ export const updateSSOProvider = async ({ backend: string; context: GQLContext; }) => { - checkIfAuthenticated(ctx); - - if (!checkPermission(ctx.user.permissions, [permissions.manageSettings])) { - throw new Error(responses.action_not_allowed); - } + assertCanManageSettings(ctx); if (!ctx.subdomain.features?.includes(Constants.Features.SSO)) { throw new Error(responses.action_not_allowed); @@ -287,22 +374,28 @@ export const updateSSOProvider = async ({ } const backendUrl = new URL(backend); + const spEntityId = `${backendUrl.origin}/api/auth/sso/saml2/sp/metadata?providerId=${SAML_PROVIDER_ID}`; try { const ssoProvider = await SSOProviderModel.findOneAndUpdate( { domain: ctx.subdomain._id, + providerId: SAML_PROVIDER_ID, }, { - providerId: "sso", + providerId: SAML_PROVIDER_ID, + issuer: spEntityId, samlConfig: JSON.stringify({ + issuer: spEntityId, entryPoint, cert, - callbackUrl: `${backendUrl.origin}/api/auth/sso/saml2/callback/sso`, + callbackUrl: `${backendUrl.origin}/api/auth/sso/saml2/sp/acs/${SAML_PROVIDER_ID}`, idpMetadata: { metadata: idpMetadata, }, - spMetadata: {}, + spMetadata: { + entityID: spEntityId, + }, }), domain_string: backendUrl.hostname, domain: ctx.subdomain._id, @@ -316,6 +409,7 @@ export const updateSSOProvider = async ({ ctx.subdomain.settings.ssoTrustedDomain = new URL(entryPoint).origin; (ctx.subdomain as any).markModified("settings"); await (ctx.subdomain as any).save(); + invalidateDomainCache(ctx.subdomain.name); return ssoProvider; } catch (error: any) { @@ -324,11 +418,7 @@ export const updateSSOProvider = async ({ }; export const getSSOProviderSettings = async (ctx: GQLContext) => { - checkIfAuthenticated(ctx); - - if (!checkPermission(ctx.user.permissions, [permissions.manageSettings])) { - throw new Error(responses.action_not_allowed); - } + assertCanManageSettings(ctx); if (!ctx.subdomain.features?.includes(Constants.Features.SSO)) { throw new Error(responses.action_not_allowed); @@ -336,6 +426,7 @@ export const getSSOProviderSettings = async (ctx: GQLContext) => { const ssoProvider = await SSOProviderModel.findOne({ domain: ctx.subdomain._id, + providerId: SAML_PROVIDER_ID, }); if (!ssoProvider) { @@ -359,6 +450,7 @@ export const getSSOProvider = async (ctx: GQLContext) => { const ssoProvider = await SSOProviderModel.findOne( { domain: ctx.subdomain._id, + providerId: SAML_PROVIDER_ID, }, { providerId: 1, @@ -376,18 +468,147 @@ export const getSSOProvider = async (ctx: GQLContext) => { }; }; -export const removeSSOProvider = async (ctx: GQLContext) => { - checkIfAuthenticated(ctx); +export const getGoogleProviderSettings = async (ctx: GQLContext) => { + assertCanManageSettings(ctx); - if (!checkPermission(ctx.user.permissions, [permissions.manageSettings])) { - throw new Error(responses.action_not_allowed); + const googleProvider = await SSOProviderModel.findOne({ + domain: ctx.subdomain._id, + providerId: GOOGLE_PROVIDER_ID, + }); + + if (!googleProvider) { + return null; + } + + const oidcConfig = parseOIDCConfig(googleProvider.oidcConfig); + + return { + clientId: oidcConfig.clientId || "", + hasClientSecret: !!oidcConfig.clientSecret, + }; +}; + +export const updateGoogleProvider = async ({ + clientId, + clientSecret, + backend, + context: ctx, +}: { + clientId: string; + clientSecret?: string; + backend: string; + context: GQLContext; +}) => { + assertCanManageSettings(ctx); + + if (!clientId || !backend) { + throw new Error(responses.provider_invalid_configuration); + } + + const existingProvider = await SSOProviderModel.findOne({ + domain: ctx.subdomain._id, + providerId: GOOGLE_PROVIDER_ID, + }); + const existingConfig = parseOIDCConfig(existingProvider?.oidcConfig); + const resolvedClientSecret = clientSecret || existingConfig.clientSecret; + + if (!resolvedClientSecret) { + throw new Error(responses.provider_invalid_configuration); } + const backendUrl = new URL(backend); + + const googleProvider = await SSOProviderModel.findOneAndUpdate( + { + domain: ctx.subdomain._id, + providerId: GOOGLE_PROVIDER_ID, + }, + { + providerId: GOOGLE_PROVIDER_ID, + issuer: "https://accounts.google.com", + oidcConfig: JSON.stringify( + buildGoogleOIDCConfig({ + clientId, + clientSecret: resolvedClientSecret, + }), + ), + domain_string: backendUrl.hostname, + domain: ctx.subdomain._id, + }, + { + upsert: true, + new: true, + }, + ); + + invalidateDomainCache(ctx.subdomain.name); + + return googleProvider; +}; + +export const getExternalLoginProviders = async (ctx: GQLContext) => { + const enabledProviders = new Set( + ctx.subdomain.settings.logins || [Constants.LoginProvider.EMAIL], + ); + const externalDefinitions = LOGIN_PROVIDER_REGISTRY.filter( + (item) => !!item.providerId, + ); + + const configuredProviders = await SSOProviderModel.find({ + domain: ctx.subdomain._id, + providerId: { + $in: externalDefinitions + .map((item) => item.providerId) + .filter(Boolean), + }, + }).lean(); + const providerMap = new Map( + configuredProviders.map((provider) => [provider.providerId, provider]), + ); + + return externalDefinitions + .filter((definition) => enabledProviders.has(definition.key)) + .filter( + (definition) => + !definition.featureFlag || + ctx.subdomain.features?.includes( + definition.featureFlag as Features, + ), + ) + .filter((definition) => { + const provider = providerMap.get(definition.providerId!); + + return ( + !!provider && + isExternalProviderConfigured({ + provider: definition.key, + oidcConfig: provider.oidcConfig, + samlConfig: provider.samlConfig, + }) + ); + }) + .map( + (definition): RuntimeLoginProvider => ({ + key: definition.key, + providerId: definition.providerId!, + label: definition.label, + buttonText: definition.buttonText!, + authType: definition.authType!, + }), + ); +}; + +export const removeSSOProvider = async (ctx: GQLContext) => { + assertCanManageSettings(ctx); + if (!ctx.subdomain.features?.includes(Constants.Features.SSO)) { throw new Error(responses.action_not_allowed); } - await SSOProviderModel.deleteMany({ domain: ctx.subdomain._id }); + await SSOProviderModel.deleteOne({ + domain: ctx.subdomain._id, + providerId: SAML_PROVIDER_ID, + }); await toggleLoginProvider({ provider: Constants.LoginProvider.SSO, @@ -398,6 +619,24 @@ export const removeSSOProvider = async (ctx: GQLContext) => { ctx.subdomain.settings.ssoTrustedDomain = undefined; (ctx.subdomain as any).markModified("settings"); await (ctx.subdomain as any).save(); + invalidateDomainCache(ctx.subdomain.name); + + return true; +}; + +export const removeGoogleProvider = async (ctx: GQLContext) => { + assertCanManageSettings(ctx); + + await SSOProviderModel.deleteOne({ + domain: ctx.subdomain._id, + providerId: GOOGLE_PROVIDER_ID, + }); + + await toggleLoginProvider({ + provider: Constants.LoginProvider.GOOGLE, + value: false, + ctx, + }); return true; }; @@ -419,11 +658,7 @@ export const toggleLoginProvider = async ({ value: boolean; ctx: GQLContext; }) => { - checkIfAuthenticated(ctx); - - if (!checkPermission(ctx.user.permissions, [permissions.manageSettings])) { - throw new Error(responses.action_not_allowed); - } + assertCanManageSettings(ctx); switch (provider) { case Constants.LoginProvider.EMAIL: @@ -443,17 +678,47 @@ export const toggleLoginProvider = async ({ provider: Constants.LoginProvider.EMAIL, }); break; + case Constants.LoginProvider.GOOGLE: + if (value) { + const googleProvider = await SSOProviderModel.findOne({ + domain: ctx.subdomain._id, + providerId: GOOGLE_PROVIDER_ID, + }); + + if ( + !googleProvider || + !isExternalProviderConfigured({ + provider, + oidcConfig: googleProvider.oidcConfig, + }) + ) { + throw new Error(responses.provider_not_configured); + } + } + await saveLoginProvider({ + ctx, + value, + provider: Constants.LoginProvider.GOOGLE, + }); + break; case Constants.LoginProvider.SSO: if (value) { if (!ctx.subdomain.features?.includes(Constants.Features.SSO)) { throw new Error(responses.action_not_allowed); } - const ssoProviders = await SSOProviderModel.find({ + const ssoProvider = await SSOProviderModel.findOne({ domain: ctx.subdomain._id, + providerId: SAML_PROVIDER_ID, }); - if (ssoProviders.length === 0) { + if ( + !ssoProvider || + !isExternalProviderConfigured({ + provider, + samlConfig: ssoProvider.samlConfig, + }) + ) { throw new Error(responses.provider_not_configured); } } diff --git a/apps/web/graphql/settings/mutation.ts b/apps/web/graphql/settings/mutation.ts index 9d878aae0..cd42d7eb2 100644 --- a/apps/web/graphql/settings/mutation.ts +++ b/apps/web/graphql/settings/mutation.ts @@ -12,8 +12,10 @@ import { updateDraftTypefaces, removeApikey, addApikey, + removeGoogleProvider, removeSSOProvider, toggleLoginProvider, + updateGoogleProvider, updateSSOProvider, } from "./logic"; import { LoginProvider, Typeface } from "@courselit/common-models"; @@ -110,11 +112,43 @@ const mutations = { context, }), }, + updateGoogleProvider: { + type: types.ssoProviderType, + args: { + clientId: { type: new GraphQLNonNull(GraphQLString) }, + clientSecret: { type: GraphQLString }, + backend: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: async ( + _: any, + { + clientId, + clientSecret, + backend, + }: { + clientId: string; + clientSecret?: string; + backend: string; + }, + context: any, + ) => + updateGoogleProvider({ + clientId, + clientSecret, + backend, + context, + }), + }, removeSSOProvider: { type: new GraphQLNonNull(GraphQLBoolean), resolve: async (_: any, __: any, context: any) => removeSSOProvider(context), }, + removeGoogleProvider: { + type: new GraphQLNonNull(GraphQLBoolean), + resolve: async (_: any, __: any, context: any) => + removeGoogleProvider(context), + }, toggleLoginProvider: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)), args: { diff --git a/apps/web/graphql/settings/query.ts b/apps/web/graphql/settings/query.ts index eaac353c8..51cc70dcd 100644 --- a/apps/web/graphql/settings/query.ts +++ b/apps/web/graphql/settings/query.ts @@ -1,8 +1,9 @@ import types from "./types"; import { getApikeys, + getExternalLoginProviders, getFeatures, - // getLoginProviders, + getGoogleProviderSettings, getSiteInfo, getSSOProvider, getSSOProviderSettings, @@ -25,22 +26,26 @@ const queries = { resolve: (_: any, {}: any, context: GQLContext) => getSSOProvider(context), }, + getGoogleProviderSettings: { + type: types.googleProviderSettingsType, + resolve: (_: any, {}: any, context: GQLContext) => + getGoogleProviderSettings(context), + }, getSSOProviderSettings: { type: types.ssoProviderSettingsType, resolve: (_: any, {}: any, context: GQLContext) => getSSOProviderSettings(context), }, + getExternalLoginProviders: { + type: new GraphQLList(types.loginProviderType), + resolve: (_: any, {}: any, context: GQLContext) => + getExternalLoginProviders(context), + }, getFeatures: { type: new GraphQLList(GraphQLString), args: {}, resolve: (_: any, {}: any, context: GQLContext) => getFeatures(context), }, - // getLoginProviders: { - // type: new GraphQLList(GraphQLString), - // args: {}, - // resolve: (_: any, { }: any, context: GQLContext) => - // getLoginProviders(context), - // }, }; export default queries; diff --git a/apps/web/graphql/settings/types.ts b/apps/web/graphql/settings/types.ts index b65e28f24..a72503208 100644 --- a/apps/web/graphql/settings/types.ts +++ b/apps/web/graphql/settings/types.ts @@ -171,6 +171,25 @@ const ssoProviderSettingsType = new GraphQLObjectType({ }, }); +const googleProviderSettingsType = new GraphQLObjectType({ + name: "GoogleProviderSettings", + fields: { + clientId: { type: new GraphQLNonNull(GraphQLString) }, + hasClientSecret: { type: new GraphQLNonNull(GraphQLBoolean) }, + }, +}); + +const loginProviderType = new GraphQLObjectType({ + name: "LoginProviderDescriptor", + fields: { + key: { type: new GraphQLNonNull(GraphQLString) }, + providerId: { type: new GraphQLNonNull(GraphQLString) }, + label: { type: new GraphQLNonNull(GraphQLString) }, + buttonText: { type: new GraphQLNonNull(GraphQLString) }, + authType: { type: new GraphQLNonNull(GraphQLString) }, + }, +}); + const types = { siteUpdateType, sitePaymentUpdateType, @@ -181,6 +200,8 @@ const types = { newApikeyType, ssoProviderType, ssoProviderSettingsType, + googleProviderSettingsType, + loginProviderType, }; export default types; diff --git a/apps/web/graphql/users/__tests__/logic.test.ts b/apps/web/graphql/users/__tests__/logic.test.ts index 95239cea9..cccbccfc3 100644 --- a/apps/web/graphql/users/__tests__/logic.test.ts +++ b/apps/web/graphql/users/__tests__/logic.test.ts @@ -2,7 +2,19 @@ * @jest-environment node */ -import { getCertificate } from "../logic"; +jest.mock("../../notifications/logic", () => ({ + seedNotificationPreferencesForUser: jest.fn(), +})); + +jest.mock("@/lib/record-activity", () => ({ + recordActivity: jest.fn(), +})); + +jest.mock("@/lib/trigger-sequences", () => ({ + triggerSequences: jest.fn(), +})); + +import { finalizeUserCreation, getCertificate } from "../logic"; import CertificateModel from "@models/Certificate"; import UserModel from "@models/User"; import CourseModel from "@models/Course"; @@ -12,6 +24,105 @@ import Domain from "@models/Domain"; import PageModel from "@models/Page"; import MembershipModel from "@models/Membership"; import CommunityModel from "@models/Community"; +import { Constants } from "@courselit/common-models"; +import { seedNotificationPreferencesForUser } from "../../notifications/logic"; +import { recordActivity } from "@/lib/record-activity"; +import { triggerSequences } from "@/lib/trigger-sequences"; + +const seedNotificationPreferencesForUserMock = + seedNotificationPreferencesForUser as jest.Mock; +const recordActivityMock = recordActivity as jest.Mock; +const triggerSequencesMock = triggerSequences as jest.Mock; + +describe("finalizeUserCreation", () => { + const domainId = "domain-123" as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should seed notification preferences and record user creation", async () => { + const user = { + userId: "user-123", + subscribedToUpdates: false, + domain: domainId, + } as any; + + await finalizeUserCreation(user); + + expect(seedNotificationPreferencesForUserMock).toHaveBeenCalledWith({ + domain: domainId, + userId: user.userId, + }); + expect(recordActivityMock).toHaveBeenCalledTimes(1); + expect(recordActivityMock).toHaveBeenCalledWith({ + domain: domainId, + userId: user.userId, + type: Constants.ActivityType.USER_CREATED, + entityId: user.userId, + }); + expect(triggerSequencesMock).not.toHaveBeenCalled(); + }); + + it("should trigger newsletter side effects for subscribed users", async () => { + const user = { + userId: "user-123", + subscribedToUpdates: true, + domain: domainId, + } as any; + + await finalizeUserCreation(user); + + expect(triggerSequencesMock).toHaveBeenCalledWith({ + user, + event: Constants.EventType.SUBSCRIBER_ADDED, + }); + expect(recordActivityMock).toHaveBeenCalledTimes(2); + expect(recordActivityMock).toHaveBeenNthCalledWith(2, { + domain: domainId, + userId: user.userId, + type: Constants.ActivityType.NEWSLETTER_SUBSCRIBED, + entityId: user.userId, + }); + }); + + it("should use explicit domain override when user domain is unavailable", async () => { + const user = { + userId: "user-123", + subscribedToUpdates: false, + } as any; + + await finalizeUserCreation(user, domainId); + + expect(seedNotificationPreferencesForUserMock).toHaveBeenCalledWith({ + domain: domainId, + userId: user.userId, + }); + expect(recordActivityMock).toHaveBeenCalledWith({ + domain: domainId, + userId: user.userId, + type: Constants.ActivityType.USER_CREATED, + entityId: user.userId, + }); + }); + + it("should pass the explicit domain override to sequence triggering", async () => { + const user = { + userId: "user-123", + subscribedToUpdates: true, + } as any; + + await finalizeUserCreation(user, domainId); + + expect(triggerSequencesMock).toHaveBeenCalledWith({ + user: { + ...user, + domain: domainId, + }, + event: Constants.EventType.SUBSCRIBER_ADDED, + }); + }); +}); describe("Certificate generation", () => { let mockCtx: any; diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts index 2196d0a5e..5bc433545 100644 --- a/apps/web/graphql/users/logic.ts +++ b/apps/web/graphql/users/logic.ts @@ -7,12 +7,7 @@ import constants from "@/config/constants"; import GQLContext from "@/models/GQLContext"; import { initMandatoryPages } from "../pages/logic"; import { Domain } from "@models/Domain"; -import { - checkPermission, - generateUniqueId, - getEmailFrom, - getPlanPrice, -} from "@courselit/utils"; +import { checkPermission, getEmailFrom, getPlanPrice } from "@courselit/utils"; import UserSegmentModel from "@models/UserSegment"; import { InternalCourse, @@ -61,7 +56,6 @@ import { cleanupPersonalData, } from "./helpers"; const { permissions } = UIConstants; -import { ObjectId } from "mongodb"; import { sealMedia } from "@/services/medialit"; import { seedNotificationPreferencesForUser } from "../notifications/logic"; import { sanitizeEmail } from "@/lib/sanitize-email"; @@ -438,66 +432,31 @@ export async function createUser({ const isNewUser = !rawResult.lastErrorObject!.updatedExisting; if (isNewUser) { - await seedNotificationPreferencesForUser({ - domain: domain._id, - userId: createdUser.userId, - }); - if (superAdmin) { await initMandatoryPages(domain, createdUser); await createInternalPaymentPlan(domain, createdUser.userId); } - await recordActivityAndTriggerSequences(createdUser, domain); + await finalizeUserCreation(createdUser); } return createdUser; } -export async function updateUserAfterCreationViaAuth( - id: string, - domain: Domain, +export async function finalizeUserCreation( + user: InternalUser, + domainOverride?: mongoose.Types.ObjectId | string, ) { - const updatedUser = await UserModel.findOneAndUpdate( - { - _id: new ObjectId(id), - domain: domain._id, - }, - { - $set: { - domain: domain._id, - userId: generateUniqueId(), - active: true, - purchases: [], - permissions: [ - constants.permissions.enrollInCourse, - constants.permissions.manageMedia, - ], - lead: constants.leadWebsite, - subscribedToUpdates: true, - tags: [], - unsubscribeToken: generateUniqueId(), - }, - }, - { new: true }, - ); + const domain = domainOverride || user.domain; + const userWithDomain = { ...user, domain } as InternalUser; - if (updatedUser) { - await seedNotificationPreferencesForUser({ - domain: domain._id, - userId: updatedUser.userId, - }); - } - - await recordActivityAndTriggerSequences(updatedUser, domain); -} + await seedNotificationPreferencesForUser({ + domain: domain as mongoose.Types.ObjectId, + userId: user.userId, + }); -async function recordActivityAndTriggerSequences( - user: InternalUser, - domain: Domain, -) { await recordActivity({ - domain: domain._id, + domain: domain as mongoose.Types.ObjectId, userId: user.userId, type: Constants.ActivityType.USER_CREATED, entityId: user.userId, @@ -505,12 +464,12 @@ async function recordActivityAndTriggerSequences( if (user.subscribedToUpdates) { await triggerSequences({ - user: user, + user: userWithDomain, event: Constants.EventType.SUBSCRIBER_ADDED, }); await recordActivity({ - domain: domain!._id, + domain: domain as mongoose.Types.ObjectId, userId: user.userId, type: Constants.ActivityType.NEWSLETTER_SUBSCRIBED, entityId: user.userId, diff --git a/apps/web/lib/login-providers.ts b/apps/web/lib/login-providers.ts new file mode 100644 index 000000000..7833da5ce --- /dev/null +++ b/apps/web/lib/login-providers.ts @@ -0,0 +1,61 @@ +import { Constants, type LoginProvider } from "@courselit/common-models"; +import { + LOGIN_PROVIDER_EMAIL_LABEL, + LOGIN_PROVIDER_GOOGLE_BUTTON, + LOGIN_PROVIDER_GOOGLE_LABEL, + LOGIN_PROVIDER_SSO_BUTTON, + LOGIN_PROVIDER_SSO_LABEL, +} from "../ui-config/strings"; + +export const LOGIN_PROVIDER_AUTH_TYPE = { + OIDC: "oidc", + SAML: "saml", +} as const; + +export type LoginProviderAuthType = + (typeof LOGIN_PROVIDER_AUTH_TYPE)[keyof typeof LOGIN_PROVIDER_AUTH_TYPE]; + +export type RuntimeLoginProvider = { + key: LoginProvider; + providerId: string; + label: string; + buttonText: string; + authType: LoginProviderAuthType; +}; + +export type LoginProviderDefinition = { + key: LoginProvider; + label: string; + providerId?: string; + buttonText?: string; + authType?: LoginProviderAuthType; + settingsRoute?: string; + featureFlag?: string; +}; + +export const LOGIN_PROVIDER_REGISTRY: LoginProviderDefinition[] = [ + { + key: Constants.LoginProvider.EMAIL, + label: LOGIN_PROVIDER_EMAIL_LABEL, + }, + { + key: Constants.LoginProvider.GOOGLE, + providerId: Constants.LoginProvider.GOOGLE, + label: LOGIN_PROVIDER_GOOGLE_LABEL, + buttonText: LOGIN_PROVIDER_GOOGLE_BUTTON, + authType: LOGIN_PROVIDER_AUTH_TYPE.OIDC, + settingsRoute: `/dashboard/settings/login-provider/${Constants.LoginProvider.GOOGLE}`, + }, + { + key: Constants.LoginProvider.SSO, + providerId: Constants.LoginProvider.SSO, + label: LOGIN_PROVIDER_SSO_LABEL, + buttonText: LOGIN_PROVIDER_SSO_BUTTON, + authType: LOGIN_PROVIDER_AUTH_TYPE.SAML, + settingsRoute: `/dashboard/settings/login-provider/${Constants.LoginProvider.SSO}`, + featureFlag: Constants.Features.SSO, + }, +]; + +export const getLoginProviderDefinition = (provider: LoginProvider) => + LOGIN_PROVIDER_REGISTRY.find((item) => item.key === provider); diff --git a/apps/web/package.json b/apps/web/package.json index c79b418fe..05a53169a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,137 +1,137 @@ { - "name": "@courselit/web", - "version": "0.73.10", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "prettier": "prettier --write **/*.ts" - }, - "browserslist": { - "production": [ - "chrome >= 109", - "edge >= 109", - "firefox >= 109", - "safari >= 15.4", - "ios_saf >= 15.4", - "not dead" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "dependencies": { - "@better-auth/sso": "^1.4.6", - "@courselit/common-logic": "workspace:^", - "@courselit/common-models": "workspace:^", - "@courselit/components-library": "workspace:^", - "@courselit/email-editor": "workspace:^", - "@courselit/icons": "workspace:^", - "@courselit/orm-models": "workspace:^", - "@courselit/page-blocks": "workspace:^", - "@courselit/page-models": "workspace:^", - "@courselit/page-primitives": "workspace:^", - "@courselit/text-editor": "workspace:^", - "@courselit/utils": "workspace:^", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^8.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@hookform/resolvers": "^3.9.1", - "@radix-ui/react-alert-dialog": "^1.1.11", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.4", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.4", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.6", - "@radix-ui/react-toggle-group": "^1.1.7", - "@radix-ui/react-tooltip": "^1.1.8", - "@radix-ui/react-visually-hidden": "^1.1.0", - "@stripe/stripe-js": "^5.4.0", - "@types/base-64": "^1.0.0", - "adm-zip": "^0.5.16", - "archiver": "^5.3.1", - "aws4": "^1.13.2", - "base-64": "^1.0.0", - "better-auth": "^1.4.1", - "chart.js": "^4.4.7", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "color-convert": "^3.1.0", - "cookie": "^0.4.2", - "date-fns": "^4.1.0", - "graphql": "^16.10.0", - "graphql-type-json": "^0.3.2", - "jsdom": "^26.1.0", - "lodash.debounce": "^4.0.8", - "lucide-react": "^0.553.0", - "medialit": "0.2.0", - "mongodb": "^6.15.0", - "mongoose": "^8.13.1", - "next": "^16.0.10", - "next-themes": "^0.4.6", - "nodemailer": "^6.7.2", - "pug": "^3.0.2", - "razorpay": "^2.9.4", - "react": "19.2.0", - "react-chartjs-2": "^5.3.0", - "react-csv": "^2.2.2", - "react-dom": "19.2.0", - "react-hook-form": "^7.54.1", - "recharts": "^2.15.1", - "remirror": "^3.0.1", - "sharp": "^0.33.2", - "slugify": "^1.6.5", - "sonner": "^2.0.7", - "stripe": "^17.5.0", - "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7", - "xml2js": "^0.6.2", - "zod": "^3.24.1" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@shelf/jest-mongodb": "^5.2.2", - "@types/adm-zip": "^0.5.7", - "@types/bcryptjs": "^2.4.2", - "@types/cookie": "^0.4.1", - "@types/mongodb": "^4.0.7", - "@types/node": "17.0.21", - "@types/nodemailer": "^6.4.4", - "@types/pug": "^2.0.6", - "@types/react": "19.2.4", - "@types/xml2js": "^0.4.14", - "eslint": "^9.12.0", - "eslint-config-next": "16.0.3", - "eslint-config-prettier": "^9.0.0", - "identity-obj-proxy": "^3.0.0", - "mongodb-memory-server": "^10.1.4", - "postcss": "^8.4.27", - "prettier": "^3.0.2", - "tailwind-config": "workspace:^", - "tailwindcss": "^3.4.1", - "ts-jest": "^29.4.4", - "tsconfig": "workspace:^", - "typescript": "^5.6.2" - }, - "pnpm": { - "overrides": { - "@types/react": "19.2.4" + "name": "@courselit/web", + "version": "0.73.10", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "prettier": "prettier --write **/*.ts" + }, + "browserslist": { + "production": [ + "chrome >= 109", + "edge >= 109", + "firefox >= 109", + "safari >= 15.4", + "ios_saf >= 15.4", + "not dead" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@better-auth/sso": "^1.5.6", + "@courselit/common-logic": "workspace:^", + "@courselit/common-models": "workspace:^", + "@courselit/components-library": "workspace:^", + "@courselit/email-editor": "workspace:^", + "@courselit/icons": "workspace:^", + "@courselit/orm-models": "workspace:^", + "@courselit/page-blocks": "workspace:^", + "@courselit/page-models": "workspace:^", + "@courselit/page-primitives": "workspace:^", + "@courselit/text-editor": "workspace:^", + "@courselit/utils": "workspace:^", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-alert-dialog": "^1.1.11", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.4", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-toggle": "^1.1.6", + "@radix-ui/react-toggle-group": "^1.1.7", + "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-visually-hidden": "^1.1.0", + "@stripe/stripe-js": "^5.4.0", + "@types/base-64": "^1.0.0", + "adm-zip": "^0.5.16", + "archiver": "^5.3.1", + "aws4": "^1.13.2", + "base-64": "^1.0.0", + "better-auth": "^1.5.6", + "chart.js": "^4.4.7", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "color-convert": "^3.1.0", + "cookie": "^0.4.2", + "date-fns": "^4.1.0", + "graphql": "^16.10.0", + "graphql-type-json": "^0.3.2", + "jsdom": "^26.1.0", + "lodash.debounce": "^4.0.8", + "lucide-react": "^0.553.0", + "medialit": "0.2.0", + "mongodb": "^6.15.0", + "mongoose": "^8.13.1", + "next": "^16.0.10", + "next-themes": "^0.4.6", + "nodemailer": "^6.7.2", + "pug": "^3.0.2", + "razorpay": "^2.9.4", + "react": "19.2.0", + "react-chartjs-2": "^5.3.0", + "react-csv": "^2.2.2", + "react-dom": "19.2.0", + "react-hook-form": "^7.54.1", + "recharts": "^2.15.1", + "remirror": "^3.0.1", + "sharp": "^0.33.2", + "slugify": "^1.6.5", + "sonner": "^2.0.7", + "stripe": "^17.5.0", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7", + "xml2js": "^0.6.2", + "zod": "^3.24.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@shelf/jest-mongodb": "^5.2.2", + "@types/adm-zip": "^0.5.7", + "@types/bcryptjs": "^2.4.2", + "@types/cookie": "^0.4.1", + "@types/mongodb": "^4.0.7", + "@types/node": "17.0.21", + "@types/nodemailer": "^6.4.4", + "@types/pug": "^2.0.6", + "@types/react": "19.2.4", + "@types/xml2js": "^0.4.14", + "eslint": "^9.12.0", + "eslint-config-next": "16.0.3", + "eslint-config-prettier": "^9.0.0", + "identity-obj-proxy": "^3.0.0", + "mongodb-memory-server": "^10.1.4", + "postcss": "^8.4.27", + "prettier": "^3.0.2", + "tailwind-config": "workspace:^", + "tailwindcss": "^3.4.1", + "ts-jest": "^29.4.4", + "tsconfig": "workspace:^", + "typescript": "^5.6.2" + }, + "pnpm": { + "overrides": { + "@types/react": "19.2.4" + } } - } } diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index a76893611..84f7f0d1e 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -4,6 +4,15 @@ import { auth } from "./auth"; export async function proxy(request: NextRequest) { const requestHeaders = request.headers; + const forwardedProto = request.headers.get("x-forwarded-proto"); + + if (!forwardedProto && request.nextUrl.protocol) { + requestHeaders.set( + "x-forwarded-proto", + request.nextUrl.protocol.replace(":", ""), + ); + } + const backend = await getBackendAddress(requestHeaders); if (request.nextUrl.pathname === "/healthy") { diff --git a/apps/web/ui-config/strings.ts b/apps/web/ui-config/strings.ts index 482738880..0c8a70a02 100644 --- a/apps/web/ui-config/strings.ts +++ b/apps/web/ui-config/strings.ts @@ -646,6 +646,11 @@ export const SINGLE_SIGN_ON_HEADER = "Single sign-on (SSO)"; export const LOGIN_METHODS_HEADER = "Login providers"; export const LOGIN_METHODS_CARD_DESCRIPTION = "Control how your users access the school"; +export const LOGIN_PROVIDER_EMAIL_LABEL = "Email"; +export const LOGIN_PROVIDER_GOOGLE_LABEL = "Google"; +export const LOGIN_PROVIDER_SSO_LABEL = "SAML SSO"; +export const LOGIN_PROVIDER_GOOGLE_BUTTON = "Continue with Google"; +export const LOGIN_PROVIDER_SSO_BUTTON = "Continue with SSO"; export const APIKEY_EXISTING_TABLE_HEADER_CREATED = "Created"; export const APIKEY_EXISTING_TABLE_HEADER_NAME = "Name"; export const APIKEY_NEW_HEADER = "New API key"; @@ -665,6 +670,24 @@ export const SSO_PROVIDER_CALLBACK_URL_LABEL = "Callback URL"; export const SSO_PROVIDER_IDP_METADATA_LABEL = "IDP Metadata"; export const SSO_PROVIDER_PROVIDER_ID_LABEL = "Provider ID"; export const SSO_PROVIDER_SUCCESS_MESSAGE = "SSO provider added successfully"; +export const GOOGLE_PROVIDER_HEADER = "Google"; +export const GOOGLE_PROVIDER_CARD_HEADER = "Google App Configuration"; +export const GOOGLE_PROVIDER_CARD_DESCRIPTION = + "Enter the values from your Google Cloud OAuth application"; +export const GOOGLE_PROVIDER_SETTINGS_HEADER = "School Settings"; +export const GOOGLE_PROVIDER_SETTINGS_DESCRIPTION = + "Use these values while configuring your Google OAuth application"; +export const GOOGLE_PROVIDER_CLIENT_ID_LABEL = "Client ID"; +export const GOOGLE_PROVIDER_CLIENT_SECRET_LABEL = "Client secret"; +export const GOOGLE_PROVIDER_REDIRECT_URI_LABEL = "Authorized redirect URI"; +export const GOOGLE_PROVIDER_ORIGIN_LABEL = "Authorized JavaScript origin"; +export const GOOGLE_PROVIDER_SUCCESS_MESSAGE = + "Google provider saved successfully"; +export const GOOGLE_PROVIDER_REMOVE_DIALOG_HEADER = "Remove Google Provider"; +export const GOOGLE_PROVIDER_SECRET_HELPER = + "Leave blank to keep the current client secret."; +export const GOOGLE_PROVIDER_SECRET_SAVED = "A client secret is already saved."; +export const URL_COPIED_TO_CLIPBOARD = "URL copied to clipboard"; export const PROVIDER_RESET_SUCCESS_MESSAGE = "Provider reset successfully"; export const APIKEY_NEW_BTN_CAPTION = "Create"; export const APIKEY_NEW_GENERATED_KEY_HEADER = "Your new API key"; @@ -795,3 +818,4 @@ export const LESSON_EMBED_URL_LABEL = "Embed code"; export const LESSON_CONTENT_LABEL = "Content"; export const EMAIL_EDITOR_EMAIL_EDIT_HEADER = "Editing email"; export const EMAIL_EDITOR_TEMPLATE_EDIT_HEADER = "Editing template"; +export const LOGIN_FORM_PERSONAL_INFORMATION_LABEL = "Personal Information"; diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml index b744bdaad..ec5a5b4d8 100644 --- a/deployment/docker/docker-compose.yml +++ b/deployment/docker/docker-compose.yml @@ -20,6 +20,8 @@ services: # In production, replace the following with the connection string of a cloud # hosted instance of MongoDB. - DB_CONNECTION_STRING=mongodb://root:example@mongo/courselit?authSource=admin + # Disable DB transactions for standalone mongo in compose (opt-out) + - DB_TRANSACTIONS=false # CourseLit uses Magic links to provide login functionality. Hence, it requires # access to the mail sending facility. diff --git a/packages/page-blocks/src/blocks/header/widget/index.tsx b/packages/page-blocks/src/blocks/header/widget/index.tsx index fec0e5c4a..59e67c8de 100644 --- a/packages/page-blocks/src/blocks/header/widget/index.tsx +++ b/packages/page-blocks/src/blocks/header/widget/index.tsx @@ -26,6 +26,7 @@ import clsx from "clsx"; import { ThemeStyle } from "@courselit/page-models"; import { Moon, Sun } from "lucide-react"; import { useGithubStars } from "./use-github-stars"; +import { usePathname } from "next/navigation"; export default function Widget({ state, @@ -39,11 +40,17 @@ export default function Widget({ settings.maxWidth || theme.theme.structure.page.width; overiddenTheme.structure.section.padding.y = "py-4"; const [isClient, setIsClient] = useState(false); + const [userMenuKey, setUserMenuKey] = useState(0); + const pathname = usePathname(); useEffect(() => { setIsClient(true); }, []); + useEffect(() => { + setUserMenuKey((current) => current + 1); + }, [pathname]); + const linkClasses = "flex w-full"; const linkAlignment = settings.linkAlignment || defaultLinkAlignment; const spacingBetweenLinks = @@ -61,6 +68,7 @@ export default function Widget({ const cardBorderWidth = overiddenTheme?.interactives?.card?.border?.width?.split("-")[1]; const isLayoutFixed = settings.layout === "fixed" || !settings.layout; + const closeUserMenu = () => setUserMenuKey((current) => current + 1); const mainContent = (
@@ -162,6 +170,7 @@ export default function Widget({ {settings.showLoginControl && (
{!state.auth.guest && ( - + )} - + =6.9.0'} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.26.8': @@ -1528,8 +1528,8 @@ packages: resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.25.9': @@ -1548,8 +1548,8 @@ packages: resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} '@babel/helper-module-transforms@7.26.0': @@ -1591,8 +1591,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true @@ -1720,62 +1720,119 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} '@babel/template@7.27.0': resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} '@babel/traverse@7.27.0': resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.5': - resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} '@babel/types@7.27.0': resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@better-auth/core@1.4.1': - resolution: {integrity: sha512-N4kyRdA472WGLoCjsJpUeYdZZvpoBDgP65hUeQQxTQYwBTqD9O17Tokax9CdNbkb4g34sTfxaJCfcncE3Hy4SA==} + '@better-auth/core@1.5.6': + resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==} peerDependencies: - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - better-call: 1.1.0 + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' + '@opentelemetry/api': ^1.9.0 + better-call: 1.3.2 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + '@better-auth/drizzle-adapter@1.5.6': + resolution: {integrity: sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + drizzle-orm: '>=0.41.0' + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.5.6': + resolution: {integrity: sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + kysely: ^0.27.0 || ^0.28.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.5.6': + resolution: {integrity: sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + + '@better-auth/mongo-adapter@1.5.6': + resolution: {integrity: sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true - '@better-auth/sso@1.4.6': - resolution: {integrity: sha512-E6ZQLE/tunc9goQd2MEWm/W8w15i+5KmZ2yt4ngyRAQX2zHaPOOOkOMO7YAEV23f3z65hcpqinvXpkZp+Lpddg==} + '@better-auth/prisma-adapter@1.5.6': + resolution: {integrity: sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==} peerDependencies: - better-auth: 1.4.6 + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/sso@1.5.6': + resolution: {integrity: sha512-R4mC3Bj9Xy4pBz+XNjs4Z7gpukmAUc7mIgIhg0zmN7R7rjj8Uz9sBBRcf6b/+6fFyXe/mZ0f+Sc3uBaYt4hRaQ==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': 0.3.1 + better-auth: 1.5.6 + better-call: 1.3.2 - '@better-auth/telemetry@1.4.1': - resolution: {integrity: sha512-yNeazXYvMbyuCe1AA6tYWsJEKgcS7gF9PmmACmrPVhVBe1ncDhVfWMZ++YCmA2h8hjkR9755ZyofiYRPbj+kXQ==} + '@better-auth/telemetry@1.5.6': + resolution: {integrity: sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==} peerDependencies: - '@better-auth/core': 1.4.1 + '@better-auth/core': 1.5.6 - '@better-auth/utils@0.3.0': - resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + '@better-auth/utils@0.3.1': + resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} - '@better-fetch/fetch@1.1.18': - resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} '@changesets/apply-release-plan@7.0.13': resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} @@ -2410,6 +2467,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2422,6 +2485,10 @@ packages: resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2438,8 +2505,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@8.57.1': @@ -2450,8 +2517,8 @@ packages: resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -2465,14 +2532,14 @@ packages: '@floating-ui/core@1.6.9': resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} - '@floating-ui/core@1.7.3': - resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} '@floating-ui/dom@1.6.13': resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} - '@floating-ui/dom@1.7.4': - resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} '@floating-ui/react-dom@2.1.2': resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} @@ -2480,8 +2547,8 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react-dom@2.1.6': - resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -2492,8 +2559,8 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} @@ -3201,8 +3268,8 @@ packages: cpu: [x64] os: [win32] - '@noble/ciphers@2.0.1': - resolution: {integrity: sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==} + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} engines: {node: '>= 20.19.0'} '@noble/hashes@2.0.1': @@ -5196,8 +5263,8 @@ packages: resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} engines: {node: '>=18.0.0'} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@stripe/stripe-js@5.10.0': resolution: {integrity: sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==} @@ -5946,8 +6013,8 @@ packages: resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==} engines: {node: '>= 16'} - '@xmldom/xmldom@0.8.11': - resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + '@xmldom/xmldom@0.8.12': + resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} engines: {node: '>=10.0.0'} a11y-status@2.0.2: @@ -6012,6 +6079,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + algoliasearch@5.23.4: resolution: {integrity: sha512-QzAKFHl3fm53s44VHrTdEo0TkpL3XVUYQpnZy1r6/EHvMAyIg+O4hwprzlsNmcCHTNyVcF2S13DAUn7XhkC6qg==} engines: {node: '>= 14.0.0'} @@ -6267,24 +6337,55 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true - better-auth@1.4.1: - resolution: {integrity: sha512-HDVE69Nw6Y1FPTcmFEmPolfsjMfVB5U823Ij9yWBoM8MdHZ2lA3JVus4xQJ2oRE1riJTlcSLFcgJKWGD7V7hmw==} + better-auth@1.5.6: + resolution: {integrity: sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==} peerDependencies: '@lynx-js/react': '*' - '@sveltejs/kit': '*' - next: '*' - react: '*' - react-dom: '*' - solid-js: '*' - svelte: '*' - vue: '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': optional: true + '@prisma/client': + optional: true '@sveltejs/kit': optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true next: optional: true + pg: + optional: true + prisma: + optional: true react: optional: true react-dom: @@ -6293,11 +6394,18 @@ packages: optional: true svelte: optional: true + vitest: + optional: true vue: optional: true - better-call@1.1.0: - resolution: {integrity: sha512-7CecYG+yN8J1uBJni/Mpjryp8bW/YySYsrGEWgFe048ORASjq17keGjbKI2kHEOSc6u8pi11UxzkJ7jIovQw6w==} + better-call@1.3.2: + resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -6331,6 +6439,9 @@ packages: brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -6900,8 +7011,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.6: + resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -7463,8 +7574,8 @@ packages: jiti: optional: true - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -7490,6 +7601,10 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -7594,12 +7709,15 @@ packages: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-parser@4.4.1: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true - fast-xml-parser@5.3.2: - resolution: {integrity: sha512-n8v8b6p4Z1sMgqRmqLJm3awW4NX7NkaKPfb3uJIBTSH7Pdvufi3PQ3/lJLQrvxcMYl7JI2jnDO90siPEpD8JBA==} + fast-xml-parser@5.5.9: + resolution: {integrity: sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==} hasBin: true fastq@1.19.1: @@ -8527,8 +8645,8 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jose@6.1.0: - resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -8650,8 +8768,8 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - kysely@0.28.8: - resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==} + kysely@0.28.15: + resolution: {integrity: sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==} engines: {node: '>=20.0.0'} language-subtag-registry@0.3.23: @@ -8727,8 +8845,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.22: - resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} lodash._baseiteratee@4.7.0: resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} @@ -8811,6 +8929,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + log-symbols@5.1.0: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} @@ -9122,6 +9243,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -9256,8 +9380,8 @@ packages: engines: {node: ^18 || >=20} hasBin: true - nanostores@1.1.0: - resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} + nanostores@1.2.0: + resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==} engines: {node: ^20.0.0 || >=22.0.0} napi-postinstall@0.1.6: @@ -9343,10 +9467,6 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-forge@1.3.3: - resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} - engines: {node: '>= 6.13.0'} - node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -9510,9 +9630,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -9561,6 +9678,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.2.0: + resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -9817,8 +9938,8 @@ packages: prosemirror-gapcursor@1.3.2: resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} - prosemirror-gapcursor@1.4.0: - resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} prosemirror-history@1.4.1: resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} @@ -9882,8 +10003,8 @@ packages: prosemirror-tables@1.7.1: resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==} - prosemirror-tables@1.8.3: - resolution: {integrity: sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==} + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} prosemirror-trailing-node@3.0.0: resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} @@ -9895,14 +10016,14 @@ packages: prosemirror-transform@1.10.4: resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} - prosemirror-transform@1.10.5: - resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==} + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} prosemirror-view@1.39.2: resolution: {integrity: sha512-BmOkml0QWNob165gyUxXi5K5CVUgVPpqMEAAml/qzgKn9boLUWVPzQ6LtzXw8Cn1GtRQX4ELumPxqtLTDaAKtg==} - prosemirror-view@1.41.4: - resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==} + prosemirror-view@1.41.7: + resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==} protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} @@ -10317,8 +10438,8 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - rou3@0.5.1: - resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} round-precision@1.0.0: resolution: {integrity: sha512-L2a0XDSNeaaBTEGmzuENMK4T8c0HqKYeS3pCDurW4MRPo8O6LeCLqVPWUt5+xW9rrEcG9QaYrAFcApEFXKziyw==} @@ -10368,8 +10489,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - samlify@2.10.2: - resolution: {integrity: sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA==} + samlify@2.12.0: + resolution: {integrity: sha512-ewGsHyY4kInDH0BfprlAZ1rHpH1jBmbqYiXDbuI3t1Y8h71gqEt4Z7jdCFyPHFR8jItJkbdckTijUZGg14CDlg==} sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} @@ -10432,8 +10553,8 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -10712,8 +10833,8 @@ packages: strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} - strnum@2.1.1: - resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + strnum@2.2.2: + resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} @@ -10769,8 +10890,8 @@ packages: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} engines: {node: ^14.18.0 || >=16.0.0} - tabbable@6.3.0: - resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} @@ -11249,10 +11370,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -11534,6 +11651,10 @@ packages: resolution: {integrity: sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==} engines: {node: '>=0.6.0'} + xpath@0.0.34: + resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} + engines: {node: '>=0.6.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -11549,6 +11670,10 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + yaml@2.7.1: resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} @@ -11590,8 +11715,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11847,7 +11972,7 @@ snapshots: '@authenio/xml-encryption@2.0.2': dependencies: - '@xmldom/xmldom': 0.8.11 + '@xmldom/xmldom': 0.8.12 escape-html: 1.0.3 xpath: 0.0.32 @@ -12259,7 +12384,7 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 @@ -12296,10 +12421,10 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/generator@7.28.5': + '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -12327,10 +12452,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.27.1': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color optional: true @@ -12367,9 +12492,9 @@ snapshots: dependencies: '@babel/types': 7.27.0 - '@babel/parser@7.28.5': + '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 optional: true '@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.26.10)': @@ -12496,7 +12621,7 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.2': {} '@babel/template@7.27.0': dependencies: @@ -12504,11 +12629,11 @@ snapshots: '@babel/parser': 7.27.0 '@babel/types': 7.27.0 - '@babel/template@7.27.2': + '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 optional: true '@babel/traverse@7.27.0': @@ -12523,14 +12648,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.5': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12541,7 +12666,7 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.28.5': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 @@ -12549,35 +12674,70 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@better-auth/core@1.4.1(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.1.0)': + '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0)': + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.2(zod@3.24.3) + jose: 6.2.2 + kysely: 0.28.15 + nanostores: 1.2.0 + zod: 4.3.6 + + '@better-auth/drizzle-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - '@standard-schema/spec': 1.0.0 - better-call: 1.1.0 - jose: 6.1.0 - kysely: 0.28.8 - nanostores: 1.1.0 - zod: 4.1.12 + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 - '@better-auth/sso@1.4.6(better-auth@1.4.1(next@16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': + '@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.15)': dependencies: - '@better-fetch/fetch': 1.1.18 - better-auth: 1.4.1(next@16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - fast-xml-parser: 5.3.2 - jose: 6.1.0 - samlify: 2.10.2 - zod: 4.1.12 + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + optionalDependencies: + kysely: 0.28.15 - '@better-auth/telemetry@1.4.1(@better-auth/core@1.4.1(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.1.0))': + '@better-auth/memory-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/core': 1.4.1(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.1.0) - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 - '@better-auth/utils@0.3.0': {} + '@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4))': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + optionalDependencies: + mongodb: 6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) - '@better-fetch/fetch@1.1.18': {} + '@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/sso@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(@opentelemetry/api@1.9.0)(mongodb@6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4))(next@16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(better-call@1.3.2(zod@3.24.3))': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + better-auth: 1.5.6(@opentelemetry/api@1.9.0)(mongodb@6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4))(next@16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + better-call: 1.3.2(zod@3.24.3) + fast-xml-parser: 5.5.9 + jose: 6.2.2 + samlify: 2.12.0 + tldts: 6.1.86 + zod: 4.3.6 + + '@better-auth/telemetry@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.1': {} + + '@better-fetch/fetch@1.1.21': {} '@changesets/apply-release-plan@7.0.13': dependencies: @@ -12851,8 +13011,8 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: - '@babel/helper-module-imports': 7.27.1 - '@babel/runtime': 7.28.4 + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.29.2 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -13147,9 +13307,14 @@ snapshots: eslint: 9.39.1(jiti@1.21.7) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.4(jiti@1.21.7))': dependencies: - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.4(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': + dependencies: + eslint: 9.39.4(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -13164,6 +13329,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 @@ -13200,16 +13373,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/eslintrc@3.3.3': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -13218,7 +13391,7 @@ snapshots: '@eslint/js@9.39.1': {} - '@eslint/js@9.39.2': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} @@ -13231,9 +13404,9 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.9 - '@floating-ui/core@1.7.3': + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/utils': 0.2.11 optional: true '@floating-ui/dom@1.6.13': @@ -13241,10 +13414,10 @@ snapshots: '@floating-ui/core': 1.6.9 '@floating-ui/utils': 0.2.9 - '@floating-ui/dom@1.7.4': + '@floating-ui/dom@1.7.6': dependencies: - '@floating-ui/core': 1.7.3 - '@floating-ui/utils': 0.2.10 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 optional: true '@floating-ui/react-dom@2.1.2(react-dom@19.2.0(react@18.3.1))(react@18.3.1)': @@ -13259,23 +13432,23 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@floating-ui/dom': 1.7.4 + '@floating-ui/dom': 1.7.6 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optional: true '@floating-ui/react@0.24.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) aria-hidden: 1.2.6 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - tabbable: 6.3.0 + tabbable: 6.4.0 optional: true - '@floating-ui/utils@0.2.10': + '@floating-ui/utils@0.2.11': optional: true '@floating-ui/utils@0.2.9': {} @@ -13991,7 +14164,7 @@ snapshots: '@next/swc-win32-x64-msvc@16.1.6': optional: true - '@noble/ciphers@2.0.1': {} + '@noble/ciphers@2.1.1': {} '@noble/hashes@2.0.1': {} @@ -16413,7 +16586,7 @@ snapshots: '@remirror/extension-react-component@3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/core-constants': 3.0.0 '@remirror/core-helpers': 4.0.0 @@ -16434,7 +16607,7 @@ snapshots: '@remirror/extension-react-tables@3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@emotion/css': 11.13.5 '@linaria/core': 4.2.10 '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) @@ -16602,25 +16775,25 @@ snapshots: '@remirror/pm@3.0.1': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core-constants': 3.0.0 '@remirror/core-helpers': 4.0.0 prosemirror-collab: 1.3.1 prosemirror-commands: 1.7.1 prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.4.0 + prosemirror-gapcursor: 1.4.1 prosemirror-history: 1.5.0 prosemirror-inputrules: 1.5.1 prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 - prosemirror-paste-rules: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4) + prosemirror-paste-rules: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 - prosemirror-suggest: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4) - prosemirror-tables: 1.8.3 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4) - prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.4 + prosemirror-suggest: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.7 '@remirror/preset-core@3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0)': dependencies: @@ -16665,7 +16838,7 @@ snapshots: '@remirror/preset-react@3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/extension-placeholder': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/extension-react-component': 3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -16716,7 +16889,7 @@ snapshots: '@remirror/react-components@3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@floating-ui/react': 0.24.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/extension-positioner': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) @@ -16746,7 +16919,7 @@ snapshots: '@remirror/react-core@3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/extension-positioner': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/extension-react-component': 3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -16773,7 +16946,7 @@ snapshots: '@remirror/react-hooks@3.0.3(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react-dom@18.3.0)(@types/react@18.3.7)(jsdom@26.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) '@remirror/core-constants': 3.0.0 '@remirror/core-helpers': 4.0.0 @@ -16802,7 +16975,7 @@ snapshots: '@remirror/react-renderer@3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(@types/react@18.3.7)(jsdom@26.1.0)(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core': 3.0.2(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0) react: 19.2.0 optionalDependencies: @@ -16815,7 +16988,7 @@ snapshots: '@remirror/react-utils@3.0.0(@remirror/pm@3.0.1)(@types/react@18.3.7)(react@19.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core-constants': 3.0.0 '@remirror/core-helpers': 4.0.0 '@remirror/core-types': 3.0.0(@remirror/pm@3.0.1) @@ -17255,7 +17428,7 @@ snapshots: tslib: 2.8.1 optional: true - '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@stripe/stripe-js@5.10.0': {} @@ -17943,15 +18116,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.4 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.4(jiti@1.21.7) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -18022,14 +18195,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.46.4 '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.1 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.4(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18130,13 +18303,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.1 - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.4(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -18263,13 +18436,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.46.4 '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) + eslint: 9.39.4(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -18351,11 +18524,11 @@ snapshots: '@xmldom/is-dom-node@1.0.1': {} - '@xmldom/xmldom@0.8.11': {} + '@xmldom/xmldom@0.8.12': {} a11y-status@2.0.2: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@types/throttle-debounce': 2.1.0 throttle-debounce: 3.0.1 optional: true @@ -18415,6 +18588,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + algoliasearch@5.23.4: dependencies: '@algolia/client-abtesting': 5.23.4 @@ -18767,7 +18947,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 cosmiconfig: 7.1.0 resolve: 1.22.11 optional: true @@ -18822,32 +19002,51 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.1(next@16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): - dependencies: - '@better-auth/core': 1.4.1(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.1(@better-auth/core@1.4.1(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.1.0)) - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - '@noble/ciphers': 2.0.1 + better-auth@1.5.6(@opentelemetry/api@1.9.0)(mongodb@6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4))(next@16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0) + '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.15) + '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)) + '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.24.3))(jose@6.2.2)(kysely@0.28.15)(nanostores@1.2.0)) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - '@standard-schema/spec': 1.0.0 - better-call: 1.1.0 - defu: 6.1.4 - jose: 6.1.0 - kysely: 0.28.8 - nanostores: 1.1.0 - zod: 4.1.12 + better-call: 1.3.2(zod@4.3.6) + defu: 6.1.6 + jose: 6.2.2 + kysely: 0.28.15 + nanostores: 1.2.0 + zod: 4.3.6 optionalDependencies: + mongodb: 6.16.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) next: 16.1.6(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/api' - better-call@1.1.0: + better-call@1.3.2(zod@3.24.3): dependencies: - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - rou3: 0.5.1 - set-cookie-parser: 2.7.2 + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 3.24.3 + + better-call@1.3.2(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 4.3.6 better-path-resolve@1.0.0: dependencies: @@ -18905,6 +19104,11 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 @@ -19217,7 +19421,7 @@ snapshots: import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 - yaml: 1.10.2 + yaml: 1.10.3 optional: true crc-32@1.2.2: {} @@ -19229,7 +19433,7 @@ snapshots: create-context-state@2.0.3(@types/react@18.3.7)(react@19.2.0): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 react: 19.2.0 optionalDependencies: '@types/react': 18.3.7 @@ -19440,7 +19644,7 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} + defu@6.1.6: {} delayed-stream@1.0.0: {} @@ -20162,21 +20366,21 @@ snapshots: transitivePeerDependencies: - supports-color - eslint@9.39.2(jiti@1.21.7): + eslint@9.39.4(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 + '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -20184,7 +20388,7 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -20195,7 +20399,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -20221,6 +20425,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -20370,14 +20578,20 @@ snapshots: fast-redact@3.5.0: {} + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.2.0 + fast-xml-parser@4.4.1: dependencies: strnum: 1.1.2 optional: true - fast-xml-parser@5.3.2: + fast-xml-parser@5.5.9: dependencies: - strnum: 2.1.1 + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.2.0 + strnum: 2.2.2 fastq@1.19.1: dependencies: @@ -21680,7 +21894,7 @@ snapshots: jiti@1.21.7: {} - jose@6.1.0: {} + jose@6.2.2: {} joycon@3.1.1: {} @@ -21839,7 +22053,7 @@ snapshots: kleur@4.1.5: {} - kysely@0.28.8: {} + kysely@0.28.15: {} language-subtag-registry@0.3.23: {} @@ -21922,7 +22136,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.22: + lodash-es@4.17.23: optional: true lodash._baseiteratee@4.7.0: @@ -21989,6 +22203,9 @@ snapshots: lodash@4.17.21: {} + lodash@4.17.23: + optional: true + log-symbols@5.1.0: dependencies: chalk: 5.4.1 @@ -22496,6 +22713,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -22637,7 +22858,7 @@ snapshots: multishift@2.0.10(@remirror/pm@3.0.1)(@types/react@18.3.7)(react@19.2.0): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core-helpers': 4.0.0 '@remirror/core-types': 3.0.0(@remirror/pm@3.0.1) '@seznam/compose-react-refs': 1.0.6 @@ -22666,7 +22887,7 @@ snapshots: nanoid@5.1.5: {} - nanostores@1.1.0: {} + nanostores@1.2.0: {} napi-postinstall@0.1.6: {} @@ -22774,8 +22995,6 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-forge@1.3.3: {} - node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 @@ -22946,8 +23165,6 @@ snapshots: dependencies: quansync: 0.2.10 - pako@1.0.11: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -23007,6 +23224,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.2.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -23322,12 +23541,12 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-view: 1.39.2 - prosemirror-gapcursor@1.4.0: + prosemirror-gapcursor@1.4.1: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.4 + prosemirror-view: 1.41.7 prosemirror-history@1.4.1: dependencies: @@ -23339,8 +23558,8 @@ snapshots: prosemirror-history@1.5.0: dependencies: prosemirror-state: 1.4.4 - prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.7 rope-sequence: 1.3.4 prosemirror-inputrules@1.5.0: @@ -23351,7 +23570,7 @@ snapshots: prosemirror-inputrules@1.5.1: dependencies: prosemirror-state: 1.4.4 - prosemirror-transform: 1.10.5 + prosemirror-transform: 1.12.0 prosemirror-keymap@1.2.2: dependencies: @@ -23384,15 +23603,15 @@ snapshots: dependencies: orderedmap: 2.1.1 - prosemirror-paste-rules@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4): + prosemirror-paste-rules@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core-constants': 3.0.0 '@remirror/core-helpers': 4.0.0 escape-string-regexp: 4.0.0 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.4 + prosemirror-view: 1.41.7 prosemirror-resizable-view@3.0.0(@remirror/pm@3.0.1)(@types/node@17.0.21)(jsdom@26.1.0): dependencies: @@ -23425,19 +23644,19 @@ snapshots: prosemirror-state@1.4.4: dependencies: prosemirror-model: 1.25.4 - prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.7 - prosemirror-suggest@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4): + prosemirror-suggest@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@remirror/core-constants': 3.0.0 '@remirror/core-helpers': 4.0.0 '@remirror/types': 2.0.0 escape-string-regexp: 4.0.0 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.4 + prosemirror-view: 1.41.7 prosemirror-tables@1.7.1: dependencies: @@ -23447,13 +23666,13 @@ snapshots: prosemirror-transform: 1.10.4 prosemirror-view: 1.39.2 - prosemirror-tables@1.8.3: + prosemirror-tables@1.8.5: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.7 prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.39.2): dependencies: @@ -23463,19 +23682,19 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-view: 1.39.2 - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4): + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): dependencies: '@remirror/core-constants': 3.0.0 escape-string-regexp: 4.0.0 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.4 + prosemirror-view: 1.41.7 prosemirror-transform@1.10.4: dependencies: prosemirror-model: 1.25.1 - prosemirror-transform@1.10.5: + prosemirror-transform@1.12.0: dependencies: prosemirror-model: 1.25.4 @@ -23485,11 +23704,11 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view@1.41.4: + prosemirror-view@1.41.7: dependencies: prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-transform: 1.10.5 + prosemirror-transform: 1.12.0 protobufjs@7.5.4: dependencies: @@ -23629,8 +23848,8 @@ snapshots: react-color@2.19.3(react@19.2.0): dependencies: '@icons/material': 0.2.4(react@19.2.0) - lodash: 4.17.21 - lodash-es: 4.17.22 + lodash: 4.17.23 + lodash-es: 4.17.23 material-colors: 1.2.6 prop-types: 15.8.1 react: 19.2.0 @@ -23755,7 +23974,7 @@ snapshots: reactcss@1.2.3(react@19.2.0): dependencies: - lodash: 4.17.21 + lodash: 4.17.23 react: 19.2.0 optional: true @@ -24143,7 +24362,7 @@ snapshots: rope-sequence@1.3.4: {} - rou3@0.5.1: {} + rou3@0.7.12: {} round-precision@1.0.0: dependencies: @@ -24196,19 +24415,15 @@ snapshots: safer-buffer@2.1.2: {} - samlify@2.10.2: + samlify@2.12.0: dependencies: '@authenio/xml-encryption': 2.0.2 - '@xmldom/xmldom': 0.8.11 - camelcase: 6.3.0 - node-forge: 1.3.3 + '@xmldom/xmldom': 0.8.12 node-rsa: 1.1.1 - pako: 1.0.11 - uuid: 8.3.2 xml: 1.0.1 xml-crypto: 6.1.2 xml-escape: 1.1.0 - xpath: 0.0.32 + xpath: 0.0.34 sass-formatter@0.7.9: dependencies: @@ -24278,7 +24493,7 @@ snapshots: transitivePeerDependencies: - supports-color - set-cookie-parser@2.7.2: {} + set-cookie-parser@3.1.0: {} set-function-length@1.2.2: dependencies: @@ -24665,7 +24880,7 @@ snapshots: strnum@1.1.2: optional: true - strnum@2.1.1: {} + strnum@2.2.2: {} styled-jsx@5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@18.3.1): dependencies: @@ -24736,7 +24951,7 @@ snapshots: '@pkgr/core': 0.1.2 tslib: 2.8.1 - tabbable@6.3.0: + tabbable@6.4.0: optional: true tailwind-merge@2.6.0: {} @@ -25391,13 +25606,13 @@ snapshots: transitivePeerDependencies: - supports-color - typescript-eslint@8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -25604,8 +25819,6 @@ snapshots: uuid@11.1.0: {} - uuid@8.3.2: {} - uuid@9.0.1: {} uvu@0.5.6: @@ -25870,7 +26083,7 @@ snapshots: xml-crypto@6.1.2: dependencies: '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.8.11 + '@xmldom/xmldom': 0.8.12 xpath: 0.0.33 xml-escape@1.1.0: {} @@ -25894,6 +26107,8 @@ snapshots: xpath@0.0.33: {} + xpath@0.0.34: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -25902,6 +26117,9 @@ snapshots: yaml@1.10.2: {} + yaml@1.10.3: + optional: true + yaml@2.7.1: {} yargs-parser@21.1.1: {} @@ -25939,6 +26157,6 @@ snapshots: zod@3.25.76: {} - zod@4.1.12: {} + zod@4.3.6: {} zwitch@2.0.4: {}