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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
5 changes: 5 additions & 0 deletions .changeset/duplicate-session-cookies.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Recover from partitioned-cookie startup races by removing stale non-partitioned cookies when partitioned cookies become available.
9 changes: 6 additions & 3 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,11 +83,11 @@ export class AuthCookieService {

eventBus.on(events.UserSignOut, () => this.handleSignOut());

// After Environment resolves, re-write dev browser cookies with correct
// partitioned attributes. Dev browser cookies are initially written before
// Environment is fetched, so they may have stale attributes.
// Environment can resolve after auth cookies are first written.
eventBus.on(events.EnvironmentUpdate, () => {
this.devBrowser.refreshCookies();
void this.refreshSessionToken({ updateCookieImmediately: true });
this.setClientUatCookieForDevelopmentInstances();
});

this.refreshTokenOnFocus();
Expand DownExpand Up@@ -266,6 +266,9 @@ export class AuthCookieService {
}

public setClientUatCookieForDevelopmentInstances() {
if (!this.clerk.client) {
return;
}
if (this.instanceType !== 'production' && this.inCustomDevelopmentDomain()) {
this.clientUat.set(this.clerk.client);
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus, events } from '../../events';
import { Environment } from '../../resources/Environment';

const mocks = vi.hoisted(() => ({
sessionCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn() },
clientUatCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn(() => 0) },
activeContextCookie: { set: vi.fn(), remove: vi.fn(), get: vi.fn<() => string | undefined>(() => undefined) },
devBrowser: {
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
},
inCrossOriginIframe: vi.fn(() => false),
}));

vi.mock('../cookies/session', () => ({ createSessionCookie: () => mocks.sessionCookie }));
vi.mock('../cookies/clientUat', () => ({ createClientUatCookie: () => mocks.clientUatCookie }));
vi.mock('../cookies/activeContext', () => ({ createActiveContextCookie: () => mocks.activeContextCookie }));
vi.mock('../cookieSuffix', () => ({ getCookieSuffix: vi.fn(() => Promise.resolve('suffix')) }));
vi.mock('../devBrowser', () => ({
createDevBrowser: () => ({
clear: vi.fn(),
setup: vi.fn(() => Promise.resolve()),
getDevBrowser: vi.fn(() => 'deadbeef'),
refreshCookies: vi.fn(),
}),
}));
vi.mock('../devBrowser', () => ({ createDevBrowser: () => mocks.devBrowser }));
vi.mock('@clerk/shared/internal/clerk-js/runtime', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, inCrossOriginIframe: () => mocks.inCrossOriginIframe() };
Expand DownExpand Up@@ -58,6 +58,7 @@ describe('AuthCookieService session cookie refresh', () => {
mocks.inCrossOriginIframe.mockReturnValue(false);
mocks.activeContextCookie.get.mockReturnValue(undefined);
getToken.mockResolvedValue('fresh-jwt');
Environment.getInstance().partitionedCookies = false;
setFocus(true);
setVisibility('visible');
});
Expand DownExpand Up@@ -136,4 +137,16 @@ describe('AuthCookieService session cookie refresh', () => {

expect(getToken).toHaveBeenCalled();
});

it('rewrites the session cookie after partitioned cookies resolve', async () => {
service = await createService();
getToken.mockResolvedValue('jwt-after-environment');
Environment.getInstance().partitionedCookies = true;
mocks.sessionCookie.set.mockClear();

eventBus.emit(events.EnvironmentUpdate, null);

await vi.waitFor(() => expect(mocks.sessionCookie.set).toHaveBeenCalledWith('jwt-after-environment'));
expect(mocks.devBrowser.refreshCookies).toHaveBeenCalled();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,8 @@ describe('createClientUatCookie', () => {
const mockExpires = new Date('2024-12-31');
const mockDomain = 'test.domain';
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -32,9 +32,13 @@ describe('createClientUatCookie', () => {
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -55,13 +59,14 @@ describe('createClientUatCookie', () => {
});

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
secure: true,
partitioned: false,
});
expect(mockSet).toHaveBeenCalledWith('__client_uat', '1704067200', expect.any(Object));
});

it('should set cookies with None sameSite in cross-origin context', () => {
Expand All@@ -73,7 +78,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -86,7 +91,7 @@ describe('createClientUatCookie', () => {
const cookieHandler = createClientUatCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(undefined);

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand All@@ -103,7 +108,7 @@ describe('createClientUatCookie', () => {
signedInSessions: [],
});

expect(mockSet).toHaveBeenCalledWith('0', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '0', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'Strict',
Expand DownExpand Up@@ -139,7 +144,7 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
Expand All@@ -156,12 +161,73 @@ describe('createClientUatCookie', () => {
signedInSessions: ['session1'],
});

expect(mockSet).toHaveBeenCalledWith('1704067200', {
expect(mockSet).toHaveBeenCalledWith('__client_uat_test-suffix', '1704067200', {
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned domain variants before writing partitioned cookies', () => {
let usePartitionedCookies = false;
const cookieHandler = createClientUatCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});
const client = {
id: 'test-client',
updatedAt: new Date('2024-01-01'),
signedInSessions: ['session1'],
};

cookieHandler.set(client);
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set(client);
Comment on lines +173 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'clientUat.test.ts' -t f --exec rg -nP -C2 'cookieHandler\.set\(|as any|as unknown as'

Repository: clerk/javascript

Length of output: 2065


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## tsconfig files\n'
fd -a 'tsconfig*.json' packages/clerk-js .| sed 's#^\./##'| sort
printf'\n## package.json test/typecheck scripts\n'
rg -n '"(test|typecheck|check|lint|tsc)"\s*:\s*' packages/clerk-js/package.json package.json -n
printf'\n## ClientResource and createClientUatCookie definitions\n'
rg -n 'type ClientResource|interface ClientResource|createClientUatCookie|set\(client' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'printf'\n## test file excerpt\n'
sed -n '1,260p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 14517


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n## relevant tsconfig contents\n'forfin packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json;do
[ -f"$f" ] && { echo"--- $f"; cat "$f";echo; }
doneprintf'\n## source types around ClientResource\n'
sed -n '1,220p'$(rg -l 'ClientResource' packages/clerk-js/src | head -n 5)printf'\n## test file lines 160-200\n'
sed -n '160,200p' packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts

Repository: clerk/javascript

Length of output: 11138


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echoecho'## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'echoecho'## createClientUatCookie signature'
rg -n 'function createClientUatCookie|const createClientUatCookie|createClientUatCookie\s*=' packages/clerk-js/src -g '!**/dist/**' -g '!**/build/**'

Repository: clerk/javascript

Length of output: 922


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## ClientResource definition candidates\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js packages -g '!**/*.map'| head -n 50
printf'\n## ClientResource related re-exports\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80

Repository: clerk/javascript

Length of output: 4730


🏁 Script executed:

#!/bin/bashset -euo pipefail
# locate the shared type definition and the exact shape used by ClientUatCookieHandler
files=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 20)printf'%s\n'"$files"printf'\n## definition excerpts\n'forfin$files;doecho"--- $f"
sed -n '1,240p'"$f"| rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 ||truedone

Repository: clerk/javascript

Length of output: 10076


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'## files named around shared types\n'
fd -a 'types.ts' packages/shared packages/clerk-js/src | head -n 50
printf'\n## files with ClientResource in shared package\n'
rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 1448


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**''ClientResource' packages/shared packages/clerk-js/src | head -n 80
echoecho'## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50

Repository: clerk/javascript

Length of output: 5444


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Search for the shared type declaration and inspect the minimal surrounding context.
type_file=$(rg -l --hidden --glob '!**/dist/**' --glob '!**/build/**''export (type|interface) ClientResource|type ClientResource =|interface ClientResource' packages/shared packages/clerk-js/src | head -n 1)echo"## type file: $type_file"
[ -n"$type_file" ] && sed -n '1,260p'"$type_file"echoecho"## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.ts

Repository: clerk/javascript

Length of output: 7485


This fixture isn't a ClientResource.client only has id, updatedAt, and signedInSessions, so this test file will fail tsc unless you use a typed helper or an explicit cast for the minimal test shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts` around
lines 173 - 188, Update the client fixture in the “clears non-partitioned domain
variants before writing partitioned cookies” test to satisfy the ClientResource
type, using an existing typed helper or an explicit cast for this minimal shape.
Keep the fixture’s runtime fields and test behavior unchanged.


expect(mockRemove.mock.calls).toEqual([
['__client_uat_test-suffix', undefined],
['__client_uat', undefined],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'Strict', secure: true, partitioned: false }],
['__client_uat_test-suffix', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
['__client_uat', { domain: mockDomain, sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__client_uat_test-suffix',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__client_uat',
'1704067200',
{
domain: mockDomain,
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 4,
firstInvocationOrder + 5,
firstInvocationOrder + 6,
firstInvocationOrder + 7,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 8, firstInvocationOrder + 9]);
});
});
82 changes: 68 additions & 14 deletions packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ describe('createSessionCookie', () => {
const mockToken = 'test-token';
const mockExpires = new Date('2024-12-31');
const defaultOptions = { usePartitionedCookies: () => false };
const mockSet = vi.fn();
const mockRemove = vi.fn();
const mockSet = vi.fn<(name: string, value: string, attributes?: object) => void>();
const mockRemove = vi.fn<(name: string, attributes?: object) => void>();
const mockGet = vi.fn();

beforeEach(() => {
Expand All@@ -29,9 +29,13 @@ describe('createSessionCookie', () => {
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
set: mockSet,
remove: mockRemove,
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation((name: string) => ({
set: (value: string, attributes?: object) => {
mockSet(name, value, attributes);
},
remove: (attributes?: object) => {
mockRemove(name, attributes);
},
get: mockGet,
}));
});
Expand All@@ -48,7 +52,7 @@ describe('createSessionCookie', () => {
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
Expand All@@ -61,7 +65,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -87,17 +91,17 @@ describe('createSessionCookie', () => {
partitioned: false,
};

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'Lax',
secure: true,
partitioned: false,
});

expect(mockRemove).toHaveBeenCalledWith(expectedAttributes);
expect(mockRemove).toHaveBeenCalledWith('__session', expectedAttributes);
expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockRemove).toHaveBeenNthCalledWith(1, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(1, '__session', expectedAttributes);
expect(mockRemove).toHaveBeenNthCalledWith(2, '__session_test-suffix', expectedAttributes);
});

it('should get cookie value from suffixed cookie first, then fallback to non-suffixed', () => {
Expand All@@ -123,7 +127,7 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, defaultOptions);
cookieHandler.set(mockToken);

expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
Expand All@@ -135,12 +139,62 @@ describe('createSessionCookie', () => {
const cookieHandler = createSessionCookie(mockCookieSuffix, { usePartitionedCookies: () => true });
cookieHandler.set(mockToken);

expect(mockRemove).toHaveBeenCalledTimes(2);
expect(mockSet).toHaveBeenCalledWith(mockToken, {
expect(mockRemove).toHaveBeenCalledTimes(4);
expect(mockSet).toHaveBeenCalledWith('__session', mockToken, {
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
});
});

it('clears non-partitioned variants before writing partitioned cookies after the environment changes', () => {
let usePartitionedCookies = false;
const cookieHandler = createSessionCookie(mockCookieSuffix, {
usePartitionedCookies: () => usePartitionedCookies,
});

cookieHandler.set('non-partitioned-token');
usePartitionedCookies = true;
mockSet.mockClear();
mockRemove.mockClear();
cookieHandler.set('partitioned-token');

expect(mockRemove.mock.calls).toEqual([
['__session', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'Lax', secure: true, partitioned: false }],
['__session', { sameSite: 'None', secure: true, partitioned: false }],
['__session_test-suffix', { sameSite: 'None', secure: true, partitioned: false }],
]);
expect(mockSet.mock.calls).toEqual([
[
'__session',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
[
'__session_test-suffix',
'partitioned-token',
{
expires: mockExpires,
sameSite: 'None',
secure: true,
partitioned: true,
},
],
]);
const firstInvocationOrder = mockRemove.mock.invocationCallOrder[0];
expect(mockRemove.mock.invocationCallOrder).toEqual([
firstInvocationOrder,
firstInvocationOrder + 1,
firstInvocationOrder + 2,
firstInvocationOrder + 3,
]);
expect(mockSet.mock.invocationCallOrder).toEqual([firstInvocationOrder + 4, firstInvocationOrder + 5]);
});
});
Loading
Loading