fix(*): recover from duplicate session cookies - #9286
Conversation
🦋 Changeset detectedLatest commit: c046539 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/electron
@clerk/electron-passkeys
@clerk/eslint-plugin
@clerk/expo
@clerk/expo-google-signin
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
b1c4e8b to
3556e11
Compare
|
!snapshot |
|
!snapshot |
API Changes Report
Summary
No API Changes DetectedAll packages have stable APIs with no detected changes. Report generated by Break Check Last ran on |
|
Hey @brkalow - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/astro@4.0.5-snapshot.v20260730145336 --save-exact
npm i @clerk/backend@3.14.1-snapshot.v20260730145336 --save-exact
npm i @clerk/chrome-extension@3.1.64-snapshot.v20260730145336 --save-exact
npm i @clerk/clerk-js@6.25.13-snapshot.v20260730145336 --save-exact
npm i @clerk/electron@0.0.25-snapshot.v20260730145336 --save-exact
npm i @clerk/electron-passkeys@0.0.4-snapshot.v20260730145336 --save-exact
npm i @clerk/eslint-plugin@0.2.1-snapshot.v20260730145336 --save-exact
npm i @clerk/expo@4.1.3-snapshot.v20260730145336 --save-exact
npm i @clerk/expo-google-signin@1.0.1-snapshot.v20260730145336 --save-exact
npm i @clerk/expo-passkeys@2.0.4-snapshot.v20260730145336 --save-exact
npm i @clerk/express@2.1.49-snapshot.v20260730145336 --save-exact
npm i @clerk/fastify@3.1.59-snapshot.v20260730145336 --save-exact
npm i @clerk/headless@0.0.18-snapshot.v20260730145336 --save-exact
npm i @clerk/hono@0.1.59-snapshot.v20260730145336 --save-exact
npm i @clerk/localizations@4.13.10-snapshot.v20260730145336 --save-exact
npm i @clerk/msw@0.0.54-snapshot.v20260730145336 --save-exact
npm i @clerk/nextjs@7.6.4-snapshot.v20260730145336 --save-exact
npm i @clerk/nuxt@3.0.1-snapshot.v20260730145336 --save-exact
npm i @clerk/react@6.12.10-snapshot.v20260730145336 --save-exact
npm i @clerk/react-router@3.6.3-snapshot.v20260730145336 --save-exact
npm i @clerk/shared@4.25.10-snapshot.v20260730145336 --save-exact
npm i @clerk/swingset@0.0.28-snapshot.v20260730145336 --save-exact
npm i @clerk/tanstack-react-start@1.4.26-snapshot.v20260730145336 --save-exact
npm i @clerk/testing@2.2.16-snapshot.v20260730145336 --save-exact
npm i @clerk/ui@1.27.2-snapshot.v20260730145336 --save-exact
npm i @clerk/upgrade@2.0.5-snapshot.v20260730145336 --save-exact
npm i @clerk/vue@2.4.22-snapshot.v20260730145336 --save-exact |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
📝 WalkthroughWalkthroughThe change removes stale non-partitioned session and client UAT cookie variants before partitioned cookies are written or removed. Tests now validate cookie names, attributes, cleanup behavior, and operation ordering. Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/backend/src/tokens/clerkRequest.ts (1)
117-130: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDouble-parsing makes
get()andgetAll()disagree on some values.
cookie.parsenormalizes values (strips surrounding double quotes, skips invalid names), whileparseCookiePairsreturns raw slices. For a quoted cookie value,get(name)returns the unquoted value butgetAll(name)[0]returns it with quotes, so session-token selection can operate on a different string than the legacy path. SinceCookieMap.appendalready sets the first value for a name, a single pass overparseCookiePairs(or normalizing pair values the same wayparsedoes) keeps both APIs consistent and avoids parsing the header twice.♻️ Single-pass alternative
private parseCookies(req: Request) { const cookieHeader = this.decodeCookieValue(req.headers.get('cookie') || ''); - const cookiesRecord = parse(cookieHeader); const cookies = new CookieMap(); - for (const [name, value] of Object.entries(cookiesRecord)) { - cookies.set(name, value); - } - for (const [name, value] of this.parseCookiePairs(cookieHeader)) { cookies.append(name, value); } return cookies; }Note this drops the
parseimport if no longer used elsewhere.🤖 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/backend/src/tokens/clerkRequest.ts` around lines 117 - 130, Update the cookie construction logic around CookieMap and parseCookiePairs to use a single parsing path, removing the separate cookie.parse/Object.entries pass. Ensure pair values are normalized consistently with the existing parse behavior so get() and getAll() return identical values, and remove the parse import if it is no longer used.packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts (1)
191-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBoth new ordering tests assert absolute
invocationCallOrderoffsets.invocationCallOrderis a shared, process-wide counter, so unrelated mock calls interleaving between the removes and sets shift the expected numbers (visible already in the+4..+7gaps in the clientUat test) and break the tests for reasons unrelated to ordering.
packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts#L191-L198: replace the four-element offset array with a relative check, e.g.expect(Math.max(...mockRemove.mock.invocationCallOrder)).toBeLessThan(Math.min(...mockSet.mock.invocationCallOrder)).packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts#L222-L231: apply the same relative remove-before-set assertion instead of the+4/+5/+6/+7and+8/+9offsets.🤖 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__/session.test.ts` around lines 191 - 198, Replace the absolute invocationCallOrder offset assertions with relative remove-before-set checks in the ordering tests. In packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts lines 191-198 and packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts lines 222-231, assert that the latest mockRemove invocation occurs before the earliest mockSet invocation, preserving the intended ordering without assuming contiguous global call numbers.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@packages/backend/src/tokens/authenticateContext.ts`:
- Around line 383-406: Update selectBestSessionToken so candidates are filtered
by sessionTokenBelongsToInstance before applying sessionTokenUsable, ensuring a
fresh foreign-instance token cannot outrank this instance’s token. Preserve the
existing cross-instance fallback only when no instance-owned candidates exist,
and retain freshest-token selection within the chosen set.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts`:
- Around line 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.
---
Nitpick comments:
In `@packages/backend/src/tokens/clerkRequest.ts`:
- Around line 117-130: Update the cookie construction logic around CookieMap and
parseCookiePairs to use a single parsing path, removing the separate
cookie.parse/Object.entries pass. Ensure pair values are normalized consistently
with the existing parse behavior so get() and getAll() return identical values,
and remove the parse import if it is no longer used.
In `@packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts`:
- Around line 191-198: Replace the absolute invocationCallOrder offset
assertions with relative remove-before-set checks in the ordering tests. In
packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts lines 191-198
and packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts lines
222-231, assert that the latest mockRemove invocation occurs before the earliest
mockSet invocation, preserving the intended ordering without assuming contiguous
global call numbers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 877419f5-e8cb-4311-ba98-1f614df51932
📒 Files selected for processing (11)
.changeset/duplicate-session-cookies.mdpackages/backend/src/tokens/__tests__/authenticateContext.test.tspackages/backend/src/tokens/__tests__/clerkRequest.test.tspackages/backend/src/tokens/authenticateContext.tspackages/backend/src/tokens/clerkRequest.tspackages/clerk-js/src/core/auth/AuthCookieService.tspackages/clerk-js/src/core/auth/__tests__/AuthCookieService.test.tspackages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.tspackages/clerk-js/src/core/auth/cookies/__tests__/session.test.tspackages/clerk-js/src/core/auth/cookies/clientUat.tspackages/clerk-js/src/core/auth/cookies/session.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)clerk/clerk-ios(auto-detected)clerk/clerk-android(auto-detected)clerk/cli(auto-detected)
| private selectBestSessionToken(tokens: string[]) { | ||
| const fallbackToken = tokens[0]; | ||
| const usableCandidates: Array<{ token: string; data: Jwt }> = []; | ||
|
|
||
| for (const token of tokens) { | ||
| const data = this.decodeSessionToken(token); | ||
|
|
||
| if (data && this.sessionTokenUsable(data)) { | ||
| usableCandidates.push({ token, data }); | ||
| } | ||
| } | ||
|
|
||
| const candidatesForInstance = usableCandidates.filter(candidate => | ||
| this.sessionTokenBelongsToInstance(candidate.data), | ||
| ); | ||
| const candidates = candidatesForInstance.length ? candidatesForInstance : usableCandidates; | ||
| const freshestCandidate = candidates.reduce<(typeof candidates)[number] | undefined>( | ||
| (freshest, candidate) => | ||
| !freshest || candidate.data.payload.iat > freshest.data.payload.iat ? candidate : freshest, | ||
| undefined, | ||
| ); | ||
|
|
||
| return freshestCandidate?.token || fallbackToken; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Usability filter runs before the instance filter, so a foreign-instance token can win over this instance's token.
usableCandidates is built first, then narrowed by instance. If this instance's duplicate is expired but a co-hosted app's duplicate (same eTLD+1, different iss) is fresh, candidatesForInstance is empty and the foreign token is selected as sessionTokenInCookie. Preferring instance ownership before usability keeps selection scoped to this instance and only falls back cross-instance when nothing matches.
♻️ Suggested ordering
- const candidatesForInstance = usableCandidates.filter(candidate =>
- this.sessionTokenBelongsToInstance(candidate.data),
- );
- const candidates = candidatesForInstance.length ? candidatesForInstance : usableCandidates;
+ const candidatesForInstance = usableCandidates.filter(candidate =>
+ this.sessionTokenBelongsToInstance(candidate.data),
+ );
+ const instanceTokens = tokens
+ .map(token => ({ token, data: this.decodeSessionToken(token) }))
+ .filter((c): c is { token: string; data: Jwt } => !!c.data && this.sessionTokenBelongsToInstance(c.data));
+ const candidates = candidatesForInstance.length
+ ? candidatesForInstance
+ : instanceTokens.length
+ ? instanceTokens
+ : usableCandidates;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private selectBestSessionToken(tokens: string[]) { | |
| const fallbackToken = tokens[0]; | |
| const usableCandidates: Array<{ token: string; data: Jwt }> = []; | |
| for (const token of tokens) { | |
| const data = this.decodeSessionToken(token); | |
| if (data && this.sessionTokenUsable(data)) { | |
| usableCandidates.push({ token, data }); | |
| } | |
| } | |
| const candidatesForInstance = usableCandidates.filter(candidate => | |
| this.sessionTokenBelongsToInstance(candidate.data), | |
| ); | |
| const candidates = candidatesForInstance.length ? candidatesForInstance : usableCandidates; | |
| const freshestCandidate = candidates.reduce<(typeof candidates)[number] | undefined>( | |
| (freshest, candidate) => | |
| !freshest || candidate.data.payload.iat > freshest.data.payload.iat ? candidate : freshest, | |
| undefined, | |
| ); | |
| return freshestCandidate?.token || fallbackToken; | |
| } | |
| private selectBestSessionToken(tokens: string[]) { | |
| const fallbackToken = tokens[0]; | |
| const usableCandidates: Array<{ token: string; data: Jwt }> = []; | |
| for (const token of tokens) { | |
| const data = this.decodeSessionToken(token); | |
| if (data && this.sessionTokenUsable(data)) { | |
| usableCandidates.push({ token, data }); | |
| } | |
| } | |
| const candidatesForInstance = usableCandidates.filter(candidate => | |
| this.sessionTokenBelongsToInstance(candidate.data), | |
| ); | |
| const instanceTokens = tokens | |
| .map(token => ({ token, data: this.decodeSessionToken(token) })) | |
| .filter( | |
| (c): c is { token: string; data: Jwt } => | |
| !!c.data && this.sessionTokenBelongsToInstance(c.data), | |
| ); | |
| const candidates = candidatesForInstance.length | |
| ? candidatesForInstance | |
| : instanceTokens.length | |
| ? instanceTokens | |
| : usableCandidates; | |
| const freshestCandidate = candidates.reduce<(typeof candidates)[number] | undefined>( | |
| (freshest, candidate) => | |
| !freshest || candidate.data.payload.iat > freshest.data.payload.iat ? candidate : freshest, | |
| undefined, | |
| ); | |
| return freshestCandidate?.token || fallbackToken; | |
| } |
🤖 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/backend/src/tokens/authenticateContext.ts` around lines 383 - 406,
Update selectBestSessionToken so candidates are filtered by
sessionTokenBelongsToInstance before applying sessionTokenUsable, ensuring a
fresh foreign-instance token cannot outrank this instance’s token. Preserve the
existing cross-instance fallback only when no instance-owned candidates exist,
and retain freshest-token selection within the chosen set.
| 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); |
There was a problem hiding this comment.
📐 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/bash
set -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.tsRepository: clerk/javascript
Length of output: 14517
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## relevant tsconfig contents\n'
for f in packages/clerk-js/tsconfig.json packages/clerk-js/tsconfig.*.json; do
[ -f "$f" ] && { echo "--- $f"; cat "$f"; echo; }
done
printf '\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.tsRepository: clerk/javascript
Length of output: 11138
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '## package tsconfig references'
rg -n '"include"|"exclude"|__tests__|test' packages/clerk-js/tsconfig*.json packages/clerk-js/package.json
echo
echo '## ClientResource definition'
rg -n 'export (type|interface) ClientResource|type ClientResource|interface ClientResource' packages/clerk-js/src packages/clerk-js -g '!**/dist/**' -g '!**/build/**'
echo
echo '## 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/bash
set -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 80Repository: clerk/javascript
Length of output: 4730
🏁 Script executed:
#!/bin/bash
set -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'
for f in $files; do
echo "--- $f"
sed -n '1,240p' "$f" | rg -n 'ClientResource|signedInSessions|updatedAt|id:' -n -C2 || true
doneRepository: clerk/javascript
Length of output: 10076
🏁 Script executed:
#!/bin/bash
set -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 50Repository: clerk/javascript
Length of output: 1448
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '## locate ClientResource definition'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' 'ClientResource' packages/shared packages/clerk-js/src | head -n 80
echo
echo '## package/shared type files'
fd -a 'types*.ts' packages/shared | head -n 50Repository: clerk/javascript
Length of output: 5444
🏁 Script executed:
#!/bin/bash
set -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"
echo
echo "## direct references in clientUat.ts"
sed -n '1,220p' packages/clerk-js/src/core/auth/cookies/clientUat.tsRepository: 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.
Description
Update Clerk-js to clean up cookies when /v1/environment loads (for CHIPS config), preventing duplicate partitioned & non-partitioned cookies.
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change