import 'server-only'; import { decodeJwt } from 'jose'; import { revalidatePath, revalidateTag } from 'next/cache'; import { cookies, headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { WORKOS_COOKIE_NAME } from './env-variables.js'; import { getCookieOptions, getPKCECookieOptions } from './cookie.js'; import { getAuthorizationUrl } from './get-authorization-url.js'; import type { AccessToken, GetAuthURLOptions, SwitchToOrganizationOptions, UserInfo } from './interfaces.js'; import { PKCE_COOKIE_NAME, setPKCECookie } from './pkce.js'; import { getSessionFromCookie, refreshSession, withAuth } from './session.js'; import { getWorkOS } from './workos.js'; /** * A wrapper around revalidateTag to provide compatibility with previous versions. * @param tag The tag to revalidate. */ function revalidateTagCompat(tag: string): void { const fn = revalidateTag as (tag: string, profile: string) => void; return fn(tag, 'max'); } async function getAuthURLAndSetPKCECookie(options: GetAuthURLOptions): Promise { const { url, sealedState } = await getAuthorizationUrl(options); await setPKCECookie(sealedState); return url; } type GetSignUrlOptions = Omit & { returnTo?: string; }; export async function getSignInUrl(authUrlOptions: GetSignUrlOptions = {}) { return getAuthURLAndSetPKCECookie({ ...authUrlOptions, returnPathname: authUrlOptions.returnTo, screenHint: 'sign-in', }); } export async function getSignUpUrl(authUrlOptions: GetSignUrlOptions = {}) { return getAuthURLAndSetPKCECookie({ ...authUrlOptions, returnPathname: authUrlOptions.returnTo, screenHint: 'sign-up', }); } /** * Sign out the user and delete the session cookie. * @param options Options for signing out. * @param options.returnTo The URL to redirect to after signing out. */ export async function signOut({ returnTo }: { returnTo?: string } = {}) { let sessionId: string | undefined; try { const { sessionId: sid } = await withAuth(); sessionId = sid; } catch (error) { // Fall back to reading session directly from cookie when middleware isn't available const session = await getSessionFromCookie(); if (session && session.accessToken) { const { sid } = decodeJwt(session.accessToken); sessionId = sid; } else { // can't recover - throw the original error. throw error; } } finally { const nextCookies = await cookies(); const cookieName = WORKOS_COOKIE_NAME || 'wos-session'; const { domain, path, sameSite, secure } = getCookieOptions(); try { nextCookies.delete({ name: cookieName, domain, path, sameSite, secure }); } catch { // Some environments (e.g., vinext) only accept a string cookie name nextCookies.delete(cookieName); } // Clear any lingering PKCE verifier cookies so orphans from abandoned // flows don't accumulate toward HTTP 431 or confuse future sign-ins. const pkceOptions = getPKCECookieOptions(); for (const { name } of nextCookies.getAll()) { if (!name.startsWith(PKCE_COOKIE_NAME)) continue; try { nextCookies.delete({ name, domain: pkceOptions.domain, path: pkceOptions.path, sameSite: pkceOptions.sameSite, secure: pkceOptions.secure, }); } catch { nextCookies.delete(name); } } if (sessionId) { redirect(getWorkOS().userManagement.getLogoutUrl({ sessionId, returnTo })); } else { redirect(returnTo ?? '/'); } } } export async function switchToOrganization( organizationId: string, options: SwitchToOrganizationOptions = {}, ): Promise { const { returnTo, revalidationStrategy = 'path', revalidationTags = [] } = options; const headersList = await headers(); let result: UserInfo; // istanbul ignore next const pathname = returnTo || headersList.get('x-url') || '/'; try { result = await refreshSession({ organizationId, ensureSignedIn: true }); } catch ( // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any ) { const { cause } = error; /* istanbul ignore next */ if (cause?.rawData?.authkit_redirect_url) { redirect(cause.rawData.authkit_redirect_url); } else { if (cause?.error === 'sso_required' || cause?.error === 'mfa_enrollment') { return redirect(await getAuthURLAndSetPKCECookie({ organizationId })); } throw error; } } try { switch (revalidationStrategy) { case 'path': revalidatePath(pathname); break; case 'tag': for (const tag of revalidationTags) { revalidateTagCompat(tag); } break; } } catch { // revalidatePath/revalidateTag may not be available in non-Next.js environments (e.g., vinext) } if (revalidationStrategy !== 'none') { redirect(pathname); } return result; }