Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/warm-touch-intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
---

Add optional `intent` parameter to `session.touch()` to indicate why the touch was triggered (focus, session switch, or org switch). This enables the backend to skip expensive client piggybacking for focus-only touches.
10 changes: 5 additions & 5 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
const sut = new Clerk(productionPublishableKey);
await sut.load();
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
expect(mockSession.touch).toHaveBeenCalled();
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
});

describe('with `touchSession` set to false', () => {
Expand All @@ -218,7 +218,7 @@
const sut = new Clerk(productionPublishableKey);
await sut.load({ touchSession: false });
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
expect(mockSession.touch).toHaveBeenCalled();
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
});
});

Expand All @@ -233,7 +233,7 @@
const sut = new Clerk(productionPublishableKey);
await sut.load();
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
expect(mockSession.touch).toHaveBeenCalled();
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
});

it('sets __session and __client_uat cookie before calling __internal_onBeforeSetActive', async () => {
Expand Down Expand Up @@ -280,7 +280,7 @@
await sut.setActive({ organization: 'some-org-slug' });

await waitFor(() => {
expect(mockSession2.touch).toHaveBeenCalled();
expect(mockSession2.touch).toHaveBeenCalledWith({ intent: 'select_org' });
expect(mockSession2.getToken).toHaveBeenCalled();
expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
expect(sut.session).toMatchObject(mockSession2);
Expand Down Expand Up @@ -363,7 +363,7 @@
const sut = new Clerk(productionPublishableKey);
await sut.load();
await sut.setActive({ session: mockSession as any as PendingSessionResource, navigate });
expect(mockSession.__internal_touch).toHaveBeenCalled();
expect(mockSession.__internal_touch).toHaveBeenCalledWith({ intent: 'select_session' });
expect(navigate).toHaveBeenCalled();
});

Expand Down Expand Up @@ -2192,7 +2192,7 @@
sut.setActive = mockSetActive;

const redirectUrl = '/2fa';
sut.handleEmailLinkVerification({ redirectUrl });

Check warning on line 2195 in packages/clerk-js/src/core/__tests__/clerk.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

await waitFor(() => {
expect(mockSetActive).not.toHaveBeenCalled();
Expand Down Expand Up @@ -2252,7 +2252,7 @@
sut.setActive = mockSetActive;

const redirectUrl = '/next-up';
sut.handleEmailLinkVerification({ redirectUrl });

Check warning on line 2255 in packages/clerk-js/src/core/__tests__/clerk.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

await waitFor(() => {
expect(mockSetActive).not.toHaveBeenCalled();
Expand Down
15 changes: 10 additions & 5 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
Resources,
SDKMetadata,
SessionResource,
SessionTouchParams,
SetActiveParams,
SignedInSessionResource,
SignInProps,
Expand Down Expand Up @@ -1579,6 +1580,7 @@
newSession?.currentTask &&
this.#options.taskUrls?.[newSession?.currentTask.key];
const shouldNavigate = !!(redirectUrl || taskUrl || setActiveNavigate);
const touchIntent: SessionTouchParams['intent'] = shouldSwitchOrganization ? 'select_org' : 'select_session';

//1. setLastActiveSession to passed user session (add a param).
// Note that this will also update the session's active organization
Expand All @@ -1599,7 +1601,7 @@
if (shouldNavigate && newSession) {
try {
// __internal_touch does not call updateClient automatically
updatedClient = await newSession.__internal_touch();
updatedClient = await newSession.__internal_touch({ intent: touchIntent });
if (updatedClient) {
// We call updateClient manually, but without letting it emit
// It's important that the setTransitiveState call happens somewhat
Expand All @@ -1615,7 +1617,7 @@
}
}
} else {
await this.#touchCurrentSession(newSession);
await this.#touchCurrentSession(newSession, touchIntent);
}
// If we do have the updatedClient, read from that, otherwise getSessionFromClient
// will fallback to this.client. This makes no difference now, but will if we
Expand Down Expand Up @@ -2081,14 +2083,14 @@
return;
};

public redirectToAfterSignIn = async (): Promise<unknown> => {

Check warning on line 2086 in packages/clerk-js/src/core/clerk.ts

View workflow job for this annotation

GitHub Actions / Static analysis

Promise-returning method provided where a void return was expected by extended/implemented type 'Clerk'
if (inBrowser()) {
return this.navigate(this.buildAfterSignInUrl());
}
return;
};

public redirectToAfterSignUp = async (): Promise<unknown> => {

Check warning on line 2093 in packages/clerk-js/src/core/clerk.ts

View workflow job for this annotation

GitHub Actions / Static analysis

Promise-returning method provided where a void return was expected by extended/implemented type 'Clerk'
if (inBrowser()) {
return this.navigate(this.buildAfterSignUpUrl());
}
Expand Down Expand Up @@ -3150,7 +3152,7 @@
this.#touchThrottledUntil = Date.now() + 5_000;

if (this.#options.touchSession) {
void this.#touchCurrentSession(this.session);
void this.#touchCurrentSession(this.session, 'focus');
}
});

Expand Down Expand Up @@ -3181,12 +3183,15 @@
};

// TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc
#touchCurrentSession = async (session?: SignedInSessionResource | null): Promise<void> => {
#touchCurrentSession = async (
session?: SignedInSessionResource | null,
intent: SessionTouchParams['intent'] = 'focus',
): Promise<void> => {
if (!session) {
return Promise.resolve();
}

await session.touch().catch(e => {
await session.touch({ intent }).catch(e => {
if (isUnauthenticatedError(e)) {
void this.handleUnauthenticated();
} else {
Expand Down
15 changes: 9 additions & 6 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
SessionResource,
SessionStatus,
SessionTask,
SessionTouchParams,
SessionVerificationJSON,
SessionVerificationResource,
SessionVerifyAttemptFirstFactorParams,
Expand Down Expand Up @@ -103,14 +104,16 @@ export class Session extends BaseResource implements SessionResource {
};

private _touchPost = async (
{ skipUpdateClient }: { skipUpdateClient: boolean } = { skipUpdateClient: false },
{ intent, skipUpdateClient }: { intent?: SessionTouchParams['intent']; skipUpdateClient: boolean } = {
skipUpdateClient: false,
},
): Promise<FapiResponseJSON<SessionJSON> | null> => {
const json = await BaseResource._fetch<SessionJSON>(
{
method: 'POST',
path: this.path('touch'),
// any is how we type the body in the BaseMutateParams as well
body: { active_organization_id: this.lastActiveOrganizationId } as any,
body: { active_organization_id: this.lastActiveOrganizationId, intent } as any,
},
{ skipUpdateClient },
);
Expand All @@ -121,8 +124,8 @@ export class Session extends BaseResource implements SessionResource {
return json;
};

touch = async (): Promise<SessionResource> => {
await this._touchPost();
touch = async ({ intent }: SessionTouchParams = {}): Promise<SessionResource> => {
await this._touchPost({ intent, skipUpdateClient: false });

// _touchPost() will have updated `this` in-place
// The post has potentially changed the session state, and so we need to ensure we emit the updated token that comes back in the response. This avoids potential issues where the session cookie is out of sync with the current session state.
Expand All @@ -143,8 +146,8 @@ export class Session extends BaseResource implements SessionResource {
*
* @internal
*/
__internal_touch = async (): Promise<ClientResource | undefined> => {
const json = await this._touchPost({ skipUpdateClient: true });
__internal_touch = async ({ intent }: SessionTouchParams = {}): Promise<ClientResource | undefined> => {
const json = await this._touchPost({ intent, skipUpdateClient: true });
return getClientResourceFromPayload(json);
};

Expand Down
52 changes: 52 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,37 @@ describe('Session', () => {
token: session.lastActiveToken,
});
});

it('passes touch intent in the request body', async () => {
const sessionData = {
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: 'org_123',
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON;
const session = new Session(sessionData);

const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
requestSpy.mockResolvedValue({
payload: { response: sessionData },
status: 200,
});

await session.touch({ intent: 'focus' });

expect(requestSpy).toHaveBeenCalledWith(
expect.objectContaining({
body: { active_organization_id: 'org_123', intent: 'focus' },
method: 'POST',
path: '/client/sessions/session_1/touch',
}),
expect.anything(),
);
});
});

describe('__internal_touch()', () => {
Expand Down Expand Up @@ -902,6 +933,27 @@ describe('Session', () => {

expect(session.lastActiveOrganizationId).toBe('org_456');
});

it('passes touch intent in the request body', async () => {
const session = new Session(mockSessionData);
const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;

requestSpy.mockResolvedValue({
payload: { response: mockSessionData },
status: 200,
});

await session.__internal_touch({ intent: 'select_session' });

expect(requestSpy).toHaveBeenCalledWith(
expect.objectContaining({
body: { active_organization_id: 'org_123', intent: 'select_session' },
method: 'POST',
path: '/client/sessions/session_1/touch',
}),
expect.anything(),
);
});
});

describe('isAuthorized()', () => {
Expand Down
10 changes: 8 additions & 2 deletions packages/shared/src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export interface SessionResource extends ClerkResource {
*/
end: () => Promise<SessionResource>;
remove: () => Promise<SessionResource>;
touch: () => Promise<SessionResource>;
touch: (params?: SessionTouchParams) => Promise<SessionResource>;
getToken: GetToken;
checkAuthorization: CheckAuthorization;
clearCache: () => void;
Expand All @@ -262,7 +262,7 @@ export interface SessionResource extends ClerkResource {
) => Promise<SessionVerificationResource>;
verifyWithPasskey: () => Promise<SessionVerificationResource>;
__internal_toSnapshot: () => SessionJSONSnapshot;
__internal_touch: () => Promise<ClientResource | undefined>;
__internal_touch: (params?: SessionTouchParams) => Promise<ClientResource | undefined>;
}

/**
Expand Down Expand Up @@ -322,6 +322,12 @@ export type SessionStatus =
| 'revoked'
| 'pending';

export type SessionTouchIntent = 'focus' | 'select_session' | 'select_org';

export type SessionTouchParams = {
intent?: SessionTouchIntent;
};

export interface PublicUserData {
firstName: string | null;
lastName: string | null;
Expand Down
Loading