From ba87fc61888f5cf331babf21508619ca6b9d97db Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:43:33 -0300 Subject: [PATCH 01/17] Add `pending` to `SessionStatus` union --- packages/types/src/session.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 32f18a93260..9b0df6dd01f 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -136,6 +136,16 @@ export interface ActiveSessionResource extends SessionResource { user: UserResource; } +export interface PendingSessionResource extends SessionResource { + status: 'pending'; + user: UserResource; +} + +// todo: figure out better naming +// the goal here is just to encapsulate those both sessions so we don't keep repeating +// the same union everyone for the same purpose: the user has successfully verified / authenticated themselves +export type AuthenticatedSession = ActiveSessionResource | PendingSessionResource; + export interface SessionWithActivitiesResource extends ClerkResource { id: string; status: string; @@ -159,7 +169,15 @@ export interface SessionActivity { isMobile?: boolean; } -export type SessionStatus = 'abandoned' | 'active' | 'ended' | 'expired' | 'removed' | 'replaced' | 'revoked'; +export type SessionStatus = + | 'abandoned' + | 'active' + | 'ended' + | 'expired' + | 'removed' + | 'replaced' + | 'revoked' + | 'pending'; export interface PublicUserData { firstName: string | null; From 918bcd10fdc7a694be250220a75b4be6862d8813 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:57:33 -0300 Subject: [PATCH 02/17] Plug pending session into main Client resource --- integration/testUtils/handshake.ts | 1 + packages/astro/src/stores/internal.ts | 4 +-- .../clerk-js/src/core/__tests__/clerk.test.ts | 2 ++ packages/clerk-js/src/core/clerk.ts | 30 +++++++++---------- .../clerk-js/src/core/resources/Client.ts | 19 +++++++----- .../ImpersonationFab/ImpersonationFab.tsx | 4 +-- .../components/UserButton/SessionActions.tsx | 14 ++++----- .../UserButton/UserButtonPopover.tsx | 4 +-- .../UserButton/useMultisessionActions.tsx | 10 +++---- .../src/ui/hooks/useMultipleSessions.ts | 12 ++++---- packages/react/src/isomorphicClerk.ts | 4 +-- packages/shared/src/deriveState.ts | 4 +-- packages/shared/src/react/contexts.tsx | 4 +-- packages/types/src/clerk.ts | 18 +++++------ packages/types/src/client.ts | 3 +- packages/types/src/hooks.ts | 4 +-- packages/types/src/session.ts | 17 ++++++++--- packages/vue/src/types.ts | 4 +-- 18 files changed, 87 insertions(+), 71 deletions(-) diff --git a/integration/testUtils/handshake.ts b/integration/testUtils/handshake.ts index 74c4cea2026..d4656b6744a 100644 --- a/integration/testUtils/handshake.ts +++ b/integration/testUtils/handshake.ts @@ -108,6 +108,7 @@ export function generateConfig({ mode, matchedKeys = true }: { mode: 'test' | 'l state, extraClaims, }: { + // todo -> add tests for pending state: 'active' | 'expired' | 'early'; extraClaims?: Map; }) => { diff --git a/packages/astro/src/stores/internal.ts b/packages/astro/src/stores/internal.ts index d9ea37d6902..ab9723642f4 100644 --- a/packages/astro/src/stores/internal.ts +++ b/packages/astro/src/stores/internal.ts @@ -1,5 +1,5 @@ import type { - ActiveSessionResource, + AuthenticatedSessionResource, Clerk, ClientResource, InitialState, @@ -12,7 +12,7 @@ export const $csrState = map<{ isLoaded: boolean; client: ClientResource | undefined | null; user: UserResource | undefined | null; - session: ActiveSessionResource | undefined | null; + session: AuthenticatedSessionResource | undefined | null; organization: OrganizationResource | undefined | null; }>({ isLoaded: false, diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index eb085dce250..39714eefe1e 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -148,6 +148,7 @@ describe('Clerk singleton', () => { }); }); + // todo -> add tests for pending describe('.setActive', () => { const mockSession = { id: '1', @@ -455,6 +456,7 @@ describe('Clerk singleton', () => { }); }); + // todo -> add tests for pending describe('.load()', () => { const mockSession = { id: '1', diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d74720f50af..825fc608497 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -10,7 +10,7 @@ import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, - ActiveSessionResource, + AuthenticatedSessionResource, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, @@ -164,7 +164,7 @@ export class Clerk implements ClerkInterface { }; public client: ClientResource | undefined; - public session: ActiveSessionResource | null | undefined; + public session: AuthenticatedSessionResource | null | undefined; public organization: OrganizationResource | null | undefined; public user: UserResource | null | undefined; public __internal_country?: string | null; @@ -387,7 +387,7 @@ export class Clerk implements ClerkInterface { }); }; - if (!opts.sessionId || this.client.activeSessions.length === 1) { + if (!opts.sessionId || this.client.authenticatedSessions.length === 1) { if (this.#options.experimental?.persistClient ?? true) { await this.client.removeSessions(); } else { @@ -397,7 +397,7 @@ export class Clerk implements ClerkInterface { return handleSetActive(); } - const session = this.client.activeSessions.find(s => s.id === opts.sessionId); + const session = this.client.authenticatedSessions.find(s => s.id === opts.sessionId); const shouldSignOutCurrent = session?.id && this.session?.id === session.id; await session?.remove(); if (shouldSignOutCurrent) { @@ -877,12 +877,12 @@ export class Clerk implements ClerkInterface { : noop; if (typeof session === 'string') { - session = (this.client.sessions.find(x => x.id === session) as ActiveSessionResource) || null; + session = (this.client.sessions.find(x => x.id === session) as AuthenticatedSessionResource) || null; } let newSession = session === undefined ? this.session : session; - // At this point, the `session` variable should contain either an `ActiveSessionResource` + // At this point, the `session` variable should contain either an `AuthenticatedSessionResource` // ,`null` or `undefined`. // We now want to set the last active organization id on that session (if it exists). // However, if the `organization` parameter is not given (i.e. `undefined`), we want @@ -920,7 +920,7 @@ export class Clerk implements ClerkInterface { // Note that this will also update the session's active organization // id. if (inActiveBrowserTab() || !this.#options.standardBrowser) { - await this.#touchLastActiveSession(newSession); + await this.#touchCurrentSession(newSession); // reload session from updated client newSession = this.#getSessionFromClient(newSession?.id); } @@ -2028,14 +2028,14 @@ export class Clerk implements ClerkInterface { this.#emit(); }; - #defaultSession = (client: ClientResource): ActiveSessionResource | null => { + #defaultSession = (client: ClientResource): AuthenticatedSessionResource | null => { if (client.lastActiveSessionId) { - const lastActiveSession = client.activeSessions.find(s => s.id === client.lastActiveSessionId); + const lastActiveSession = client.authenticatedSessions.find(s => s.id === client.lastActiveSessionId); if (lastActiveSession) { return lastActiveSession; } } - const session = client.activeSessions[0]; + const session = client.authenticatedSessions[0]; return session || null; }; @@ -2055,7 +2055,7 @@ export class Clerk implements ClerkInterface { } this.#touchThrottledUntil = Date.now() + 5_000; - return this.#touchLastActiveSession(this.session); + return this.#touchCurrentSession(this.session); }; this.#sessionTouchOfflineScheduler.schedule(performTouch); @@ -2069,7 +2069,7 @@ export class Clerk implements ClerkInterface { }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc - #touchLastActiveSession = async (session?: ActiveSessionResource | null): Promise => { + #touchCurrentSession = async (session?: AuthenticatedSessionResource | null): Promise => { if (!session || !this.#options.touchSession) { return Promise.resolve(); } @@ -2118,14 +2118,14 @@ export class Clerk implements ClerkInterface { ); }; - #setAccessors = (session?: ActiveSessionResource | null) => { + #setAccessors = (session?: AuthenticatedSessionResource | null) => { this.session = session || null; this.organization = this.#getLastActiveOrganizationFromSession(); this.#aliasUser(); }; - #getSessionFromClient = (sessionId: string | undefined): ActiveSessionResource | null => { - return this.client?.activeSessions.find(x => x.id === sessionId) || null; + #getSessionFromClient = (sessionId: string | undefined): AuthenticatedSessionResource | null => { + return this.client?.authenticatedSessions.find(x => x.id === sessionId) || null; }; #aliasUser = () => { diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index b7558c49a07..8ff4e1b1902 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -1,10 +1,11 @@ -import type { - ActiveSessionResource, - ClientJSON, - ClientJSONSnapshot, - ClientResource, - SignInResource, - SignUpResource, +import { + type ActiveSessionResource, + type AuthenticatedSessionResource, + type ClientJSON, + type ClientJSONSnapshot, + type ClientResource, + type SignInResource, + type SignUpResource, } from '@clerk/types'; import { unixEpochToDate } from '../../utils/date'; @@ -57,6 +58,10 @@ export class Client extends BaseResource implements ClientResource { return this.sessions.filter(s => s.status === 'active') as ActiveSessionResource[]; } + get authenticatedSessions(): AuthenticatedSessionResource[] { + return this.sessions.filter(s => s.status === 'active' || s.status === 'pending') as AuthenticatedSessionResource[]; + } + create(): Promise { return this._basePut(); } diff --git a/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx b/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx index d9d7299424c..ccac75ed8af 100644 --- a/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx +++ b/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx @@ -1,5 +1,5 @@ import { useClerk, useSession, useUser } from '@clerk/shared/react'; -import type { ActiveSessionResource } from '@clerk/types'; +import type { AuthenticatedSessionResource } from '@clerk/types'; import type { PointerEventHandler } from 'react'; import React, { useEffect, useRef } from 'react'; @@ -66,7 +66,7 @@ const FabContent = ({ title, signOutText }: FabContentProps) => { const { otherSessions } = useMultipleSessions({ user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); - const handleSignOutSessionClicked = (session: ActiveSessionResource) => () => { + const handleSignOutSessionClicked = (session: AuthenticatedSessionResource) => () => { if (otherSessions.length === 0) { return signOut(navigateAfterSignOut); } diff --git a/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx b/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx index 867ab5345ab..d980a28cc4d 100644 --- a/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx @@ -1,4 +1,4 @@ -import type { ActiveSessionResource } from '@clerk/types'; +import type { AuthenticatedSessionResource } from '@clerk/types'; import type { ElementDescriptor, ElementId } from '../../../ui/customizables/elementDescriptors'; import { useRouter } from '../../../ui/router'; @@ -13,9 +13,9 @@ import type { DefaultItemIds, MenuItem } from '../../utils/createCustomMenuItems type SingleSessionActionsProps = { handleManageAccountClicked: () => Promise | void; - handleSignOutSessionClicked: (session: ActiveSessionResource) => () => Promise | void; + handleSignOutSessionClicked: (session: AuthenticatedSessionResource) => () => Promise | void; handleUserProfileActionClicked: (startPath?: string) => Promise | void; - session: ActiveSessionResource; + session: AuthenticatedSessionResource; completedCallback: () => void; }; @@ -113,12 +113,12 @@ export const SingleSessionActions = (props: SingleSessionActionsProps) => { type MultiSessionActionsProps = { handleManageAccountClicked: () => Promise | void; - handleSignOutSessionClicked: (session: ActiveSessionResource) => () => Promise | void; - handleSessionClicked: (session: ActiveSessionResource) => () => Promise | void; + handleSignOutSessionClicked: (session: AuthenticatedSessionResource) => () => Promise | void; + handleSessionClicked: (session: AuthenticatedSessionResource) => () => Promise | void; handleAddAccountClicked: () => Promise | void; handleUserProfileActionClicked: (startPath?: string) => Promise | void; - session: ActiveSessionResource; - otherSessions: ActiveSessionResource[]; + session: AuthenticatedSessionResource; + otherSessions: AuthenticatedSessionResource[]; completedCallback: () => void; }; diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx index e773bdf2e08..af4249b3b05 100644 --- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx @@ -1,5 +1,5 @@ import { useSession, useUser } from '@clerk/shared/react'; -import type { ActiveSessionResource } from '@clerk/types'; +import type { AuthenticatedSessionResource } from '@clerk/types'; import React from 'react'; import { useEnvironment, useUserButtonContext } from '../../contexts'; @@ -14,7 +14,7 @@ type UserButtonPopoverProps = { close?: (open: boolean) => void } & PropsOfCompo export const UserButtonPopover = React.forwardRef((props, ref) => { const { close: unsafeClose, ...rest } = props; const close = () => unsafeClose?.(false); - const { session } = useSession() as { session: ActiveSessionResource }; + const { session } = useSession() as { session: AuthenticatedSessionResource }; const userButtonContext = useUserButtonContext(); const { __experimental_asStandalone } = userButtonContext; const { authConfig } = useEnvironment(); diff --git a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx index c5acbd5ed2b..837d3b8ca22 100644 --- a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx @@ -1,5 +1,5 @@ import { useClerk } from '@clerk/shared/react'; -import type { ActiveSessionResource, UserButtonProps, UserResource } from '@clerk/types'; +import type { AuthenticatedSessionResource, UserButtonProps, UserResource } from '@clerk/types'; import { windowNavigate } from '../../../utils/windowNavigate'; import { useCardState } from '../../elements'; @@ -20,10 +20,10 @@ type UseMultisessionActionsParams = { export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { const { setActive, signOut, openUserProfile } = useClerk(); const card = useCardState(); - const { activeSessions, otherSessions } = useMultipleSessions({ user: opts.user }); + const { authenticatedSessions, otherSessions } = useMultipleSessions({ user: opts.user }); const { navigate } = useRouter(); - const handleSignOutSessionClicked = (session: ActiveSessionResource) => () => { + const handleSignOutSessionClicked = (session: AuthenticatedSessionResource) => () => { if (otherSessions.length === 0) { return signOut(opts.navigateAfterSignOut); } @@ -66,7 +66,7 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { return signOut(opts.navigateAfterSignOut); }; - const handleSessionClicked = (session: ActiveSessionResource) => async () => { + const handleSessionClicked = (session: AuthenticatedSessionResource) => async () => { card.setLoading(); return setActive({ session, redirectUrl: opts.afterSwitchSessionUrl }).finally(() => { card.setIdle(); @@ -87,6 +87,6 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { handleSessionClicked, handleAddAccountClicked, otherSessions, - activeSessions, + authenticatedSessions, }; }; diff --git a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts index c62caba2936..0f134c236c9 100644 --- a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts +++ b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts @@ -1,18 +1,16 @@ -import { useSessionList } from '@clerk/shared/react'; -import type { ActiveSessionResource, UserResource } from '@clerk/types'; +import { useClerk } from '@clerk/shared/react'; +import type { UserResource } from '@clerk/types'; type UseMultipleSessionsParam = { user: UserResource | null | undefined; }; const useMultipleSessions = (params: UseMultipleSessionsParam) => { - const { sessions } = useSessionList(); - const activeSessions = sessions?.filter(s => s.status === 'active') as ActiveSessionResource[]; - const otherSessions = activeSessions.filter(s => s.user?.id !== params.user?.id); + const clerk = useClerk(); return { - activeSessions, - otherSessions, + authenticatedSessions: clerk.client.authenticatedSessions, + otherSessions: clerk.client.authenticatedSessions.filter(s => s.user?.id !== params.user?.id), }; }; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 736570ae8a8..b6d088a9c2d 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -5,7 +5,7 @@ import { handleValueOrFn } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, __internal_UserVerificationProps, - ActiveSessionResource, + AuthenticatedSessionResource, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, @@ -616,7 +616,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } - get session(): ActiveSessionResource | undefined | null { + get session(): AuthenticatedSessionResource | undefined | null { if (this.clerkjs) { return this.clerkjs.session; } else { diff --git a/packages/shared/src/deriveState.ts b/packages/shared/src/deriveState.ts index 771c8040839..608c015277e 100644 --- a/packages/shared/src/deriveState.ts +++ b/packages/shared/src/deriveState.ts @@ -1,5 +1,5 @@ import type { - ActiveSessionResource, + AuthenticatedSessionResource, InitialState, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, @@ -22,7 +22,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const userId = initialState.userId; const user = initialState.user as UserResource; const sessionId = initialState.sessionId; - const session = initialState.session as ActiveSessionResource; + const session = initialState.session as AuthenticatedSessionResource; const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as OrganizationCustomRoleKey; diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index ad178e59202..332d97f2935 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -1,7 +1,7 @@ 'use client'; import type { - ActiveSessionResource, + AuthenticatedSessionResource, ClerkOptions, ClientResource, LoadedClerk, @@ -17,7 +17,7 @@ import { createContextAndHook } from './hooks/createContextAndHook'; const [ClerkInstanceContext, useClerkInstanceContext] = createContextAndHook('ClerkInstanceContext'); const [UserContext, useUserContext] = createContextAndHook('UserContext'); const [ClientContext, useClientContext] = createContextAndHook('ClientContext'); -const [SessionContext, useSessionContext] = createContextAndHook( +const [SessionContext, useSessionContext] = createContextAndHook( 'SessionContext', ); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 2b0e8ff27c2..dc381e83286 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -31,7 +31,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; -import type { ActiveSessionResource } from './session'; +import type { AuthenticatedSessionResource } from './session'; import type { SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; @@ -63,7 +63,7 @@ export type SDKMetadata = { export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; -export type BeforeEmitCallback = (session?: ActiveSessionResource | null) => void | Promise; +export type BeforeEmitCallback = (session?: AuthenticatedSessionResource | null) => void | Promise; export type SignOutCallback = () => void | Promise; @@ -130,8 +130,8 @@ export interface Clerk { /** Client handling most Clerk operations. */ client: ClientResource | undefined; - /** Active Session. */ - session: ActiveSessionResource | null | undefined; + /** Current Session. */ + session: AuthenticatedSessionResource | null | undefined; /** Active Organization */ organization: OrganizationResource | null | undefined; @@ -708,9 +708,9 @@ export type ClerkOptions = ClerkOptionsNavigation & localization?: LocalizationResource; polling?: boolean; /** - * By default, the last active session is used during client initialization. This option allows you to override that behavior, e.g. by selecting a specific session. + * By default, the last authenticated session is used during client initialization. This option allows you to override that behavior, e.g. by selecting a specific session. */ - selectInitialSession?: (client: ClientResource) => ActiveSessionResource | null; + selectInitialSession?: (client: ClientResource) => AuthenticatedSessionResource | null; /** * By default, ClerkJS is loaded with the assumption that cookies can be set (browser setup). On native platforms this value must be set to `false`. */ @@ -796,7 +796,7 @@ export interface NavigateOptions { export interface Resources { client: ClientResource; - session?: ActiveSessionResource | null; + session?: AuthenticatedSessionResource | null; user?: UserResource | null; organization?: OrganizationResource | null; } @@ -869,10 +869,10 @@ export type SignUpRedirectOptions = RedirectOptions & export type SetActiveParams = { /** - * The session resource or session id (string version) to be set as active. + * The session resource or session id (string version) to be set on the client. * If `null`, the current session is deleted. */ - session?: ActiveSessionResource | string | null; + session?: AuthenticatedSessionResource | string | null; /** * The organization resource or organization ID/slug (string version) to be set as active in the current session. diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index d9b235bade1..deebf5d1140 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,5 +1,5 @@ import type { ClerkResource } from './resource'; -import type { ActiveSessionResource, SessionResource } from './session'; +import type { ActiveSessionResource, PendingSessionResource, SessionResource } from './session'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; import type { ClientJSONSnapshot } from './snapshots'; @@ -7,6 +7,7 @@ import type { ClientJSONSnapshot } from './snapshots'; export interface ClientResource extends ClerkResource { sessions: SessionResource[]; activeSessions: ActiveSessionResource[]; + authenticatedSessions: (ActiveSessionResource | PendingSessionResource)[]; signUp: SignUpResource; signIn: SignInResource; isNew: () => boolean; diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index 4a1a05c66ba..a694e15f0d9 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -4,7 +4,7 @@ import type { SignInResource } from 'signIn'; import type { SetActive, SignOut } from './clerk'; import type { ActJWTClaim } from './jwt'; import type { - ActiveSessionResource, + AuthenticatedSessionResource, CheckAuthorizationWithCustomPermissions, GetToken, SessionResource, @@ -180,7 +180,7 @@ export type UseSessionReturn = | { isLoaded: true; isSignedIn: true; - session: ActiveSessionResource; + session: AuthenticatedSessionResource; }; /** diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 9b0df6dd01f..23cc9e411cf 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -131,20 +131,29 @@ export interface SessionResource extends ClerkResource { __internal_toSnapshot: () => SessionJSONSnapshot; } +/** + * Represents a session resource that has completed all tasks + * and authentication factors + */ export interface ActiveSessionResource extends SessionResource { status: 'active'; user: UserResource; } +/** + * Represents a session resource that has pending tasks to be + * completed, eg: User has to select an organization + */ export interface PendingSessionResource extends SessionResource { status: 'pending'; user: UserResource; } -// todo: figure out better naming -// the goal here is just to encapsulate those both sessions so we don't keep repeating -// the same union everyone for the same purpose: the user has successfully verified / authenticated themselves -export type AuthenticatedSession = ActiveSessionResource | PendingSessionResource; +/** + * Represents session resources for users who have completed + * the full authentication flow. + */ +export type AuthenticatedSessionResource = ActiveSessionResource | PendingSessionResource; export interface SessionWithActivitiesResource extends ClerkResource { id: string; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 213c909b658..decfdef886b 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -1,6 +1,6 @@ import type { - ActiveSessionResource, ActJWTClaim, + AuthenticatedSessionResource, Clerk, ClerkOptions, ClientResource, @@ -26,7 +26,7 @@ export interface VueClerkInjectionKeyType { orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; }>; clientCtx: ComputedRef; - sessionCtx: ComputedRef; + sessionCtx: ComputedRef; userCtx: ComputedRef; organizationCtx: ComputedRef; } From 1a6dffb2551f707745b7f59628ef4c62aaa83b28 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:07:31 -0300 Subject: [PATCH 03/17] Add test coverage for `pending` and `active` statuses --- .../clerk-js/src/core/__tests__/clerk.test.ts | 819 +++++++++--------- 1 file changed, 426 insertions(+), 393 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 39714eefe1e..b55061bf962 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -1,4 +1,10 @@ -import type { ActiveSessionResource, SignInJSON, SignUpJSON, TokenResource } from '@clerk/types'; +import type { + ActiveSessionResource, + AuthenticatedSessionResource, + SignInJSON, + SignUpJSON, + TokenResource, +} from '@clerk/types'; import { waitFor } from '@testing-library/dom'; import { mockNativeRuntime } from '../../testUtils'; @@ -122,6 +128,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ activeSessions: [], + authenticatedSessions: [], }), ); @@ -148,373 +155,394 @@ describe('Clerk singleton', () => { }); }); - // todo -> add tests for pending describe('.setActive', () => { - const mockSession = { - id: '1', - remove: jest.fn(), - status: 'active', - user: {}, - touch: jest.fn(() => Promise.resolve()), - getToken: jest.fn(), - lastActiveToken: { getRawString: () => 'mocked-token' }, - }; - let eventBusSpy; - - beforeEach(() => { - eventBusSpy = jest.spyOn(eventBus, 'dispatch'); - }); - - afterEach(() => { - mockSession.remove.mockReset(); - mockSession.touch.mockReset(); - - eventBusSpy?.mockRestore(); - // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; - }); - - it('does not call session touch on signOut', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: null }); - await waitFor(() => { - expect(mockSession.touch).not.toHaveBeenCalled(); - expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: null }); - }); - }); - - it('calls session.touch by default', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - expect(mockSession.touch).toHaveBeenCalled(); - }); - - it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - mockSession.getToken.mockResolvedValue('mocked-token'); - - const sut = new Clerk(productionPublishableKey); - await sut.load({ touchSession: false }); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - await waitFor(() => { - expect(mockSession.touch).not.toHaveBeenCalled(); - expect(mockSession.getToken).toHaveBeenCalled(); - }); - }); - - it('calls __unstable__onBeforeSetActive before session.touch', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - (window as any).__unstable__onBeforeSetActive = () => { - expect(mockSession.touch).not.toHaveBeenCalled(); - }; + describe.each(['active', 'pending'] satisfies Array)( + 'when session has %s status', + status => { + const mockSession = { + id: '1', + remove: jest.fn(), + status, + user: {}, + touch: jest.fn(() => Promise.resolve()), + getToken: jest.fn(), + lastActiveToken: { getRawString: () => 'mocked-token' }, + }; + let eventBusSpy; + + beforeEach(() => { + eventBusSpy = jest.spyOn(eventBus, 'dispatch'); + }); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - expect(mockSession.touch).toHaveBeenCalled(); - }); + afterEach(() => { + mockSession.remove.mockReset(); + mockSession.touch.mockReset(); - it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + eventBusSpy?.mockRestore(); + // cleanup global window pollution + (window as any).__unstable__onBeforeSetActive = null; + (window as any).__unstable__onAfterSetActive = null; + }); - (window as any).__unstable__onBeforeSetActive = () => { - expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken }); - }; + it('does not call session touch on signOut', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: null }); + await waitFor(() => { + expect(mockSession.touch).not.toHaveBeenCalled(); + expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: null }); + }); + }); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - }); + it('calls session.touch by default', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); - it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => { - const beforeEmitMock = jest.fn(); - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + expect(mockSession.touch).toHaveBeenCalled(); + }); - (window as any).__unstable__onAfterSetActive = () => { - expect(mockSession.touch).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalled(); - }; + it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockSession.getToken.mockResolvedValue('mocked-token'); + + const sut = new Clerk(productionPublishableKey); + await sut.load({ touchSession: false }); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + await waitFor(() => { + expect(mockSession.touch).not.toHaveBeenCalled(); + expect(mockSession.getToken).toHaveBeenCalled(); + }); + }); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); - }); + it('calls __unstable__onBeforeSetActive before session.touch', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); - // TODO: @dimkl include set transitive state - it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => { - const mockSession2 = { - id: '2', - remove: jest.fn(), - status: 'active', - user: {}, - touch: jest.fn(), - getToken: jest.fn(), - }; - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession, mockSession2] })); + (window as any).__unstable__onBeforeSetActive = () => { + expect(mockSession.touch).not.toHaveBeenCalled(); + }; - const sut = new Clerk(productionPublishableKey); - await sut.load(); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + expect(mockSession.touch).toHaveBeenCalled(); + }); - const executionOrder: string[] = []; - mockSession2.touch.mockImplementationOnce(() => { - sut.session = mockSession2 as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - mockSession2.getToken.mockImplementation(() => { - executionOrder.push('set cookie'); - return 'mocked-token-2'; - }); - const beforeEmitMock = jest.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); - }); + it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); - await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); + (window as any).__unstable__onBeforeSetActive = () => { + expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken }); + }; - await waitFor(() => { - expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); - expect(mockSession2.touch).toHaveBeenCalled(); - expect(mockSession2.getToken).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2); - expect(sut.session).toMatchObject(mockSession2); - }); - }); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + }); - // TODO: @dimkl include set transitive state - it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); + it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => { + const beforeEmitMock = jest.fn(); + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); - const executionOrder: string[] = []; - mockSession.touch.mockImplementationOnce(() => { - sut.session = mockSession as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - mockSession.getToken.mockImplementation(() => { - executionOrder.push('set cookie'); - return 'mocked-token'; - }); + (window as any).__unstable__onAfterSetActive = () => { + expect(mockSession.touch).toHaveBeenCalled(); + expect(beforeEmitMock).toHaveBeenCalled(); + }; - const beforeEmitMock = jest.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); - }); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); + }); - await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + // TODO: @dimkl include set transitive state + it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => { + const mockSession2 = { + id: '2', + remove: jest.fn(), + status: 'active', + user: {}, + touch: jest.fn(), + getToken: jest.fn(), + }; + mockClientFetch.mockReturnValue( + Promise.resolve({ + authenticatedSessions: [mockSession, mockSession2], + }), + ); + + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + const executionOrder: string[] = []; + mockSession2.touch.mockImplementationOnce(() => { + sut.session = mockSession2 as any; + executionOrder.push('session.touch'); + return Promise.resolve(); + }); + mockSession2.getToken.mockImplementation(() => { + executionOrder.push('set cookie'); + return 'mocked-token-2'; + }); + const beforeEmitMock = jest.fn().mockImplementationOnce(() => { + executionOrder.push('before emit'); + return Promise.resolve(); + }); + + await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); + + await waitFor(() => { + expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); + expect(mockSession2.touch).toHaveBeenCalled(); + expect(mockSession2.getToken).toHaveBeenCalled(); + expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2); + expect(sut.session).toMatchObject(mockSession2); + }); + }); - await waitFor(() => { - expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); - expect(mockSession.touch).toHaveBeenCalled(); - expect(mockSession.getToken).toHaveBeenCalled(); - expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); - expect(sut.session).toMatchObject(mockSession); - }); - }); + // TODO: @dimkl include set transitive state + it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + const executionOrder: string[] = []; + mockSession.touch.mockImplementationOnce(() => { + sut.session = mockSession as any; + executionOrder.push('session.touch'); + return Promise.resolve(); + }); + mockSession.getToken.mockImplementation(() => { + executionOrder.push('set cookie'); + return 'mocked-token'; + }); + + const beforeEmitMock = jest.fn().mockImplementationOnce(() => { + executionOrder.push('before emit'); + return Promise.resolve(); + }); + + await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + + await waitFor(() => { + expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); + expect(mockSession.touch).toHaveBeenCalled(); + expect(mockSession.getToken).toHaveBeenCalled(); + expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); + expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); + expect(sut.session).toMatchObject(mockSession); + }); + }); - it('sets active organization by slug', async () => { - const mockSession2 = { - id: '1', - status: 'active', - user: { - organizationMemberships: [ - { - id: 'orgmem_id', - organization: { - id: 'org_id', - slug: 'some-org-slug', - }, + it('sets active organization by slug', async () => { + const mockSession2 = { + id: '1', + status: 'active', + user: { + organizationMemberships: [ + { + id: 'orgmem_id', + organization: { + id: 'org_id', + slug: 'some-org-slug', + }, + }, + ], }, - ], - }, - touch: jest.fn(), - getToken: jest.fn(), - }; - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession2] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - - mockSession2.touch.mockImplementationOnce(() => { - sut.session = mockSession2 as any; - return Promise.resolve(); - }); - mockSession2.getToken.mockImplementation(() => 'mocked-token'); - - await sut.setActive({ organization: 'some-org-slug' }); - - await waitFor(() => { - expect(mockSession2.touch).toHaveBeenCalled(); - expect(mockSession2.getToken).toHaveBeenCalled(); - expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); - expect(sut.session).toMatchObject(mockSession2); - }); - }); - - it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession], - cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now - isEligibleForTouch: () => true, - buildTouchUrl: () => - `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, - }), - ); - - const sut = new Clerk(productionPublishableKey); - sut.navigate = jest.fn(); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); - const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]); - expect(redirectUrl.pathname).toEqual('/v1/client/touch'); - expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`); - }); - - it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession], - cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now - isEligibleForTouch: () => false, - buildTouchUrl: () => - `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, - }), - ); - - const sut = new Clerk(productionPublishableKey); - sut.navigate = jest.fn(); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); - expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); - }); - - it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession], - cookieExpiresAt: null, - isEligibleForTouch: () => false, - buildTouchUrl: () => - `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, - }), - ); - - const sut = new Clerk(productionPublishableKey); - sut.navigate = jest.fn(); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); - expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); - }); - - mockNativeRuntime(() => { - it('calls session.touch in a non-standard browser', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load({ standardBrowser: false }); + touch: jest.fn(), + getToken: jest.fn(), + }; + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession2] })); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + mockSession2.touch.mockImplementationOnce(() => { + sut.session = mockSession2 as any; + return Promise.resolve(); + }); + mockSession2.getToken.mockImplementation(() => 'mocked-token'); + + await sut.setActive({ organization: 'some-org-slug' }); + + await waitFor(() => { + expect(mockSession2.touch).toHaveBeenCalled(); + expect(mockSession2.getToken).toHaveBeenCalled(); + expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); + expect(sut.session).toMatchObject(mockSession2); + }); + }); - const executionOrder: string[] = []; - mockSession.touch.mockImplementationOnce(() => { - sut.session = mockSession as any; - executionOrder.push('session.touch'); - return Promise.resolve(); + it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + authenticatedSessions: [mockSession], + cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + isEligibleForTouch: () => true, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ + session: mockSession as any as ActiveSessionResource, + redirectUrl: '/redirect-url-path', + }); + const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]); + expect(redirectUrl.pathname).toEqual('/v1/client/touch'); + expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`); }); - const beforeEmitMock = jest.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); + + it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + authenticatedSessions: [mockSession], + cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now + isEligibleForTouch: () => false, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ + session: mockSession as any as ActiveSessionResource, + redirectUrl: '/redirect-url-path', + }); + expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); }); - await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + authenticatedSessions: [mockSession], + cookieExpiresAt: null, + isEligibleForTouch: () => false, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ + session: mockSession as any as ActiveSessionResource, + redirectUrl: '/redirect-url-path', + }); + expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); + }); - expect(executionOrder).toEqual(['session.touch', 'before emit']); - expect(mockSession.touch).toHaveBeenCalled(); - expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); - expect(mockSession.getToken).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); - expect(sut.session).toMatchObject(mockSession); - }); - }); + mockNativeRuntime(() => { + it('calls session.touch in a non-standard browser', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + + const sut = new Clerk(productionPublishableKey); + await sut.load({ standardBrowser: false }); + + const executionOrder: string[] = []; + mockSession.touch.mockImplementationOnce(() => { + sut.session = mockSession as any; + executionOrder.push('session.touch'); + return Promise.resolve(); + }); + const beforeEmitMock = jest.fn().mockImplementationOnce(() => { + executionOrder.push('before emit'); + return Promise.resolve(); + }); + + await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + + expect(executionOrder).toEqual(['session.touch', 'before emit']); + expect(mockSession.touch).toHaveBeenCalled(); + expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); + expect(mockSession.getToken).toHaveBeenCalled(); + expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); + expect(sut.session).toMatchObject(mockSession); + }); + }); + }, + ); }); - // todo -> add tests for pending describe('.load()', () => { - const mockSession = { - id: '1', - status: 'active', - user: {}, - getToken: jest.fn(), - lastActiveToken: { getRawString: () => mockJwt }, - }; - - afterEach(() => { - // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; - }); - - it('gracefully handles an incorrect value returned from the user provided selectInitialSession', async () => { - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [], - }), - ); - - // any is intentional here. We simulate a runtime value that should not exist - const mockSelectInitialSession = jest.fn(() => undefined) as any; - const sut = new Clerk(productionPublishableKey); - await sut.load({ - selectInitialSession: mockSelectInitialSession, - }); + describe.each(['active', 'pending'] satisfies Array)( + 'when session has %s status', + status => { + const mockSession = { + id: '1', + status, + user: {}, + getToken: jest.fn(), + lastActiveToken: { getRawString: () => mockJwt }, + }; + + afterEach(() => { + // cleanup global window pollution + (window as any).__unstable__onBeforeSetActive = null; + (window as any).__unstable__onAfterSetActive = null; + }); - await waitFor(() => { - expect(sut.session).not.toBe(undefined); - expect(sut.session).toBe(null); - }); - }); + it('gracefully handles an incorrect value returned from the user provided selectInitialSession', async () => { + mockClientFetch.mockReturnValue( + Promise.resolve({ + authenticatedSessions: [], + }), + ); + + // any is intentional here. We simulate a runtime value that should not exist + const mockSelectInitialSession = jest.fn(() => undefined) as any; + const sut = new Clerk(productionPublishableKey); + await sut.load({ + selectInitialSession: mockSelectInitialSession, + }); + + await waitFor(() => { + expect(sut.session).not.toBe(undefined); + expect(sut.session).toBe(null); + }); + }); - it('updates auth cookie on load from fetched session', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + it('updates auth cookie on load from fetched session', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); + const sut = new Clerk(productionPublishableKey); + await sut.load(); - expect(document.cookie).toContain(mockJwt); - }); + expect(document.cookie).toContain(mockJwt); + }); - it('updates auth cookie on token:update event', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + it('updates auth cookie on token:update event', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); + const sut = new Clerk(productionPublishableKey); + await sut.load(); - const token = { - jwt: {}, - getRawString: () => 'updated-jwt', - } as TokenResource; - eventBus.dispatch(events.TokenUpdate, { token }); + const token = { + jwt: {}, + getRawString: () => 'updated-jwt', + } as TokenResource; + eventBus.dispatch(events.TokenUpdate, { token }); - expect(document.cookie).toContain('updated-jwt'); - }); + expect(document.cookie).toContain('updated-jwt'); + }); + }, + ); }); describe('.signOut()', () => { @@ -522,19 +550,21 @@ describe('Clerk singleton', () => { const mockClientRemoveSessions = jest.fn(); const mockSession1 = { id: '1', remove: jest.fn(), status: 'active', user: {}, getToken: jest.fn() }; const mockSession2 = { id: '2', remove: jest.fn(), status: 'active', user: {}, getToken: jest.fn() }; + const mockSession3 = { id: '2', remove: jest.fn(), status: 'pending', user: {}, getToken: jest.fn() }; beforeEach(() => { mockClientDestroy.mockReset(); mockClientRemoveSessions.mockReset(); mockSession1.remove.mockReset(); mockSession2.remove.mockReset(); + mockSession3.remove.mockReset(); }); - it('has no effect if called when no active sessions exist', async () => { + it('has no effect if called when no sessions exist', async () => { const sut = new Clerk(productionPublishableKey); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], destroy: mockClientDestroy, }), @@ -547,11 +577,11 @@ describe('Clerk singleton', () => { }); }); - it('signs out all sessions if no sessionId is passed and multiple sessions are active', async () => { + it('signs out all sessions if no sessionId is passed and multiple sessions have authenticated status', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + authenticatedSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, removeSessions: mockClientRemoveSessions, }), @@ -571,36 +601,39 @@ describe('Clerk singleton', () => { }); }); - it('signs out all sessions if no sessionId is passed and only one session is active', async () => { - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession1], - sessions: [mockSession1], - destroy: mockClientDestroy, - removeSessions: mockClientRemoveSessions, - }), - ); + it.each(['active', 'pending'] satisfies Array)( + 'signs out all sessions if no sessionId is passed and only one session has %s status', + async status => { + mockClientFetch.mockReturnValue( + Promise.resolve({ + authenticatedSessions: [{ ...mockSession1, status }], + sessions: [{ ...mockSession1, status }], + destroy: mockClientDestroy, + removeSessions: mockClientRemoveSessions, + }), + ); - const sut = new Clerk(productionPublishableKey); - sut.setActive = jest.fn(); - await sut.load(); - await sut.signOut(); - await waitFor(() => { - expect(mockClientDestroy).not.toHaveBeenCalled(); - expect(mockClientRemoveSessions).toHaveBeenCalled(); - expect(mockSession1.remove).not.toHaveBeenCalled(); - expect(sut.setActive).toHaveBeenCalledWith({ - session: null, - redirectUrl: '/', + const sut = new Clerk(productionPublishableKey); + sut.setActive = jest.fn(); + await sut.load(); + await sut.signOut(); + await waitFor(() => { + expect(mockClientDestroy).not.toHaveBeenCalled(); + expect(mockClientRemoveSessions).toHaveBeenCalled(); + expect(mockSession1.remove).not.toHaveBeenCalled(); + expect(sut.setActive).toHaveBeenCalledWith({ + session: null, + redirectUrl: '/', + }); }); - }); - }); + }, + ); it('only removes the session that corresponds to the passed sessionId if it is not the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + authenticatedSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), ); @@ -621,8 +654,8 @@ describe('Clerk singleton', () => { it('removes and signs out the session that corresponds to the passed sessionId if it is the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + authenticatedSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), ); @@ -644,8 +677,8 @@ describe('Clerk singleton', () => { it('removes and signs out the session and redirects to the provided redirectUrl ', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + authenticatedSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), ); @@ -765,7 +798,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -824,7 +857,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -886,7 +919,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -948,7 +981,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1015,7 +1048,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1066,7 +1099,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -1121,7 +1154,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_second_factor', first_factor_verification: { @@ -1161,7 +1194,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_second_factor', first_factor_verification: { @@ -1215,7 +1248,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ sessions: [mockSession], - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1276,7 +1309,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ sessions: [mockSession], - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1328,7 +1361,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1376,7 +1409,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1424,7 +1457,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1465,7 +1498,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1512,7 +1545,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_first_factor', } as unknown as SignInJSON), @@ -1544,7 +1577,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1592,7 +1625,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1652,7 +1685,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_first_factor', first_factor_verification: { @@ -1702,7 +1735,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], signIn: new SignIn({ status: 'needs_new_password', } as unknown as SignInJSON), @@ -1750,7 +1783,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [{ id: createdSessionId }], signIn: new SignIn({ status: 'completed', @@ -1779,7 +1812,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'verified']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], signIn: new SignIn({ status: 'needs_second_factor', @@ -1810,7 +1843,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [{ id: createdSessionId }], signUp: new SignUp({ status: 'completed', @@ -1839,7 +1872,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'verified']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], signUp: new SignUp({ status: 'missing_requirements', @@ -1866,7 +1899,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'expired']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1888,7 +1921,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'failed']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1913,7 +1946,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1936,7 +1969,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_created_session', 'sess_123']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1959,7 +1992,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], sessions: [{ id: 'sess_123' }], signIn: new SignIn({ status: 'completed', From 99a782f4afbd4da1ac6038ed49bfb18e3ac72514 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:42:45 -0300 Subject: [PATCH 04/17] Update handshake tests --- integration/testUtils/handshake.ts | 5 +- integration/tests/handshake.test.ts | 273 +++++++++++++++++++++------- 2 files changed, 210 insertions(+), 68 deletions(-) diff --git a/integration/testUtils/handshake.ts b/integration/testUtils/handshake.ts index d4656b6744a..b7701f87d57 100644 --- a/integration/testUtils/handshake.ts +++ b/integration/testUtils/handshake.ts @@ -108,14 +108,13 @@ export function generateConfig({ mode, matchedKeys = true }: { mode: 'test' | 'l state, extraClaims, }: { - // todo -> add tests for pending - state: 'active' | 'expired' | 'early'; + state: 'active' | 'expired' | 'early' | 'pending'; extraClaims?: Map; }) => { const claims = { sub: 'user_12345' } as Claims; const now = Math.floor(Date.now() / 1000); - if (state === 'active') { + if (state === 'active' || state === 'pending') { claims.iat = now; claims.nbf = now - 10; claims.exp = now + 60; diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index af70df88937..21877843df9 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -3,6 +3,7 @@ import * as http from 'node:http'; import { expect, test } from '@playwright/test'; import type { OrganizationSyncOptions } from '../../packages/backend/src/tokens/types'; +import type { AuthenticatedSessionResource } from '../../packages/types'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; import { generateConfig, getJwksFromSecretKey } from '../testUtils/handshake'; @@ -69,75 +70,77 @@ test.describe('Client handshake @generic', () => { await new Promise(resolve => jwksServer.close(() => resolve())); }); - test('standard signed-in - dev', async () => { - const config = generateConfig({ mode: 'test' }); - const { token, claims } = config.generateToken({ state: 'active' }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - 'Sec-Fetch-Dest': 'document', - }), - redirect: 'manual', + (['active', 'pending'] satisfies Array).forEach(state => { + test(`standard signed-in with ${state} session - dev`, async () => { + const config = generateConfig({ mode: 'test' }); + const { token, claims } = config.generateToken({ state }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'Sec-Fetch-Dest': 'document', + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); }); - expect(res.status).toBe(200); - }); - test('standard signed-in - authorization header - dev', async () => { - const config = generateConfig({ - mode: 'test', - }); - const { token, claims } = config.generateToken({ state: 'active' }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `${devBrowserCookie} __client_uat=${clientUat};`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - Authorization: `Bearer ${token}`, - 'Sec-Fetch-Dest': 'document', - }), - redirect: 'manual', + test(`standard signed-in with ${state} - authorization header - dev`, async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat};`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Authorization: `Bearer ${token}`, + 'Sec-Fetch-Dest': 'document', + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); }); - expect(res.status).toBe(200); - }); - test('standard signed-in - prod', async () => { - const config = generateConfig({ - mode: 'live', - }); - const { token, claims } = config.generateToken({ state: 'active' }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `__client_uat=${clientUat}; __session=${token}`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - 'Sec-Fetch-Dest': 'document', - }), - redirect: 'manual', + test(`standard signed-in with ${state} - prod`, async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'Sec-Fetch-Dest': 'document', + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); }); - expect(res.status).toBe(200); - }); - test('standard signed-in - authorization header - prod', async () => { - const config = generateConfig({ - mode: 'live', - }); - const { token, claims } = config.generateToken({ state: 'active' }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `__client_uat=${clientUat};`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - Authorization: `Bearer ${token}`, - }), - redirect: 'manual', + test(`standard signed-in with ${state} - authorization header - prod`, async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat};`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Authorization: `Bearer ${token}`, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); }); - expect(res.status).toBe(200); }); test('expired session token - dev', async () => { @@ -919,7 +922,7 @@ test.describe('Client handshake with organization activation @nextjs', () => { }; type When = { // With this initial state... - initialAuthState: 'active' | 'expired' | 'early'; + initialAuthState: 'active' | 'expired' | 'early' | 'pending'; initialSessionClaims: Map; // When the customer app specifies these orgSyncOptions to middleware... @@ -985,6 +988,25 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: 'org_a', }, }, + { + name: 'Pending session, no org in session, but org a requested by ID => attempts to activate org A', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([ + // Intentionally empty + ]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-id/:id'], + }, + appRequestPath: '/organizations-by-id/org_a', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'org_a', + }, + }, // ---------------- Header-based auth tests ---------------- // Header-based auth requests come from non-browser actors, which don't have the __client cookie. @@ -1009,6 +1031,25 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: null, }, }, + { + name: 'Header-based auth should not handshake with pending auth', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([ + // Intentionally empty + ]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-id/:id'], + }, + appRequestPath: '/organizations-by-id/org_a', + tokenAppearsIn: 'header', + secFetchDestHeader: null, + }, + then: { + expectStatus: 200, + fapiOrganizationIdParamValue: null, + }, + }, { name: 'Header-based auth should not handshake with expired auth', when: { @@ -1029,7 +1070,7 @@ test.describe('Client handshake with organization activation @nextjs', () => { }, }, - // ---------------- Existing session active org tests ---------------- + // ---------------- Existing session org tests ---------------- { name: 'Active session, org A active in session, but org B is requested by ID => attempts to activate org B', when: { @@ -1113,6 +1154,70 @@ test.describe('Client handshake with organization activation @nextjs', () => { }, }, + { + name: 'Pending session, org A active in session, but org B is requested by ID => attempts to activate org B', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-id/:id', '/organizations-by-id/:id/(.*)'], + }, + appRequestPath: '/organizations-by-id/org_b', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'org_b', + }, + }, + { + name: 'Pending session, no active org in session, but org B is requested by slug => attempts to activate org B', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([ + // Intentionally empty + ]), + orgSyncOptions: { + organizationPatterns: [ + '/organizations-by-id/:id', + '/organizations-by-id/:id/(.*)', + '/organizations-by-slug/:slug', + '/organizations-by-slug/:id/(.*)', + ], + }, + appRequestPath: '/organizations-by-slug/bcorp', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'bcorp', + }, + }, + { + name: 'Pending session, org a in session, but *an org B subresource* is requested by slug => attempts to activate org B', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: [ + '/organizations-by-slug/:slug', + '/organizations-by-slug/:id/(.*)', + '/organizations-by-id/:id', + '/organizations-by-id/:id/(.*)', + ], + }, + appRequestPath: '/organizations-by-slug/bcorp/settings', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'bcorp', + }, + }, + // ---------------- Personal account tests ---------------- { name: 'Active session, org a in session, but *the personal account* is requested => attempts to activate PWS', @@ -1177,6 +1282,44 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: null, }, }, + { + name: 'Pending session, nothing session, and the personal account is requested => nothing to activate!', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([ + // Intentionally empty + ]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-slug/:slug', '/organizations-by-slug/:id/(.*)'], + personalAccountPatterns: ['/personal-account', '/personal-account/(.*)'], + }, + appRequestPath: '/personal-account', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 200, + fapiOrganizationIdParamValue: null, + }, + }, + { + name: 'Pending session, org a active in session, and org a is requested => nothing to activate!', + when: { + initialAuthState: 'pending', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-id/:id', '/organizations-by-id/:id/(.*)'], + personalAccountPatterns: ['/personal-account', '/personal-account/(.*)'], + }, + appRequestPath: '/organizations-by-id/org_a', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 200, + fapiOrganizationIdParamValue: null, + }, + }, { // NOTE(izaak): Would we prefer 500ing in this case? name: 'No config => nothing to activate, return 200', @@ -1338,7 +1481,7 @@ test.describe('Client handshake with an organization activation avoids infinite test('Ignores organization config when being redirected to', async () => { // Create a new map with an org_id key const { token, claims } = config.generateToken({ - state: 'active', // Must be active - handshake logic only runs once session is determined to be active + state: 'active', extraClaims: new Map([]), }); From 7fe2081e5c3e4ef26b40c31dceadfd44d85b0a95 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:12:27 -0300 Subject: [PATCH 05/17] Update device section to list for pending --- integration/testUtils/handshake.ts | 3 ++- .../UserProfile/ActiveDevicesSection.tsx | 2 +- .../__tests__/SecurityPage.test.tsx | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/integration/testUtils/handshake.ts b/integration/testUtils/handshake.ts index b7701f87d57..372b62d3e08 100644 --- a/integration/testUtils/handshake.ts +++ b/integration/testUtils/handshake.ts @@ -114,7 +114,8 @@ export function generateConfig({ mode, matchedKeys = true }: { mode: 'test' | 'l const claims = { sub: 'user_12345' } as Claims; const now = Math.floor(Date.now() / 1000); - if (state === 'active' || state === 'pending') { + const isAuthenticated = state === 'active' || state === 'pending'; + if (isAuthenticated) { claims.iat = now; claims.nbf = now - 10; claims.exp = now + 60; diff --git a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx index d13aa8a5b31..ecf1d31d9d3 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx @@ -29,7 +29,7 @@ export const ActiveDevicesSection = () => { ) : ( sessions?.sort(currentSessionFirst(session!.id)).map(sa => { - if (sa.status !== 'active') { + if (!['active', 'pending'].includes(sa.status)) { return null; } return ( diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx index d5927dfc576..fcb84d29bbf 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx @@ -134,6 +134,25 @@ describe('SecurityPage', () => { actor: null, revoke: jest.fn().mockResolvedValue({}), } as any as SessionWithActivitiesResource, + { + pathRoot: '/me/sessions', + id: 'sess_2HyQfBh8wRJUbpvCtPNllWdsHFi', + status: 'pending', + expireAt: '2022-12-01T01:55:44.636Z', + abandonAt: '2022-12-24T01:55:44.636Z', + lastActiveAt: '2022-11-24T12:11:49.328Z', + latestActivity: { + id: 'sess_activity_2HyQwElm529O5NDL1KNpJAGWVJZ', + deviceType: 'Macintosh', + browserName: 'Chrome', + browserVersion: '107.0.0.0', + country: 'Greece', + city: 'Athens', + isMobile: false, + }, + actor: null, + revoke: jest.fn().mockResolvedValue({}), + } as any as SessionWithActivitiesResource, ]), ); From 76cbf8d1eb3e672c38a7008ea3a4e5f41a7b1d2d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:16:37 -0300 Subject: [PATCH 06/17] Fix sign in account switcher to look for pending sessions --- integration/testUtils/handshake.ts | 5 +- integration/tests/handshake.test.ts | 273 +++++------------- .../SignIn/SignInAccountSwitcher.tsx | 4 +- 3 files changed, 69 insertions(+), 213 deletions(-) diff --git a/integration/testUtils/handshake.ts b/integration/testUtils/handshake.ts index 372b62d3e08..74c4cea2026 100644 --- a/integration/testUtils/handshake.ts +++ b/integration/testUtils/handshake.ts @@ -108,14 +108,13 @@ export function generateConfig({ mode, matchedKeys = true }: { mode: 'test' | 'l state, extraClaims, }: { - state: 'active' | 'expired' | 'early' | 'pending'; + state: 'active' | 'expired' | 'early'; extraClaims?: Map; }) => { const claims = { sub: 'user_12345' } as Claims; const now = Math.floor(Date.now() / 1000); - const isAuthenticated = state === 'active' || state === 'pending'; - if (isAuthenticated) { + if (state === 'active') { claims.iat = now; claims.nbf = now - 10; claims.exp = now + 60; diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index 21877843df9..af70df88937 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -3,7 +3,6 @@ import * as http from 'node:http'; import { expect, test } from '@playwright/test'; import type { OrganizationSyncOptions } from '../../packages/backend/src/tokens/types'; -import type { AuthenticatedSessionResource } from '../../packages/types'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; import { generateConfig, getJwksFromSecretKey } from '../testUtils/handshake'; @@ -70,77 +69,75 @@ test.describe('Client handshake @generic', () => { await new Promise(resolve => jwksServer.close(() => resolve())); }); - (['active', 'pending'] satisfies Array).forEach(state => { - test(`standard signed-in with ${state} session - dev`, async () => { - const config = generateConfig({ mode: 'test' }); - const { token, claims } = config.generateToken({ state }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - 'Sec-Fetch-Dest': 'document', - }), - redirect: 'manual', - }); - expect(res.status).toBe(200); + test('standard signed-in - dev', async () => { + const config = generateConfig({ mode: 'test' }); + const { token, claims } = config.generateToken({ state: 'active' }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'Sec-Fetch-Dest': 'document', + }), + redirect: 'manual', }); + expect(res.status).toBe(200); + }); - test(`standard signed-in with ${state} - authorization header - dev`, async () => { - const config = generateConfig({ - mode: 'test', - }); - const { token, claims } = config.generateToken({ state }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `${devBrowserCookie} __client_uat=${clientUat};`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - Authorization: `Bearer ${token}`, - 'Sec-Fetch-Dest': 'document', - }), - redirect: 'manual', - }); - expect(res.status).toBe(200); + test('standard signed-in - authorization header - dev', async () => { + const config = generateConfig({ + mode: 'test', }); + const { token, claims } = config.generateToken({ state: 'active' }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat};`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Authorization: `Bearer ${token}`, + 'Sec-Fetch-Dest': 'document', + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); + }); - test(`standard signed-in with ${state} - prod`, async () => { - const config = generateConfig({ - mode: 'live', - }); - const { token, claims } = config.generateToken({ state }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `__client_uat=${clientUat}; __session=${token}`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - 'Sec-Fetch-Dest': 'document', - }), - redirect: 'manual', - }); - expect(res.status).toBe(200); + test('standard signed-in - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'active' }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'Sec-Fetch-Dest': 'document', + }), + redirect: 'manual', }); + expect(res.status).toBe(200); + }); - test(`standard signed-in with ${state} - authorization header - prod`, async () => { - const config = generateConfig({ - mode: 'live', - }); - const { token, claims } = config.generateToken({ state }); - const clientUat = claims.iat; - const res = await fetch(app.serverUrl + '/', { - headers: new Headers({ - Cookie: `__client_uat=${clientUat};`, - 'X-Publishable-Key': config.pk, - 'X-Secret-Key': config.sk, - Authorization: `Bearer ${token}`, - }), - redirect: 'manual', - }); - expect(res.status).toBe(200); + test('standard signed-in - authorization header - prod', async () => { + const config = generateConfig({ + mode: 'live', }); + const { token, claims } = config.generateToken({ state: 'active' }); + const clientUat = claims.iat; + const res = await fetch(app.serverUrl + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat};`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Authorization: `Bearer ${token}`, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); }); test('expired session token - dev', async () => { @@ -922,7 +919,7 @@ test.describe('Client handshake with organization activation @nextjs', () => { }; type When = { // With this initial state... - initialAuthState: 'active' | 'expired' | 'early' | 'pending'; + initialAuthState: 'active' | 'expired' | 'early'; initialSessionClaims: Map; // When the customer app specifies these orgSyncOptions to middleware... @@ -988,25 +985,6 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: 'org_a', }, }, - { - name: 'Pending session, no org in session, but org a requested by ID => attempts to activate org A', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([ - // Intentionally empty - ]), - orgSyncOptions: { - organizationPatterns: ['/organizations-by-id/:id'], - }, - appRequestPath: '/organizations-by-id/org_a', - tokenAppearsIn: 'cookie', - secFetchDestHeader: 'document', - }, - then: { - expectStatus: 307, - fapiOrganizationIdParamValue: 'org_a', - }, - }, // ---------------- Header-based auth tests ---------------- // Header-based auth requests come from non-browser actors, which don't have the __client cookie. @@ -1031,25 +1009,6 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: null, }, }, - { - name: 'Header-based auth should not handshake with pending auth', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([ - // Intentionally empty - ]), - orgSyncOptions: { - organizationPatterns: ['/organizations-by-id/:id'], - }, - appRequestPath: '/organizations-by-id/org_a', - tokenAppearsIn: 'header', - secFetchDestHeader: null, - }, - then: { - expectStatus: 200, - fapiOrganizationIdParamValue: null, - }, - }, { name: 'Header-based auth should not handshake with expired auth', when: { @@ -1070,7 +1029,7 @@ test.describe('Client handshake with organization activation @nextjs', () => { }, }, - // ---------------- Existing session org tests ---------------- + // ---------------- Existing session active org tests ---------------- { name: 'Active session, org A active in session, but org B is requested by ID => attempts to activate org B', when: { @@ -1154,70 +1113,6 @@ test.describe('Client handshake with organization activation @nextjs', () => { }, }, - { - name: 'Pending session, org A active in session, but org B is requested by ID => attempts to activate org B', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([['org_id', 'org_a']]), - orgSyncOptions: { - organizationPatterns: ['/organizations-by-id/:id', '/organizations-by-id/:id/(.*)'], - }, - appRequestPath: '/organizations-by-id/org_b', - tokenAppearsIn: 'cookie', - secFetchDestHeader: 'document', - }, - then: { - expectStatus: 307, - fapiOrganizationIdParamValue: 'org_b', - }, - }, - { - name: 'Pending session, no active org in session, but org B is requested by slug => attempts to activate org B', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([ - // Intentionally empty - ]), - orgSyncOptions: { - organizationPatterns: [ - '/organizations-by-id/:id', - '/organizations-by-id/:id/(.*)', - '/organizations-by-slug/:slug', - '/organizations-by-slug/:id/(.*)', - ], - }, - appRequestPath: '/organizations-by-slug/bcorp', - tokenAppearsIn: 'cookie', - secFetchDestHeader: 'document', - }, - then: { - expectStatus: 307, - fapiOrganizationIdParamValue: 'bcorp', - }, - }, - { - name: 'Pending session, org a in session, but *an org B subresource* is requested by slug => attempts to activate org B', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([['org_id', 'org_a']]), - orgSyncOptions: { - organizationPatterns: [ - '/organizations-by-slug/:slug', - '/organizations-by-slug/:id/(.*)', - '/organizations-by-id/:id', - '/organizations-by-id/:id/(.*)', - ], - }, - appRequestPath: '/organizations-by-slug/bcorp/settings', - tokenAppearsIn: 'cookie', - secFetchDestHeader: 'document', - }, - then: { - expectStatus: 307, - fapiOrganizationIdParamValue: 'bcorp', - }, - }, - // ---------------- Personal account tests ---------------- { name: 'Active session, org a in session, but *the personal account* is requested => attempts to activate PWS', @@ -1282,44 +1177,6 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: null, }, }, - { - name: 'Pending session, nothing session, and the personal account is requested => nothing to activate!', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([ - // Intentionally empty - ]), - orgSyncOptions: { - organizationPatterns: ['/organizations-by-slug/:slug', '/organizations-by-slug/:id/(.*)'], - personalAccountPatterns: ['/personal-account', '/personal-account/(.*)'], - }, - appRequestPath: '/personal-account', - tokenAppearsIn: 'cookie', - secFetchDestHeader: 'document', - }, - then: { - expectStatus: 200, - fapiOrganizationIdParamValue: null, - }, - }, - { - name: 'Pending session, org a active in session, and org a is requested => nothing to activate!', - when: { - initialAuthState: 'pending', - initialSessionClaims: new Map([['org_id', 'org_a']]), - orgSyncOptions: { - organizationPatterns: ['/organizations-by-id/:id', '/organizations-by-id/:id/(.*)'], - personalAccountPatterns: ['/personal-account', '/personal-account/(.*)'], - }, - appRequestPath: '/organizations-by-id/org_a', - tokenAppearsIn: 'cookie', - secFetchDestHeader: 'document', - }, - then: { - expectStatus: 200, - fapiOrganizationIdParamValue: null, - }, - }, { // NOTE(izaak): Would we prefer 500ing in this case? name: 'No config => nothing to activate, return 200', @@ -1481,7 +1338,7 @@ test.describe('Client handshake with an organization activation avoids infinite test('Ignores organization config when being redirected to', async () => { // Create a new map with an org_id key const { token, claims } = config.generateToken({ - state: 'active', + state: 'active', // Must be active - handshake logic only runs once session is determined to be active extraClaims: new Map([]), }); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index 1bf0a3d8069..a5d15c7f734 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -12,7 +12,7 @@ const _SignInAccountSwitcher = () => { const { userProfileUrl } = useEnvironment().displayConfig; const { afterSignInUrl, path: signInPath } = useSignInContext(); const { navigateAfterSignOut } = useSignOutContext(); - const { handleSignOutAllClicked, handleSessionClicked, activeSessions, handleAddAccountClicked } = + const { handleSignOutAllClicked, handleSessionClicked, authenticatedSessions, handleAddAccountClicked } = useMultisessionActions({ navigateAfterSignOut, afterSwitchSessionUrl: afterSignInUrl, @@ -40,7 +40,7 @@ const _SignInAccountSwitcher = () => { })} > - {activeSessions.map(s => ( + {authenticatedSessions.map(s => ( Date: Wed, 12 Feb 2025 17:45:45 -0300 Subject: [PATCH 07/17] Consider `pending` as authenticate state for client UAT --- packages/clerk-js/src/core/auth/cookies/clientUat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index ed87e2a6ae8..77ea0ecbf87 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -38,7 +38,7 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand // '0' indicates the user is signed out let val = '0'; - if (client && client.updatedAt && client.activeSessions.length > 0) { + if (client && client.updatedAt && client.authenticatedSessions.length > 0) { // truncate timestamp to seconds, since this is a unix timestamp val = Math.floor(client.updatedAt.getTime() / 1000).toString(); } From a22328f1e1454d879784fe0afc8541370b3a1636 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:47:35 -0300 Subject: [PATCH 08/17] Save JWT token on Expo for pending session status --- packages/expo/src/provider/singleton/createClerkInstance.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index a617471fe7d..40b794c9cc6 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -123,8 +123,8 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { if (client) { void ClientResourceCache.save(client.__internal_toSnapshot()); if (client.lastActiveSessionId) { - const lastActiveSession = client.activeSessions.find(s => s.id === client.lastActiveSessionId); - const token = lastActiveSession?.lastActiveToken?.getRawString(); + const currentSessionToken = client.authenticatedSessions.find(s => s.id === client.lastActiveSessionId); + const token = currentSessionToken?.lastActiveToken?.getRawString(); if (token) { void SessionJWTCache.save(token); } From cf8d19aafd06d5670e78b4652aeced73960fcb6e Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:54:57 -0300 Subject: [PATCH 09/17] Add `isAuthenticated` property to main Clerk instance --- packages/clerk-js/src/core/auth/cookies/clientUat.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 10 +++++++--- packages/clerk-js/src/core/resources/Client.ts | 4 ++++ packages/react/src/components/controlComponents.tsx | 5 ++--- packages/types/src/clerk.ts | 7 +++++++ packages/types/src/client.ts | 1 + packages/types/src/session.ts | 5 ++--- packages/vue/src/components/controlComponents.ts | 6 ++---- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index 77ea0ecbf87..876cb46aa4c 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -38,7 +38,7 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand // '0' indicates the user is signed out let val = '0'; - if (client && client.updatedAt && client.authenticatedSessions.length > 0) { + if (client && client.updatedAt && client.hasAuthenticated) { // truncate timestamp to seconds, since this is a unix timestamp val = Math.floor(client.updatedAt.getTime() / 1000).toString(); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 825fc608497..ce8bac739cb 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -288,6 +288,10 @@ export class Clerk implements ClerkInterface { return this.#options[key]; } + get hasAuthenticatedClient(): boolean { + return !!this.client?.hasAuthenticated; + } + public constructor(key: string, options?: DomainOrProxyUrl) { key = (key || '').trim(); @@ -2030,9 +2034,9 @@ export class Clerk implements ClerkInterface { #defaultSession = (client: ClientResource): AuthenticatedSessionResource | null => { if (client.lastActiveSessionId) { - const lastActiveSession = client.authenticatedSessions.find(s => s.id === client.lastActiveSessionId); - if (lastActiveSession) { - return lastActiveSession; + const currentSession = client.authenticatedSessions.find(s => s.id === client.lastActiveSessionId); + if (currentSession) { + return currentSession; } } const session = client.authenticatedSessions[0]; diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 8ff4e1b1902..3f19723959d 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -115,6 +115,10 @@ export class Client extends BaseResource implements ClientResource { return this._basePostBypass({ body: params, path: this.path() + '/verify' }); } + get hasAuthenticated() { + return (this.authenticatedSessions ?? []).length > 0; + } + fromJSON(data: ClientJSON | ClientJSONSnapshot | null): this { if (data) { this.id = data.id; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index e8b71d770d8..ba10366f9f2 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -142,11 +142,10 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect }; export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { - const { client, session } = clerk; - const hasActiveSessions = client.activeSessions && client.activeSessions.length > 0; + const { session, hasAuthenticatedClient } = clerk; React.useEffect(() => { - if (session === null && hasActiveSessions) { + if (session === null && hasAuthenticatedClient) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index dc381e83286..6390a529f1b 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -127,6 +127,13 @@ export interface Clerk { /** Clerk flag for loading Clerk in a standard browser setup */ isStandardBrowser: boolean | undefined; + /** + * Indicates whether the current user has a valid, fully authenticated client session. + * A session is considered valid when the user has successfully authenticated, + * completed all required authentication factors, and resolved all pending authentication tasks. + */ + hasAuthenticatedClient: boolean; + /** Client handling most Clerk operations. */ client: ClientResource | undefined; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index deebf5d1140..12c72689e9c 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -8,6 +8,7 @@ export interface ClientResource extends ClerkResource { sessions: SessionResource[]; activeSessions: ActiveSessionResource[]; authenticatedSessions: (ActiveSessionResource | PendingSessionResource)[]; + hasAuthenticated: boolean; signUp: SignUpResource; signIn: SignInResource; isNew: () => boolean; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 23cc9e411cf..11f8b839f7b 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -132,7 +132,7 @@ export interface SessionResource extends ClerkResource { } /** - * Represents a session resource that has completed all tasks + * Represents a session resource that has completed all pending tasks * and authentication factors */ export interface ActiveSessionResource extends SessionResource { @@ -141,8 +141,7 @@ export interface ActiveSessionResource extends SessionResource { } /** - * Represents a session resource that has pending tasks to be - * completed, eg: User has to select an organization + * Represents a session resource that is authenticated but has pending tasks */ export interface PendingSessionResource extends SessionResource { status: 'pending'; diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index 218e0c5b8cc..c48404d71b1 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -38,12 +38,10 @@ export const ClerkLoading = defineComponent((_, { slots }) => { }); export const RedirectToSignIn = defineComponent((props: RedirectOptions) => { - const { sessionCtx, clientCtx } = useClerkContext(); + const { sessionCtx } = useClerkContext(); useClerkLoaded(clerk => { - const hasActiveSessions = clientCtx.value?.activeSessions && clientCtx.value.activeSessions.length > 0; - - if (sessionCtx.value === null && hasActiveSessions) { + if (sessionCtx.value === null && clerk.hasAuthenticatedClient) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); From 8c88903d06f9529b9cd26e94a42bd6fa8e0f2242 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:53:31 -0300 Subject: [PATCH 10/17] Update `SignIn.SessionList` to include pending sessions --- .../ui/components/UserProfile/ActiveDevicesSection.tsx | 8 ++++++-- .../react/sign-in/choose-session/choose-session.hooks.ts | 8 +++++--- .../src/react/sign-in/choose-session/choose-session.tsx | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx index ecf1d31d9d3..28ebb104fc5 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx @@ -1,5 +1,5 @@ import { useReverification, useSession, useUser } from '@clerk/shared/react'; -import type { SessionWithActivitiesResource } from '@clerk/types'; +import type { AuthenticatedSessionResource, SessionWithActivitiesResource } from '@clerk/types'; import { Badge, Col, descriptors, Flex, Icon, localizationKeys, Text, useLocalizations } from '../../customizables'; import { FullHeightLoader, ProfileSection, ThreeDotsMenu } from '../../elements'; @@ -29,7 +29,7 @@ export const ActiveDevicesSection = () => { ) : ( sessions?.sort(currentSessionFirst(session!.id)).map(sa => { - if (!['active', 'pending'].includes(sa.status)) { + if (!isAuthenticated(sa.status)) { return null; } return ( @@ -45,6 +45,10 @@ export const ActiveDevicesSection = () => { ); }; +const isAuthenticated = (status: string): status is AuthenticatedSessionResource['status'] => { + return ['active', 'pending'].includes(status as AuthenticatedSessionResource['status']); +}; + const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) => { const isCurrent = useSession().session?.id === session.id; const status = useLoadingStatus(); diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts index dceea5ea7d2..76a4b719bfb 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts @@ -27,13 +27,15 @@ export type UseSignInActiveSessionListParams = { omitCurrent: boolean; }; -export function useSignInActiveSessionList(params?: UseSignInActiveSessionListParams): SignInActiveSessionListItem[] { +export function useSignInSessionList(params?: UseSignInActiveSessionListParams): SignInActiveSessionListItem[] { const { omitCurrent = true } = params || {}; return SignInRouterCtx.useSelector(state => { - const activeSessions = state.context.clerk?.client?.activeSessions || []; + const authenticatedSessions = state.context.clerk?.client?.authenticatedSessions || []; const currentSessionId = state.context.clerk?.session?.id; - const filteredSessions = omitCurrent ? activeSessions.filter(s => s.id !== currentSessionId) : activeSessions; + const filteredSessions = omitCurrent + ? authenticatedSessions.filter(s => s.id !== currentSessionId) + : authenticatedSessions; return filteredSessions.map(s => ({ id: s.id, diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.tsx b/packages/elements/src/react/sign-in/choose-session/choose-session.tsx index 020464b9bd8..6ce9860e74e 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.tsx +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.tsx @@ -8,8 +8,8 @@ import { SignInActiveSessionContext, type SignInActiveSessionListItem, useSignInActiveSessionContext, - useSignInActiveSessionList, useSignInChooseSessionIsActive, + useSignInSessionList, } from './choose-session.hooks'; // ----------------------------------- TYPES ------------------------------------ @@ -44,7 +44,7 @@ export function SignInChooseSession({ asChild, children, ...props }: SignInChoos } export function SignInSessionList({ asChild, children, includeCurrentSession, ...props }: SignInSessionListProps) { - const sessions = useSignInActiveSessionList({ omitCurrent: !includeCurrentSession }); + const sessions = useSignInSessionList({ omitCurrent: !includeCurrentSession }); if (!children || !sessions?.length) { return null; From 0b7e5fe585346451ec27785fb47ecce1543c65ea Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:01:01 -0300 Subject: [PATCH 11/17] Deprecate `activeSessions` --- .../clerk-js/src/core/__tests__/clerk.redirects.test.ts | 2 +- packages/clerk-js/src/core/__tests__/clerk.test.ts | 1 - packages/clerk-js/src/core/resources/Client.ts | 3 +++ packages/react/src/isomorphicClerk.ts | 8 ++++++++ packages/types/src/client.ts | 5 ++++- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 339dd41e783..61c31e0133d 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -91,7 +91,7 @@ describe('Clerk singleton - Redirects', () => { beforeEach(() => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + authenticatedSessions: [], }), ); }); diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index b55061bf962..c8086328a26 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -127,7 +127,6 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], authenticatedSessions: [], }), ); diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 3f19723959d..45b7584202f 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -54,6 +54,9 @@ export class Client extends BaseResource implements ClientResource { return this.signIn; } + /** + * @deprecated Use `authenticatedSessions` instead + */ get activeSessions(): ActiveSessionResource[] { return this.sessions.filter(s => s.status === 'active') as ActiveSessionResource[]; } diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index b6d088a9c2d..a66350e4845 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -658,6 +658,14 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } + get hasAuthenticatedClient(): boolean { + if (this.clerkjs) { + return this.clerkjs.hasAuthenticatedClient; + } else { + return false; + } + } + __unstable__setEnvironment(...args: any): void { if (this.clerkjs && '__unstable__setEnvironment' in this.clerkjs) { (this.clerkjs as any).__unstable__setEnvironment(args); diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 12c72689e9c..3c395edef1d 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -6,7 +6,6 @@ import type { ClientJSONSnapshot } from './snapshots'; export interface ClientResource extends ClerkResource { sessions: SessionResource[]; - activeSessions: ActiveSessionResource[]; authenticatedSessions: (ActiveSessionResource | PendingSessionResource)[]; hasAuthenticated: boolean; signUp: SignUpResource; @@ -25,4 +24,8 @@ export interface ClientResource extends ClerkResource { createdAt: Date | null; updatedAt: Date | null; __internal_toSnapshot: () => ClientJSONSnapshot; + /** + * @deprecated Use `authenticatedSessions` instead + */ + activeSessions: ActiveSessionResource[]; } From 6f3a0a7235546a3045ff92fe901bb4a7d4e39a58 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:45:27 -0300 Subject: [PATCH 12/17] Add changeset --- .changeset/proud-cycles-roll.md | 33 +++++++++++++++++++ .../__tests__/choose-session.test.tsx | 2 +- .../choose-session/choose-session.hooks.ts | 4 +-- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 .changeset/proud-cycles-roll.md diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md new file mode 100644 index 00000000000..bc191870241 --- /dev/null +++ b/.changeset/proud-cycles-roll.md @@ -0,0 +1,33 @@ +--- +'@clerk/clerk-js': minor +'@clerk/elements': patch +'@clerk/shared': patch +'@clerk/astro': patch +'@clerk/clerk-react': patch +'@clerk/types': patch +'@clerk/clerk-expo': patch +'@clerk/vue': patch +--- + +### @clerk/clerk-js + +- Introduce a new client session status: `pending` +- Handle and initialize a `pending` session as an authenticated state, similarity as `active` + +[TODO - Add more details here on DX changes such as `activeSessions` deprecation after tacking code review feedback] + +```diff +- if (Clerk.user) { ++ if (clerk.hasAuthenticatedClient) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) +} else { + // ... +} +``` diff --git a/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx b/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx index e3a7b8b7cb0..c39f64627dc 100644 --- a/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx +++ b/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx @@ -8,7 +8,7 @@ import * as Hooks from '../choose-session.hooks'; describe('SignInSessionList/SignInSessionListItem', () => { beforeAll(() => { jest.spyOn(Hooks, 'useSignInChooseSessionIsActive').mockImplementation(() => true); - jest.spyOn(Hooks, 'useSignInActiveSessionList').mockImplementation(() => [ + jest.spyOn(Hooks, 'useSignInSessionList').mockImplementation(() => [ { id: 'abc123', firstName: 'firstName', diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts index 76a4b719bfb..79b1e673c41 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts @@ -23,11 +23,11 @@ export function useSignInChooseSessionIsActive() { return useActiveTags(routerRef, 'step:choose-session'); } -export type UseSignInActiveSessionListParams = { +export type useSignInSessionListParams = { omitCurrent: boolean; }; -export function useSignInSessionList(params?: UseSignInActiveSessionListParams): SignInActiveSessionListItem[] { +export function useSignInSessionList(params?: useSignInSessionListParams): SignInActiveSessionListItem[] { const { omitCurrent = true } = params || {}; return SignInRouterCtx.useSelector(state => { From b344ea5c33d11cb8ec14ae6e35bbabf227337390 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:16:02 -0300 Subject: [PATCH 13/17] Introduce separate changesets --- .changeset/nasty-mangos-live.md | 11 +++++++++++ .changeset/proud-cycles-roll.md | 13 +------------ packages/clerk-js/src/core/__tests__/clerk.test.ts | 4 ++-- packages/types/src/session.ts | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 .changeset/nasty-mangos-live.md diff --git a/.changeset/nasty-mangos-live.md b/.changeset/nasty-mangos-live.md new file mode 100644 index 00000000000..2222cfac762 --- /dev/null +++ b/.changeset/nasty-mangos-live.md @@ -0,0 +1,11 @@ +--- +'@clerk/elements': minor +'@clerk/shared': minor +'@clerk/astro': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +'@clerk/clerk-expo': minor +'@clerk/vue': minor +--- + +Surface new `pending` session as an authenticated state, similarly as `active` diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md index bc191870241..0e567ee9f11 100644 --- a/.changeset/proud-cycles-roll.md +++ b/.changeset/proud-cycles-roll.md @@ -1,20 +1,9 @@ --- '@clerk/clerk-js': minor -'@clerk/elements': patch -'@clerk/shared': patch -'@clerk/astro': patch -'@clerk/clerk-react': patch -'@clerk/types': patch -'@clerk/clerk-expo': patch -'@clerk/vue': patch --- -### @clerk/clerk-js - - Introduce a new client session status: `pending` -- Handle and initialize a `pending` session as an authenticated state, similarity as `active` - -[TODO - Add more details here on DX changes such as `activeSessions` deprecation after tacking code review feedback] +- Initialize a `pending` session as an authenticated state ```diff - if (Clerk.user) { diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index c8086328a26..e6ec63c4f38 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -344,7 +344,7 @@ describe('Clerk singleton', () => { it('sets active organization by slug', async () => { const mockSession2 = { id: '1', - status: 'active', + status, user: { organizationMemberships: [ { @@ -1287,7 +1287,7 @@ describe('Clerk singleton', () => { const mockSession = { id: sessionId, remove: jest.fn(), - status: 'active', + status, user: {}, touch: jest.fn(() => Promise.resolve()), getToken: jest.fn(), diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 11f8b839f7b..99a2429bae7 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -150,7 +150,7 @@ export interface PendingSessionResource extends SessionResource { /** * Represents session resources for users who have completed - * the full authentication flow. + * the full authentication flow */ export type AuthenticatedSessionResource = ActiveSessionResource | PendingSessionResource; From a79b33995ae99dd96cfe5fd7b6bf491e93b6841b Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:09:32 -0300 Subject: [PATCH 14/17] Refactor from `isAuthenticated` to `isSignedIn` --- .changeset/nasty-mangos-live.md | 2 +- .changeset/proud-cycles-roll.md | 6 +- packages/astro/src/stores/internal.ts | 4 +- .../core/__tests__/clerk.redirects.test.ts | 2 +- .../clerk-js/src/core/__tests__/clerk.test.ts | 110 +++++++++--------- .../src/core/auth/cookies/clientUat.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 30 ++--- .../clerk-js/src/core/resources/Client.ts | 12 +- .../ImpersonationFab/ImpersonationFab.tsx | 4 +- .../SignIn/SignInAccountSwitcher.tsx | 4 +- .../components/UserButton/SessionActions.tsx | 14 +-- .../UserButton/UserButtonPopover.tsx | 4 +- .../UserButton/useMultisessionActions.tsx | 10 +- .../UserProfile/ActiveDevicesSection.tsx | 10 +- .../src/ui/hooks/useMultipleSessions.ts | 4 +- .../choose-session/choose-session.hooks.ts | 6 +- .../provider/singleton/createClerkInstance.ts | 4 +- .../src/components/controlComponents.tsx | 4 +- packages/react/src/isomorphicClerk.ts | 8 +- packages/shared/src/deriveState.ts | 4 +- packages/shared/src/react/contexts.tsx | 4 +- packages/types/src/clerk.ts | 20 ++-- packages/types/src/client.ts | 6 +- packages/types/src/hooks.ts | 4 +- packages/types/src/session.ts | 4 +- .../vue/src/components/controlComponents.ts | 2 +- packages/vue/src/types.ts | 4 +- 27 files changed, 144 insertions(+), 144 deletions(-) diff --git a/.changeset/nasty-mangos-live.md b/.changeset/nasty-mangos-live.md index 2222cfac762..381c0a124b4 100644 --- a/.changeset/nasty-mangos-live.md +++ b/.changeset/nasty-mangos-live.md @@ -8,4 +8,4 @@ '@clerk/vue': minor --- -Surface new `pending` session as an authenticated state, similarly as `active` +Surface new `pending` session as an signed-in state, similarly as `active` diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md index 0e567ee9f11..fd3016b36e4 100644 --- a/.changeset/proud-cycles-roll.md +++ b/.changeset/proud-cycles-roll.md @@ -2,12 +2,12 @@ '@clerk/clerk-js': minor --- -- Introduce a new client session status: `pending` -- Initialize a `pending` session as an authenticated state +- Initialize new `pending` session status as an signed-in state +- Add new `Clerk.isSignedIn` property to provide a more explicit way to check authentication status, replacing the previous `Clerk.user` check ```diff - if (Clerk.user) { -+ if (clerk.hasAuthenticatedClient) { ++ if (Clerk.isSignedIn) { // Mount user button component document.getElementById('signed-in').innerHTML = `
diff --git a/packages/astro/src/stores/internal.ts b/packages/astro/src/stores/internal.ts index ab9723642f4..4f9a9d50dc4 100644 --- a/packages/astro/src/stores/internal.ts +++ b/packages/astro/src/stores/internal.ts @@ -1,9 +1,9 @@ import type { - AuthenticatedSessionResource, Clerk, ClientResource, InitialState, OrganizationResource, + SignedInSessionResource, UserResource, } from '@clerk/types'; import { atom, map } from 'nanostores'; @@ -12,7 +12,7 @@ export const $csrState = map<{ isLoaded: boolean; client: ClientResource | undefined | null; user: UserResource | undefined | null; - session: AuthenticatedSessionResource | undefined | null; + session: SignedInSessionResource | undefined | null; organization: OrganizationResource | undefined | null; }>({ isLoaded: false, diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 61c31e0133d..c744fffd27f 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -91,7 +91,7 @@ describe('Clerk singleton - Redirects', () => { beforeEach(() => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], }), ); }); diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index e6ec63c4f38..dc233ebfbc7 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -1,6 +1,6 @@ import type { ActiveSessionResource, - AuthenticatedSessionResource, + SignedInSessionResource, SignInJSON, SignUpJSON, TokenResource, @@ -127,7 +127,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], }), ); @@ -155,7 +155,7 @@ describe('Clerk singleton', () => { }); describe('.setActive', () => { - describe.each(['active', 'pending'] satisfies Array)( + describe.each(['active', 'pending'] satisfies Array)( 'when session has %s status', status => { const mockSession = { @@ -185,7 +185,7 @@ describe('Clerk singleton', () => { it('does not call session touch on signOut', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -198,7 +198,7 @@ describe('Clerk singleton', () => { it('calls session.touch by default', async () => { mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -208,7 +208,7 @@ describe('Clerk singleton', () => { it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); mockSession.getToken.mockResolvedValue('mocked-token'); const sut = new Clerk(productionPublishableKey); @@ -222,7 +222,7 @@ describe('Clerk singleton', () => { it('calls __unstable__onBeforeSetActive before session.touch', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); (window as any).__unstable__onBeforeSetActive = () => { expect(mockSession.touch).not.toHaveBeenCalled(); @@ -236,7 +236,7 @@ describe('Clerk singleton', () => { it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); (window as any).__unstable__onBeforeSetActive = () => { expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken }); @@ -250,7 +250,7 @@ describe('Clerk singleton', () => { it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => { const beforeEmitMock = jest.fn(); mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); (window as any).__unstable__onAfterSetActive = () => { expect(mockSession.touch).toHaveBeenCalled(); @@ -274,7 +274,7 @@ describe('Clerk singleton', () => { }; mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession, mockSession2], + signedInSessions: [mockSession, mockSession2], }), ); @@ -309,7 +309,7 @@ describe('Clerk singleton', () => { // TODO: @dimkl include set transitive state it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -359,7 +359,7 @@ describe('Clerk singleton', () => { touch: jest.fn(), getToken: jest.fn(), }; - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession2] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession2] })); const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -383,7 +383,7 @@ describe('Clerk singleton', () => { mockSession.touch.mockReturnValue(Promise.resolve()); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession], + signedInSessions: [mockSession], cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now isEligibleForTouch: () => true, buildTouchUrl: () => @@ -407,7 +407,7 @@ describe('Clerk singleton', () => { mockSession.touch.mockReturnValue(Promise.resolve()); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession], + signedInSessions: [mockSession], cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now isEligibleForTouch: () => false, buildTouchUrl: () => @@ -429,7 +429,7 @@ describe('Clerk singleton', () => { mockSession.touch.mockReturnValue(Promise.resolve()); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession], + signedInSessions: [mockSession], cookieExpiresAt: null, isEligibleForTouch: () => false, buildTouchUrl: () => @@ -449,7 +449,7 @@ describe('Clerk singleton', () => { mockNativeRuntime(() => { it('calls session.touch in a non-standard browser', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const sut = new Clerk(productionPublishableKey); await sut.load({ standardBrowser: false }); @@ -480,7 +480,7 @@ describe('Clerk singleton', () => { }); describe('.load()', () => { - describe.each(['active', 'pending'] satisfies Array)( + describe.each(['active', 'pending'] satisfies Array)( 'when session has %s status', status => { const mockSession = { @@ -500,7 +500,7 @@ describe('Clerk singleton', () => { it('gracefully handles an incorrect value returned from the user provided selectInitialSession', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], }), ); @@ -518,7 +518,7 @@ describe('Clerk singleton', () => { }); it('updates auth cookie on load from fetched session', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -527,7 +527,7 @@ describe('Clerk singleton', () => { }); it('updates auth cookie on token:update event', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ authenticatedSessions: [mockSession] })); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -563,7 +563,7 @@ describe('Clerk singleton', () => { const sut = new Clerk(productionPublishableKey); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], destroy: mockClientDestroy, }), @@ -579,7 +579,7 @@ describe('Clerk singleton', () => { it('signs out all sessions if no sessionId is passed and multiple sessions have authenticated status', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession1, mockSession2, mockSession3], + signedInSessions: [mockSession1, mockSession2, mockSession3], sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, removeSessions: mockClientRemoveSessions, @@ -600,12 +600,12 @@ describe('Clerk singleton', () => { }); }); - it.each(['active', 'pending'] satisfies Array)( + it.each(['active', 'pending'] satisfies Array)( 'signs out all sessions if no sessionId is passed and only one session has %s status', async status => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [{ ...mockSession1, status }], + signedInSessions: [{ ...mockSession1, status }], sessions: [{ ...mockSession1, status }], destroy: mockClientDestroy, removeSessions: mockClientRemoveSessions, @@ -631,7 +631,7 @@ describe('Clerk singleton', () => { it('only removes the session that corresponds to the passed sessionId if it is not the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession1, mockSession2, mockSession3], + signedInSessions: [mockSession1, mockSession2, mockSession3], sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), @@ -653,7 +653,7 @@ describe('Clerk singleton', () => { it('removes and signs out the session that corresponds to the passed sessionId if it is the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession1, mockSession2, mockSession3], + signedInSessions: [mockSession1, mockSession2, mockSession3], sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), @@ -676,7 +676,7 @@ describe('Clerk singleton', () => { it('removes and signs out the session and redirects to the provided redirectUrl ', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [mockSession1, mockSession2, mockSession3], + signedInSessions: [mockSession1, mockSession2, mockSession3], sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), @@ -797,7 +797,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -856,7 +856,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -918,7 +918,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -980,7 +980,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1047,7 +1047,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1098,7 +1098,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -1153,7 +1153,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_second_factor', first_factor_verification: { @@ -1193,7 +1193,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_second_factor', first_factor_verification: { @@ -1247,7 +1247,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ sessions: [mockSession], - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1308,7 +1308,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ sessions: [mockSession], - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1360,7 +1360,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1408,7 +1408,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1456,7 +1456,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1497,7 +1497,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1544,7 +1544,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_first_factor', } as unknown as SignInJSON), @@ -1576,7 +1576,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1624,7 +1624,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1684,7 +1684,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_first_factor', first_factor_verification: { @@ -1734,7 +1734,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_new_password', } as unknown as SignInJSON), @@ -1782,7 +1782,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [{ id: createdSessionId }], signIn: new SignIn({ status: 'completed', @@ -1811,7 +1811,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'verified']]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], signIn: new SignIn({ status: 'needs_second_factor', @@ -1842,7 +1842,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [{ id: createdSessionId }], signUp: new SignUp({ status: 'completed', @@ -1871,7 +1871,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'verified']]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp({ status: 'missing_requirements', @@ -1898,7 +1898,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'expired']]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1920,7 +1920,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'failed']]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1945,7 +1945,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1968,7 +1968,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_created_session', 'sess_123']]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1991,7 +1991,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - authenticatedSessions: [], + signedInSessions: [], sessions: [{ id: 'sess_123' }], signIn: new SignIn({ status: 'completed', diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index 876cb46aa4c..85ecd676563 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -38,7 +38,7 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand // '0' indicates the user is signed out let val = '0'; - if (client && client.updatedAt && client.hasAuthenticated) { + if (client && client.updatedAt && client.isSignedIn) { // truncate timestamp to seconds, since this is a unix timestamp val = Math.floor(client.updatedAt.getTime() / 1000).toString(); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ce8bac739cb..efd89d7a426 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -10,7 +10,6 @@ import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, - AuthenticatedSessionResource, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, @@ -47,6 +46,7 @@ import type { Resources, SDKMetadata, SetActiveParams, + SignedInSessionResource, SignInProps, SignInRedirectOptions, SignInResource, @@ -164,7 +164,7 @@ export class Clerk implements ClerkInterface { }; public client: ClientResource | undefined; - public session: AuthenticatedSessionResource | null | undefined; + public session: SignedInSessionResource | null | undefined; public organization: OrganizationResource | null | undefined; public user: UserResource | null | undefined; public __internal_country?: string | null; @@ -288,8 +288,8 @@ export class Clerk implements ClerkInterface { return this.#options[key]; } - get hasAuthenticatedClient(): boolean { - return !!this.client?.hasAuthenticated; + get isSignedIn(): boolean { + return !!this.client?.isSignedIn; } public constructor(key: string, options?: DomainOrProxyUrl) { @@ -391,7 +391,7 @@ export class Clerk implements ClerkInterface { }); }; - if (!opts.sessionId || this.client.authenticatedSessions.length === 1) { + if (!opts.sessionId || this.client.signedInSessions.length === 1) { if (this.#options.experimental?.persistClient ?? true) { await this.client.removeSessions(); } else { @@ -401,7 +401,7 @@ export class Clerk implements ClerkInterface { return handleSetActive(); } - const session = this.client.authenticatedSessions.find(s => s.id === opts.sessionId); + const session = this.client.signedInSessions.find(s => s.id === opts.sessionId); const shouldSignOutCurrent = session?.id && this.session?.id === session.id; await session?.remove(); if (shouldSignOutCurrent) { @@ -881,12 +881,12 @@ export class Clerk implements ClerkInterface { : noop; if (typeof session === 'string') { - session = (this.client.sessions.find(x => x.id === session) as AuthenticatedSessionResource) || null; + session = (this.client.sessions.find(x => x.id === session) as SignedInSessionResource) || null; } let newSession = session === undefined ? this.session : session; - // At this point, the `session` variable should contain either an `AuthenticatedSessionResource` + // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. // We now want to set the last active organization id on that session (if it exists). // However, if the `organization` parameter is not given (i.e. `undefined`), we want @@ -2032,14 +2032,14 @@ export class Clerk implements ClerkInterface { this.#emit(); }; - #defaultSession = (client: ClientResource): AuthenticatedSessionResource | null => { + #defaultSession = (client: ClientResource): SignedInSessionResource | null => { if (client.lastActiveSessionId) { - const currentSession = client.authenticatedSessions.find(s => s.id === client.lastActiveSessionId); + const currentSession = client.signedInSessions.find(s => s.id === client.lastActiveSessionId); if (currentSession) { return currentSession; } } - const session = client.authenticatedSessions[0]; + const session = client.signedInSessions[0]; return session || null; }; @@ -2073,7 +2073,7 @@ export class Clerk implements ClerkInterface { }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc - #touchCurrentSession = async (session?: AuthenticatedSessionResource | null): Promise => { + #touchCurrentSession = async (session?: SignedInSessionResource | null): Promise => { if (!session || !this.#options.touchSession) { return Promise.resolve(); } @@ -2122,14 +2122,14 @@ export class Clerk implements ClerkInterface { ); }; - #setAccessors = (session?: AuthenticatedSessionResource | null) => { + #setAccessors = (session?: SignedInSessionResource | null) => { this.session = session || null; this.organization = this.#getLastActiveOrganizationFromSession(); this.#aliasUser(); }; - #getSessionFromClient = (sessionId: string | undefined): AuthenticatedSessionResource | null => { - return this.client?.authenticatedSessions.find(x => x.id === sessionId) || null; + #getSessionFromClient = (sessionId: string | undefined): SignedInSessionResource | null => { + return this.client?.signedInSessions.find(x => x.id === sessionId) || null; }; #aliasUser = () => { diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 45b7584202f..b3164fb300a 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -1,9 +1,9 @@ import { type ActiveSessionResource, - type AuthenticatedSessionResource, type ClientJSON, type ClientJSONSnapshot, type ClientResource, + type SignedInSessionResource, type SignInResource, type SignUpResource, } from '@clerk/types'; @@ -55,14 +55,14 @@ export class Client extends BaseResource implements ClientResource { } /** - * @deprecated Use `authenticatedSessions` instead + * @deprecated Use `signedInSessions` instead */ get activeSessions(): ActiveSessionResource[] { return this.sessions.filter(s => s.status === 'active') as ActiveSessionResource[]; } - get authenticatedSessions(): AuthenticatedSessionResource[] { - return this.sessions.filter(s => s.status === 'active' || s.status === 'pending') as AuthenticatedSessionResource[]; + get signedInSessions(): SignedInSessionResource[] { + return this.sessions.filter(s => s.status === 'active' || s.status === 'pending') as SignedInSessionResource[]; } create(): Promise { @@ -118,8 +118,8 @@ export class Client extends BaseResource implements ClientResource { return this._basePostBypass({ body: params, path: this.path() + '/verify' }); } - get hasAuthenticated() { - return (this.authenticatedSessions ?? []).length > 0; + get isSignedIn() { + return (this.signedInSessions ?? []).length > 0; } fromJSON(data: ClientJSON | ClientJSONSnapshot | null): this { diff --git a/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx b/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx index ccac75ed8af..4c606c06498 100644 --- a/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx +++ b/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx @@ -1,5 +1,5 @@ import { useClerk, useSession, useUser } from '@clerk/shared/react'; -import type { AuthenticatedSessionResource } from '@clerk/types'; +import type { SignedInSessionResource } from '@clerk/types'; import type { PointerEventHandler } from 'react'; import React, { useEffect, useRef } from 'react'; @@ -66,7 +66,7 @@ const FabContent = ({ title, signOutText }: FabContentProps) => { const { otherSessions } = useMultipleSessions({ user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); - const handleSignOutSessionClicked = (session: AuthenticatedSessionResource) => () => { + const handleSignOutSessionClicked = (session: SignedInSessionResource) => () => { if (otherSessions.length === 0) { return signOut(navigateAfterSignOut); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index a5d15c7f734..f5f8e6d0ee3 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -12,7 +12,7 @@ const _SignInAccountSwitcher = () => { const { userProfileUrl } = useEnvironment().displayConfig; const { afterSignInUrl, path: signInPath } = useSignInContext(); const { navigateAfterSignOut } = useSignOutContext(); - const { handleSignOutAllClicked, handleSessionClicked, authenticatedSessions, handleAddAccountClicked } = + const { handleSignOutAllClicked, handleSessionClicked, signedInSessions, handleAddAccountClicked } = useMultisessionActions({ navigateAfterSignOut, afterSwitchSessionUrl: afterSignInUrl, @@ -40,7 +40,7 @@ const _SignInAccountSwitcher = () => { })} > - {authenticatedSessions.map(s => ( + {signedInSessions.map(s => ( Promise | void; - handleSignOutSessionClicked: (session: AuthenticatedSessionResource) => () => Promise | void; + handleSignOutSessionClicked: (session: SignedInSessionResource) => () => Promise | void; handleUserProfileActionClicked: (startPath?: string) => Promise | void; - session: AuthenticatedSessionResource; + session: SignedInSessionResource; completedCallback: () => void; }; @@ -113,12 +113,12 @@ export const SingleSessionActions = (props: SingleSessionActionsProps) => { type MultiSessionActionsProps = { handleManageAccountClicked: () => Promise | void; - handleSignOutSessionClicked: (session: AuthenticatedSessionResource) => () => Promise | void; - handleSessionClicked: (session: AuthenticatedSessionResource) => () => Promise | void; + handleSignOutSessionClicked: (session: SignedInSessionResource) => () => Promise | void; + handleSessionClicked: (session: SignedInSessionResource) => () => Promise | void; handleAddAccountClicked: () => Promise | void; handleUserProfileActionClicked: (startPath?: string) => Promise | void; - session: AuthenticatedSessionResource; - otherSessions: AuthenticatedSessionResource[]; + session: SignedInSessionResource; + otherSessions: SignedInSessionResource[]; completedCallback: () => void; }; diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx index af4249b3b05..1b3fe1a0077 100644 --- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx @@ -1,5 +1,5 @@ import { useSession, useUser } from '@clerk/shared/react'; -import type { AuthenticatedSessionResource } from '@clerk/types'; +import type { SignedInSessionResource } from '@clerk/types'; import React from 'react'; import { useEnvironment, useUserButtonContext } from '../../contexts'; @@ -14,7 +14,7 @@ type UserButtonPopoverProps = { close?: (open: boolean) => void } & PropsOfCompo export const UserButtonPopover = React.forwardRef((props, ref) => { const { close: unsafeClose, ...rest } = props; const close = () => unsafeClose?.(false); - const { session } = useSession() as { session: AuthenticatedSessionResource }; + const { session } = useSession() as { session: SignedInSessionResource }; const userButtonContext = useUserButtonContext(); const { __experimental_asStandalone } = userButtonContext; const { authConfig } = useEnvironment(); diff --git a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx index 837d3b8ca22..0b824e2999b 100644 --- a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx @@ -1,5 +1,5 @@ import { useClerk } from '@clerk/shared/react'; -import type { AuthenticatedSessionResource, UserButtonProps, UserResource } from '@clerk/types'; +import type { SignedInSessionResource, UserButtonProps, UserResource } from '@clerk/types'; import { windowNavigate } from '../../../utils/windowNavigate'; import { useCardState } from '../../elements'; @@ -20,10 +20,10 @@ type UseMultisessionActionsParams = { export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { const { setActive, signOut, openUserProfile } = useClerk(); const card = useCardState(); - const { authenticatedSessions, otherSessions } = useMultipleSessions({ user: opts.user }); + const { signedInSessions, otherSessions } = useMultipleSessions({ user: opts.user }); const { navigate } = useRouter(); - const handleSignOutSessionClicked = (session: AuthenticatedSessionResource) => () => { + const handleSignOutSessionClicked = (session: SignedInSessionResource) => () => { if (otherSessions.length === 0) { return signOut(opts.navigateAfterSignOut); } @@ -66,7 +66,7 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { return signOut(opts.navigateAfterSignOut); }; - const handleSessionClicked = (session: AuthenticatedSessionResource) => async () => { + const handleSessionClicked = (session: SignedInSessionResource) => async () => { card.setLoading(); return setActive({ session, redirectUrl: opts.afterSwitchSessionUrl }).finally(() => { card.setIdle(); @@ -87,6 +87,6 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { handleSessionClicked, handleAddAccountClicked, otherSessions, - authenticatedSessions, + signedInSessions, }; }; diff --git a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx index 28ebb104fc5..d26820b64fe 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx @@ -1,5 +1,5 @@ import { useReverification, useSession, useUser } from '@clerk/shared/react'; -import type { AuthenticatedSessionResource, SessionWithActivitiesResource } from '@clerk/types'; +import type { SessionWithActivitiesResource, SignedInSessionResource } from '@clerk/types'; import { Badge, Col, descriptors, Flex, Icon, localizationKeys, Text, useLocalizations } from '../../customizables'; import { FullHeightLoader, ProfileSection, ThreeDotsMenu } from '../../elements'; @@ -29,7 +29,7 @@ export const ActiveDevicesSection = () => { ) : ( sessions?.sort(currentSessionFirst(session!.id)).map(sa => { - if (!isAuthenticated(sa.status)) { + if (!isSignedInStatus(sa.status)) { return null; } return ( @@ -45,8 +45,10 @@ export const ActiveDevicesSection = () => { ); }; -const isAuthenticated = (status: string): status is AuthenticatedSessionResource['status'] => { - return ['active', 'pending'].includes(status as AuthenticatedSessionResource['status']); +const isSignedInStatus = (status: string): status is SignedInSessionResource['status'] => { + return (['active', 'pending'] satisfies Array).includes( + status as SignedInSessionResource['status'], + ); }; const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) => { diff --git a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts index 0f134c236c9..747273411ce 100644 --- a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts +++ b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts @@ -9,8 +9,8 @@ const useMultipleSessions = (params: UseMultipleSessionsParam) => { const clerk = useClerk(); return { - authenticatedSessions: clerk.client.authenticatedSessions, - otherSessions: clerk.client.authenticatedSessions.filter(s => s.user?.id !== params.user?.id), + signedInSessions: clerk.client.signedInSessions, + otherSessions: clerk.client.signedInSessions.filter(s => s.user?.id !== params.user?.id), }; }; diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts index 79b1e673c41..be17b76959f 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts @@ -31,11 +31,9 @@ export function useSignInSessionList(params?: useSignInSessionListParams): SignI const { omitCurrent = true } = params || {}; return SignInRouterCtx.useSelector(state => { - const authenticatedSessions = state.context.clerk?.client?.authenticatedSessions || []; + const signedInSessions = state.context.clerk?.client?.signedInSessions || []; const currentSessionId = state.context.clerk?.session?.id; - const filteredSessions = omitCurrent - ? authenticatedSessions.filter(s => s.id !== currentSessionId) - : authenticatedSessions; + const filteredSessions = omitCurrent ? signedInSessions.filter(s => s.id !== currentSessionId) : signedInSessions; return filteredSessions.map(s => ({ id: s.id, diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index 40b794c9cc6..b62d3d42c50 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -123,8 +123,8 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { if (client) { void ClientResourceCache.save(client.__internal_toSnapshot()); if (client.lastActiveSessionId) { - const currentSessionToken = client.authenticatedSessions.find(s => s.id === client.lastActiveSessionId); - const token = currentSessionToken?.lastActiveToken?.getRawString(); + const currentSession = client.signedInSessions.find(s => s.id === client.lastActiveSessionId); + const token = currentSession?.lastActiveToken?.getRawString(); if (token) { void SessionJWTCache.save(token); } diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index ba10366f9f2..81d336ed614 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -142,10 +142,10 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect }; export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { - const { session, hasAuthenticatedClient } = clerk; + const { session, isSignedIn } = clerk; React.useEffect(() => { - if (session === null && hasAuthenticatedClient) { + if (session === null && isSignedIn) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index a66350e4845..61b8c439cc9 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -5,7 +5,6 @@ import { handleValueOrFn } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, __internal_UserVerificationProps, - AuthenticatedSessionResource, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, @@ -31,6 +30,7 @@ import type { RedirectOptions, SDKMetadata, SetActiveParams, + SignedInSessionResource, SignInProps, SignInRedirectOptions, SignInResource, @@ -616,7 +616,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } - get session(): AuthenticatedSessionResource | undefined | null { + get session(): SignedInSessionResource | undefined | null { if (this.clerkjs) { return this.clerkjs.session; } else { @@ -658,9 +658,9 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } - get hasAuthenticatedClient(): boolean { + get isSignedIn(): boolean { if (this.clerkjs) { - return this.clerkjs.hasAuthenticatedClient; + return this.clerkjs.isSignedIn; } else { return false; } diff --git a/packages/shared/src/deriveState.ts b/packages/shared/src/deriveState.ts index 608c015277e..d23c70ffe31 100644 --- a/packages/shared/src/deriveState.ts +++ b/packages/shared/src/deriveState.ts @@ -1,10 +1,10 @@ import type { - AuthenticatedSessionResource, InitialState, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, Resources, + SignedInSessionResource, UserResource, } from '@clerk/types'; @@ -22,7 +22,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const userId = initialState.userId; const user = initialState.user as UserResource; const sessionId = initialState.sessionId; - const session = initialState.session as AuthenticatedSessionResource; + const session = initialState.session as SignedInSessionResource; const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as OrganizationCustomRoleKey; diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index 332d97f2935..e3170145c09 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -1,11 +1,11 @@ 'use client'; import type { - AuthenticatedSessionResource, ClerkOptions, ClientResource, LoadedClerk, OrganizationResource, + SignedInSessionResource, UserResource, } from '@clerk/types'; import type { PropsWithChildren } from 'react'; @@ -17,7 +17,7 @@ import { createContextAndHook } from './hooks/createContextAndHook'; const [ClerkInstanceContext, useClerkInstanceContext] = createContextAndHook('ClerkInstanceContext'); const [UserContext, useUserContext] = createContextAndHook('UserContext'); const [ClientContext, useClientContext] = createContextAndHook('ClientContext'); -const [SessionContext, useSessionContext] = createContextAndHook( +const [SessionContext, useSessionContext] = createContextAndHook( 'SessionContext', ); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 6390a529f1b..e63b7fb1aa6 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -31,7 +31,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; -import type { AuthenticatedSessionResource } from './session'; +import type { SignedInSessionResource } from './session'; import type { SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; @@ -63,7 +63,7 @@ export type SDKMetadata = { export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; -export type BeforeEmitCallback = (session?: AuthenticatedSessionResource | null) => void | Promise; +export type BeforeEmitCallback = (session?: SignedInSessionResource | null) => void | Promise; export type SignOutCallback = () => void | Promise; @@ -128,17 +128,17 @@ export interface Clerk { isStandardBrowser: boolean | undefined; /** - * Indicates whether the current user has a valid, fully authenticated client session. + * Indicates whether the current user has a valid, fully signed-in client session. * A session is considered valid when the user has successfully authenticated, - * completed all required authentication factors, and resolved all pending authentication tasks. + * completed all required authentication factors, and resolved all pending tasks. */ - hasAuthenticatedClient: boolean; + isSignedIn: boolean; /** Client handling most Clerk operations. */ client: ClientResource | undefined; /** Current Session. */ - session: AuthenticatedSessionResource | null | undefined; + session: SignedInSessionResource | null | undefined; /** Active Organization */ organization: OrganizationResource | null | undefined; @@ -715,9 +715,9 @@ export type ClerkOptions = ClerkOptionsNavigation & localization?: LocalizationResource; polling?: boolean; /** - * By default, the last authenticated session is used during client initialization. This option allows you to override that behavior, e.g. by selecting a specific session. + * By default, the last signed-in session is used during client initialization. This option allows you to override that behavior, e.g. by selecting a specific session. */ - selectInitialSession?: (client: ClientResource) => AuthenticatedSessionResource | null; + selectInitialSession?: (client: ClientResource) => SignedInSessionResource | null; /** * By default, ClerkJS is loaded with the assumption that cookies can be set (browser setup). On native platforms this value must be set to `false`. */ @@ -803,7 +803,7 @@ export interface NavigateOptions { export interface Resources { client: ClientResource; - session?: AuthenticatedSessionResource | null; + session?: SignedInSessionResource | null; user?: UserResource | null; organization?: OrganizationResource | null; } @@ -879,7 +879,7 @@ export type SetActiveParams = { * The session resource or session id (string version) to be set on the client. * If `null`, the current session is deleted. */ - session?: AuthenticatedSessionResource | string | null; + session?: SignedInSessionResource | string | null; /** * The organization resource or organization ID/slug (string version) to be set as active in the current session. diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 3c395edef1d..3c8555f3723 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -6,8 +6,8 @@ import type { ClientJSONSnapshot } from './snapshots'; export interface ClientResource extends ClerkResource { sessions: SessionResource[]; - authenticatedSessions: (ActiveSessionResource | PendingSessionResource)[]; - hasAuthenticated: boolean; + signedInSessions: (ActiveSessionResource | PendingSessionResource)[]; + isSignedIn: boolean; signUp: SignUpResource; signIn: SignInResource; isNew: () => boolean; @@ -25,7 +25,7 @@ export interface ClientResource extends ClerkResource { updatedAt: Date | null; __internal_toSnapshot: () => ClientJSONSnapshot; /** - * @deprecated Use `authenticatedSessions` instead + * @deprecated Use `signedInSessions` instead */ activeSessions: ActiveSessionResource[]; } diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index a694e15f0d9..3d1a3d9f04f 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -4,10 +4,10 @@ import type { SignInResource } from 'signIn'; import type { SetActive, SignOut } from './clerk'; import type { ActJWTClaim } from './jwt'; import type { - AuthenticatedSessionResource, CheckAuthorizationWithCustomPermissions, GetToken, SessionResource, + SignedInSessionResource, } from './session'; import type { SignUpResource } from './signUp'; import type { UserResource } from './user'; @@ -180,7 +180,7 @@ export type UseSessionReturn = | { isLoaded: true; isSignedIn: true; - session: AuthenticatedSessionResource; + session: SignedInSessionResource; }; /** diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 99a2429bae7..8484dfb450b 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -150,9 +150,9 @@ export interface PendingSessionResource extends SessionResource { /** * Represents session resources for users who have completed - * the full authentication flow + * the full sign-in flow */ -export type AuthenticatedSessionResource = ActiveSessionResource | PendingSessionResource; +export type SignedInSessionResource = ActiveSessionResource | PendingSessionResource; export interface SessionWithActivitiesResource extends ClerkResource { id: string; diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index c48404d71b1..4e98e48ff6d 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -41,7 +41,7 @@ export const RedirectToSignIn = defineComponent((props: RedirectOptions) => { const { sessionCtx } = useClerkContext(); useClerkLoaded(clerk => { - if (sessionCtx.value === null && clerk.hasAuthenticatedClient) { + if (sessionCtx.value === null && clerk.isSignedIn) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index decfdef886b..032183d8e10 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -1,6 +1,5 @@ import type { ActJWTClaim, - AuthenticatedSessionResource, Clerk, ClerkOptions, ClientResource, @@ -9,6 +8,7 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, + SignedInSessionResource, UserResource, Without, } from '@clerk/types'; @@ -26,7 +26,7 @@ export interface VueClerkInjectionKeyType { orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; }>; clientCtx: ComputedRef; - sessionCtx: ComputedRef; + sessionCtx: ComputedRef; userCtx: ComputedRef; organizationCtx: ComputedRef; } From 760434f2cdb3dc9c93201349d8c78cc29949a8a5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Feb 2025 08:18:29 -0300 Subject: [PATCH 15/17] Update changeset --- .changeset/proud-cycles-roll.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md index fd3016b36e4..a4b0ffcf20b 100644 --- a/.changeset/proud-cycles-roll.md +++ b/.changeset/proud-cycles-roll.md @@ -3,20 +3,4 @@ --- - Initialize new `pending` session status as an signed-in state -- Add new `Clerk.isSignedIn` property to provide a more explicit way to check authentication status, replacing the previous `Clerk.user` check - -```diff -- if (Clerk.user) { -+ if (Clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` - - const userbuttonDiv = document.getElementById('user-button') - - clerk.mountUserButton(userbuttonDiv) -} else { - // ... -} -``` +- Add new `Clerk.isSignedIn` property to provide a more explicit way to check authentication status From 4ac18aac254490dac1e6020dc0425dea705d7e6b Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:55:17 -0300 Subject: [PATCH 16/17] Update clerk-js change to include deprecate property --- .changeset/proud-cycles-roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md index a4b0ffcf20b..34da5db470e 100644 --- a/.changeset/proud-cycles-roll.md +++ b/.changeset/proud-cycles-roll.md @@ -3,4 +3,4 @@ --- - Initialize new `pending` session status as an signed-in state -- Add new `Clerk.isSignedIn` property to provide a more explicit way to check authentication status +- Deprecate `Clerk.client.activeSessions` in favor of `Clerk.client.signedInSessions` From e69c175c20a7a857b979db9b976dc7f5490988fb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:54:29 -0300 Subject: [PATCH 17/17] Update `isSignedIn` to check against initialized session --- .changeset/nasty-mangos-live.md | 2 +- .changeset/proud-cycles-roll.md | 14 ++++++++++++++ .../clerk-js/src/core/auth/cookies/clientUat.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 2 +- packages/clerk-js/src/core/resources/Client.ts | 4 ---- .../clerk-js/src/ui/hooks/useMultipleSessions.ts | 5 +++-- .../sign-in/choose-session/choose-session.hooks.ts | 4 ++-- .../react/src/components/controlComponents.tsx | 5 +++-- packages/types/src/clerk.ts | 4 +--- packages/types/src/client.ts | 5 ++--- packages/types/src/session.ts | 2 +- packages/vue/src/components/controlComponents.ts | 6 ++++-- 12 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.changeset/nasty-mangos-live.md b/.changeset/nasty-mangos-live.md index 381c0a124b4..1206fda3bd8 100644 --- a/.changeset/nasty-mangos-live.md +++ b/.changeset/nasty-mangos-live.md @@ -8,4 +8,4 @@ '@clerk/vue': minor --- -Surface new `pending` session as an signed-in state, similarly as `active` +Surface new `pending` session as a signed-in state diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md index 34da5db470e..b15a69c5577 100644 --- a/.changeset/proud-cycles-roll.md +++ b/.changeset/proud-cycles-roll.md @@ -4,3 +4,17 @@ - Initialize new `pending` session status as an signed-in state - Deprecate `Clerk.client.activeSessions` in favor of `Clerk.client.signedInSessions` +- Introduce `Clerk.isSignedIn` property as an explicit signed-in state check, instead of `!!Clerk.session` or `!!Clerk.user`: + +```ts +- if (Clerk.user) { ++ if (Clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) +} diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index 85ecd676563..03144edabd1 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -38,7 +38,7 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand // '0' indicates the user is signed out let val = '0'; - if (client && client.updatedAt && client.isSignedIn) { + if (client && client.updatedAt && client.signedInSessions.length > 0) { // truncate timestamp to seconds, since this is a unix timestamp val = Math.floor(client.updatedAt.getTime() / 1000).toString(); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index efd89d7a426..8c205ab9909 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -289,7 +289,7 @@ export class Clerk implements ClerkInterface { } get isSignedIn(): boolean { - return !!this.client?.isSignedIn; + return !!this.session; } public constructor(key: string, options?: DomainOrProxyUrl) { diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index b3164fb300a..b46fc02a42f 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -118,10 +118,6 @@ export class Client extends BaseResource implements ClientResource { return this._basePostBypass({ body: params, path: this.path() + '/verify' }); } - get isSignedIn() { - return (this.signedInSessions ?? []).length > 0; - } - fromJSON(data: ClientJSON | ClientJSONSnapshot | null): this { if (data) { this.id = data.id; diff --git a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts index 747273411ce..b70217d8b88 100644 --- a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts +++ b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts @@ -7,10 +7,11 @@ type UseMultipleSessionsParam = { const useMultipleSessions = (params: UseMultipleSessionsParam) => { const clerk = useClerk(); + const signedInSessions = clerk.client.signedInSessions; return { - signedInSessions: clerk.client.signedInSessions, - otherSessions: clerk.client.signedInSessions.filter(s => s.user?.id !== params.user?.id), + signedInSessions, + otherSessions: signedInSessions.filter(s => s.user?.id !== params.user?.id), }; }; diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts index be17b76959f..ee7cfb697a4 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts @@ -23,11 +23,11 @@ export function useSignInChooseSessionIsActive() { return useActiveTags(routerRef, 'step:choose-session'); } -export type useSignInSessionListParams = { +export type UseSignInSessionListParams = { omitCurrent: boolean; }; -export function useSignInSessionList(params?: useSignInSessionListParams): SignInActiveSessionListItem[] { +export function useSignInSessionList(params?: UseSignInSessionListParams): SignInActiveSessionListItem[] { const { omitCurrent = true } = params || {}; return SignInRouterCtx.useSelector(state => { diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 81d336ed614..91c9d7ee00e 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -142,10 +142,11 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect }; export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { - const { session, isSignedIn } = clerk; + const { client, session } = clerk; + const hasSignedInSessions = client.signedInSessions && client.signedInSessions.length > 0; React.useEffect(() => { - if (session === null && isSignedIn) { + if (session === null && hasSignedInSessions) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index e63b7fb1aa6..1296685e084 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -128,9 +128,7 @@ export interface Clerk { isStandardBrowser: boolean | undefined; /** - * Indicates whether the current user has a valid, fully signed-in client session. - * A session is considered valid when the user has successfully authenticated, - * completed all required authentication factors, and resolved all pending tasks. + * Indicates whether the current user has a valid signed-in client session */ isSignedIn: boolean; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 3c8555f3723..f5374e8af54 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,13 +1,12 @@ import type { ClerkResource } from './resource'; -import type { ActiveSessionResource, PendingSessionResource, SessionResource } from './session'; +import type { ActiveSessionResource, SessionResource, SignedInSessionResource } from './session'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; import type { ClientJSONSnapshot } from './snapshots'; export interface ClientResource extends ClerkResource { sessions: SessionResource[]; - signedInSessions: (ActiveSessionResource | PendingSessionResource)[]; - isSignedIn: boolean; + signedInSessions: SignedInSessionResource[]; signUp: SignUpResource; signIn: SignInResource; isNew: () => boolean; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 8484dfb450b..db4772c3813 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -141,7 +141,7 @@ export interface ActiveSessionResource extends SessionResource { } /** - * Represents a session resource that is authenticated but has pending tasks + * Represents a session resource that has completed sign-in but has pending tasks */ export interface PendingSessionResource extends SessionResource { status: 'pending'; diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index 4e98e48ff6d..c48925befae 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -38,10 +38,12 @@ export const ClerkLoading = defineComponent((_, { slots }) => { }); export const RedirectToSignIn = defineComponent((props: RedirectOptions) => { - const { sessionCtx } = useClerkContext(); + const { sessionCtx, clientCtx } = useClerkContext(); useClerkLoaded(clerk => { - if (sessionCtx.value === null && clerk.isSignedIn) { + const hasSignedInSessions = clientCtx.value?.signedInSessions && clientCtx.value.signedInSessions.length > 0; + + if (sessionCtx.value === null && hasSignedInSessions) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props);