Skip to content

Commit 3efdd2c

Browse files
authored
fix(backend,clerk-js): treat undefined satelliteAutoSync as false (#8001)
1 parent 35d45be commit 3efdd2c

File tree

7 files changed

+152
-20
lines changed

7 files changed

+152
-20
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clerk/backend": patch
3+
"@clerk/clerk-js": patch
4+
---
5+
6+
Fix `satelliteAutoSync` to default to `false` as documented. Previously, not passing the prop resulted in `undefined`, which was treated as `true` due to a strict equality check (`=== false`). This preserved Core 2 auto-sync behavior instead of the intended Core 3 default. The check is now `!== true`, so both `undefined` and `false` skip automatic satellite sync.

integration/tests/handshake.test.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ test.describe('Client handshake @generic', () => {
532532
expect(res.status).toBe(200);
533533
});
534534

535-
test('signed out satellite with sec-fetch-dest=document - prod', async () => {
535+
test('signed out satellite with sec-fetch-dest=document skips handshake by default (satelliteAutoSync unset) - prod', async () => {
536536
const config = generateConfig({
537537
mode: 'live',
538538
});
@@ -546,13 +546,8 @@ test.describe('Client handshake @generic', () => {
546546
}),
547547
redirect: 'manual',
548548
});
549-
expect(res.status).toBe(307);
550-
const locationUrl = new URL(res.headers.get('location'));
551-
expect(locationUrl.origin + locationUrl.pathname).toBe('https://clerk.example.com/v1/client/handshake');
552-
expect(locationUrl.searchParams.get('redirect_url')).toBe(`${app.serverUrl}/`);
553-
expect(locationUrl.searchParams.get('__clerk_hs_reason')).toBe('satellite-needs-syncing');
554-
expect(locationUrl.searchParams.has('__clerk_api_version')).toBe(true);
555-
expect(locationUrl.searchParams.get('suffixed_cookies')).toBe('false');
549+
// In Core 3, satelliteAutoSync defaults to false, so no handshake redirect
550+
expect(res.status).toBe(200);
556551
});
557552

558553
test('signed out satellite - dev', async () => {
@@ -631,7 +626,28 @@ test.describe('Client handshake @generic', () => {
631626
expect(res.status).toBe(200);
632627
});
633628

634-
test('signed out satellite with satelliteAutoSync=true (default) triggers handshake - prod', async () => {
629+
test('signed out satellite with satelliteAutoSync unset triggers handshake when __clerk_synced=false - prod', async () => {
630+
const config = generateConfig({
631+
mode: 'live',
632+
});
633+
const res = await fetch(app.serverUrl + '/?__clerk_synced=false', {
634+
headers: new Headers({
635+
'X-Publishable-Key': config.pk,
636+
'X-Secret-Key': config.sk,
637+
'X-Satellite': 'true',
638+
'X-Domain': 'example.com',
639+
'Sec-Fetch-Dest': 'document',
640+
}),
641+
redirect: 'manual',
642+
});
643+
// Even without satelliteAutoSync, __clerk_synced=false (post sign-in) should trigger handshake
644+
expect(res.status).toBe(307);
645+
const locationUrl = new URL(res.headers.get('location'));
646+
expect(locationUrl.origin + locationUrl.pathname).toBe('https://clerk.example.com/v1/client/handshake');
647+
expect(locationUrl.searchParams.get('__clerk_hs_reason')).toBe('satellite-needs-syncing');
648+
});
649+
650+
test('signed out satellite with satelliteAutoSync=true (explicit opt-in) triggers handshake - prod', async () => {
635651
const config = generateConfig({
636652
mode: 'live',
637653
});
@@ -646,7 +662,7 @@ test.describe('Client handshake @generic', () => {
646662
}),
647663
redirect: 'manual',
648664
});
649-
// Should redirect to handshake with default/true satelliteAutoSync
665+
// Should redirect to handshake when satelliteAutoSync is explicitly true
650666
expect(res.status).toBe(307);
651667
const locationUrl = new URL(res.headers.get('location'));
652668
expect(locationUrl.origin + locationUrl.pathname).toBe('https://clerk.example.com/v1/client/handshake');

packages/backend/src/tokens/__tests__/request.test.ts

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ describe('tokens.authenticateRequest(options)', () => {
651651
expect(requestState).toBeSignedOutToAuth();
652652
});
653653

654-
test('cookieToken: returns handshake when clientUat is missing or equals to 0 and is satellite and not is synced [11y]', async () => {
654+
test('cookieToken: returns handshake when clientUat is missing or equals to 0 and is satellite with satelliteAutoSync=true and not is synced [11y]', async () => {
655655
server.use(
656656
http.get('https://api.clerk.test/v1/jwks', () => {
657657
return HttpResponse.json(mockJwks);
@@ -671,6 +671,7 @@ describe('tokens.authenticateRequest(options)', () => {
671671
isSatellite: true,
672672
signInUrl: 'https://primary.dev/sign-in',
673673
domain: 'satellite.dev',
674+
satelliteAutoSync: true,
674675
}),
675676
);
676677

@@ -684,7 +685,7 @@ describe('tokens.authenticateRequest(options)', () => {
684685
expect(requestState.toAuth()).toBeNull();
685686
});
686687

687-
test('cookieToken: redirects to signInUrl when is satellite dev and not synced', async () => {
688+
test('cookieToken: redirects to signInUrl when is satellite dev with satelliteAutoSync=true and not synced', async () => {
688689
server.use(
689690
http.get('https://api.clerk.test/v1/jwks', () => {
690691
return HttpResponse.json(mockJwks);
@@ -705,6 +706,7 @@ describe('tokens.authenticateRequest(options)', () => {
705706
isSatellite: true,
706707
signInUrl: 'https://primary.dev/sign-in',
707708
domain: 'satellite.dev',
709+
satelliteAutoSync: true,
708710
}),
709711
);
710712

@@ -873,6 +875,114 @@ describe('tokens.authenticateRequest(options)', () => {
873875
expect(requestState.toAuth()).toBeSignedOutToAuth();
874876
});
875877

878+
test('cookieToken: returns signed out without handshake when satelliteAutoSync is not set (defaults to false) and no cookies - prod', async () => {
879+
const requestState = await authenticateRequest(
880+
mockRequestWithCookies(
881+
{ ...defaultHeaders, 'sec-fetch-dest': 'document' },
882+
{ __client_uat: '0' },
883+
`http://satellite.example/path`,
884+
),
885+
mockOptions({
886+
secretKey: 'deadbeef',
887+
publishableKey: PK_LIVE,
888+
signInUrl: 'https://primary.example/sign-in',
889+
isSatellite: true,
890+
domain: 'satellite.example',
891+
}),
892+
);
893+
894+
expect(requestState).toBeSignedOut({
895+
reason: AuthErrorReason.SessionTokenAndUATMissing,
896+
isSatellite: true,
897+
domain: 'satellite.example',
898+
signInUrl: 'https://primary.example/sign-in',
899+
});
900+
expect(requestState.toAuth()).toBeSignedOutToAuth();
901+
expect(requestState.headers.get('location')).toBeNull();
902+
});
903+
904+
test('cookieToken: returns signed out without handshake when satelliteAutoSync is not set (defaults to false) and no cookies - dev', async () => {
905+
const requestState = await authenticateRequest(
906+
mockRequestWithCookies(
907+
{ ...defaultHeaders, 'sec-fetch-dest': 'document' },
908+
{
909+
__client_uat: '0',
910+
__clerk_db_jwt: mockJwt,
911+
},
912+
),
913+
mockOptions({
914+
secretKey: 'sk_test_deadbeef',
915+
publishableKey: PK_TEST,
916+
isSatellite: true,
917+
signInUrl: 'https://primary.dev/sign-in',
918+
domain: 'satellite.dev',
919+
}),
920+
);
921+
922+
expect(requestState).toBeSignedOut({
923+
reason: AuthErrorReason.SessionTokenAndUATMissing,
924+
isSatellite: true,
925+
domain: 'satellite.dev',
926+
signInUrl: 'https://primary.dev/sign-in',
927+
});
928+
expect(requestState.toAuth()).toBeSignedOutToAuth();
929+
expect(requestState.headers.get('location')).toBeNull();
930+
});
931+
932+
test('cookieToken: triggers handshake when satelliteAutoSync is not set but __clerk_synced=false is present - prod', async () => {
933+
const requestState = await authenticateRequest(
934+
mockRequestWithCookies(
935+
{ ...defaultHeaders, 'sec-fetch-dest': 'document' },
936+
{ __client_uat: '0' },
937+
`http://satellite.example/path?__clerk_synced=false`,
938+
),
939+
mockOptions({
940+
secretKey: 'deadbeef',
941+
publishableKey: PK_LIVE,
942+
signInUrl: 'https://primary.example/sign-in',
943+
isSatellite: true,
944+
domain: 'satellite.example',
945+
}),
946+
);
947+
948+
expect(requestState).toMatchHandshake({
949+
reason: AuthErrorReason.SatelliteCookieNeedsSyncing,
950+
isSatellite: true,
951+
domain: 'satellite.example',
952+
signInUrl: 'https://primary.example/sign-in',
953+
});
954+
});
955+
956+
test('cookieToken: triggers handshake when satelliteAutoSync is not set but __clerk_synced=false is present - dev', async () => {
957+
const requestState = await authenticateRequest(
958+
mockRequestWithCookies(
959+
{ ...defaultHeaders, 'sec-fetch-dest': 'document' },
960+
{
961+
__client_uat: '0',
962+
__clerk_db_jwt: mockJwt,
963+
},
964+
`http://satellite.dev/path?__clerk_synced=false`,
965+
),
966+
mockOptions({
967+
secretKey: 'sk_test_deadbeef',
968+
publishableKey: PK_TEST,
969+
signInUrl: 'https://primary.dev/sign-in',
970+
isSatellite: true,
971+
domain: 'satellite.dev',
972+
}),
973+
);
974+
975+
expect(requestState).toMatchHandshake({
976+
reason: AuthErrorReason.SatelliteCookieNeedsSyncing,
977+
isSatellite: true,
978+
domain: 'satellite.dev',
979+
signInUrl: 'https://primary.dev/sign-in',
980+
});
981+
expect(requestState.headers.get('location')).toEqual(
982+
`https://primary.dev/sign-in?__clerk_redirect_url=http%3A%2F%2Fexample.com%2Fpath%3F__clerk_synced%3Dfalse`,
983+
);
984+
});
985+
876986
test('cookieToken: returns handshake when app is not satellite and responds to syncing on dev instances[12y]', async () => {
877987
const sp = new URLSearchParams();
878988
sp.set('__clerk_redirect_url', 'http://localhost:3000');

packages/backend/src/tokens/request.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
484484
* - 'false' (NeedsSync): Trigger sync - satellite returning from primary sign-in
485485
* - 'true' (Completed): Sync done - prevents re-sync loop
486486
*
487-
* With satelliteAutoSync=false:
487+
* With satelliteAutoSync=false or unset (Core 3 default):
488488
* - Skip handshake on first visit if no cookies exist (return signedOut immediately)
489489
* - Trigger handshake when __clerk_synced=false is present (post sign-in redirect)
490490
* - Allow normal token verification flow when cookies exist (enables refresh)
@@ -499,8 +499,8 @@ export const authenticateRequest: AuthenticateRequest = (async (
499499
const hasCookies = hasSessionToken || hasActiveClient;
500500

501501
// Determine if we should skip handshake for satellites with no cookies
502-
// satelliteAutoSync defaults to true, so we only skip when explicitly set to false
503-
const shouldSkipSatelliteHandshake = authenticateContext.satelliteAutoSync === false && !hasCookies && !needsSync;
502+
// satelliteAutoSync defaults to false (Core 3), so we skip unless explicitly set to true
503+
const shouldSkipSatelliteHandshake = authenticateContext.satelliteAutoSync !== true && !hasCookies && !needsSync;
504504

505505
if (authenticateContext.instanceType === 'production' && isRequestEligibleForMultiDomainSync && !syncCompleted) {
506506
// With satelliteAutoSync=false: skip handshake if no cookies and no sync trigger

packages/clerk-js/src/core/__tests__/clerk.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,6 +2386,7 @@ describe('Clerk singleton', () => {
23862386
describe('Clerk().isSatellite and Clerk().domain getters', () => {
23872387
beforeEach(() => {
23882388
mockClientFetch.mockReset();
2389+
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [] }));
23892390
mockEnvironmentFetch.mockReturnValue(
23902391
Promise.resolve({
23912392
authConfig: {},

packages/clerk-js/src/core/auth/AuthCookieService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class AuthCookieService {
116116
if (!this.clerk.loaded) {
117117
return this.clientUat.get() <= 0;
118118
}
119-
return !!this.clerk.user;
119+
return !this.clerk.user;
120120
}
121121

122122
public async handleUnauthenticatedDevBrowser() {

packages/clerk-js/src/core/clerk.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,10 +2860,9 @@ export class Clerk implements ClerkInterface {
28602860
return true;
28612861
}
28622862

2863-
// Check if satelliteAutoSync is disabled - if so, skip automatic sync
2864-
// unless explicitly triggered via __clerk_synced=false
2865-
if (this.#options.satelliteAutoSync === false) {
2866-
// Skip automatic sync when satelliteAutoSync is false
2863+
// Check if satelliteAutoSync is enabled - only auto-sync when explicitly opted in
2864+
// In Core 3, satelliteAutoSync defaults to false (undefined is treated as false)
2865+
if (this.#options.satelliteAutoSync !== true) {
28672866
return false;
28682867
}
28692868

0 commit comments

Comments
 (0)