Skip to content

fix(clerk-js,expo): harden native session-minter token path - #9284

Open
nikosdouvlis wants to merge 8 commits into
mainfrom
nikos/native-minter-hardening
Open

fix(clerk-js,expo): harden native session-minter token path#9284
nikosdouvlis wants to merge 8 commits into
mainfrom
nikos/native-minter-hardening

Conversation

@nikosdouvlis

@nikosdouvlis nikosdouvlis commented Jul 30, 2026

Copy link
Copy Markdown
Member

Why

Native SDKs are getting Session Minter support: the app sends its previous session token to the edge as the seed the new token is minted from. That raises the stakes on the token clerk-js holds. A stale or regressed lastActiveToken is no longer just a stale local token, it becomes the input to the next mint, so staleness chains forward instead of self-correcting. This closes the Expo/native gaps that let that happen.

What changed

clerk-js: Client.fromJSON rebuilds every session object on a client update and used to adopt the payload's last_active_token unconditionally, so a piggybacked older token could regress the active session's token. It now carries the freshest of the prior instance's token and the incoming one through the same sid/org oiat guard the in-place path already uses, so a genuine session or org switch still adopts the incoming token while a stale piggyback cannot win. Clearing on a token-less payload is preserved.

expo: a failed initial load used to substitute dummy resources for both environment and client, wiping auth_config.session_minter for the whole instance lifetime; it now keeps a good cached environment when only the client cache is missing. The native 401 handler gets a short cooldown so a burst collapses to a single native-recovery cycle, and a device-token rotation observed through the token cache reopens the cooldown so fresh native identity gets a fresh attempt. tokenCache doc corrected to say it holds the client JWT.

Related

Part of a four-PR set adding native Session Minter support. This one covers clerk-js and Expo; the edge worker (cloudflare-workers), clerk-ios, and clerk-android land alongside it.

Native apps feed the previous session token to the edge Session Minter as the
mint seed, so a stale or regressed lastActiveToken is no longer just a stale
local token, it becomes the input to the next mint and the staleness chains
forward. Three gaps made that reachable on the Expo/native path.

clerk-js: Client.fromJSON rebuilds every session object on a client update, and
the rebuilt objects replaced the live ones while unconditionally adopting the
payload's last_active_token. A piggybacked response carrying an older token
could therefore regress the active session's token. fromJSON now carries the
freshest of the prior instance's token and the payload token, using the same
same-sid/same-org oiat guard that already protects the in-place path, so a
genuine session or org switch still adopts the incoming token while a stale
piggyback cannot win. Token-clearing on a token-less payload is preserved.

expo: a failed initial load substituted dummy resources for both environment
and client, wiping auth_config.session_minter for the whole instance lifetime
even when a good cached environment existed; it now substitutes only the
missing resource. The patched 401 handler ran full native-state recovery plus a
client refetch on every 401; a short cooldown collapses a burst to one cycle,
and a rotated device token clears the cooldown so fresh native identity always
gets a fresh attempt. tokenCache doc corrected to say it stores the client JWT.
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5bff88b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@clerk/clerk-js Patch
@clerk/expo Patch
@clerk/chrome-extension Patch
@clerk/electron Patch

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

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 7, 2026 1:20pm
swingset Ready Ready Preview Aug 7, 2026 1:20pm

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Jul 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9284

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9284

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9284

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9284

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9284

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9284

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9284

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9284

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9284

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9284

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9284

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9284

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9284

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9284

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9284

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9284

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9284

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9284

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9284

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9284

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9284

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9284

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9284

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9284

commit: 5bff88b

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-07T13:22:03.728Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 5bff88b.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Clerk JS now preserves the freshest lastActiveToken during session hydration, touch updates, and piggybacked client reconstruction. Expo now caches valid environment resources independently from client resources, excludes dummy resources from persistence, and schedules recovery for missing resources. Native unauthenticated recovery now uses cooldown and token-rollback synchronization rules. Tests cover token monotonicity, cache persistence, fallback behavior, and recovery re-entry.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: wobsoriano

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: hardening the Clerk JS and Expo native session-minter token path.
Description check ✅ Passed The description directly explains the Clerk JS and Expo token, cache, and native recovery changes.

Comment @coderabbitai help to get the list of available commands.

…e device token

The native 401 cooldown clears whenever the device-token cache changes, so a
fresh external identity gets a fresh recovery attempt. But a failed recovery
rolls the device token back to its previous value, and that rollback write fired
the same listener and cleared the cooldown, so a second 401 inside the window
re-ran full recovery, reopening the storm on exactly the failing-recovery path
the cooldown is meant to bound.

Route the rollback write on the 401 path through the notification suppression
recovery already uses for its own writes, so a rollback no longer clears the
cooldown. The native-client-event recovery path keeps notifying, since there the
rollback notification is load-bearing: it queues the native refresh that pushes
the restored token back to the native module.
The suppressed rollback write skips the token-cache listener that used to
resync native, so rejecting a foreign client left native holding the
rejected token. The reject branch now pushes the restored token to native
directly. The error branch stays cache-only since its second-chance
recovery re-adopts the native token anyway.

The cooldown stamp moves to when the attempt settles, so a slow recovery
no longer finishes with a mostly spent window. A rotation landing
mid-attempt still clears the stamp and forces a fresh attempt, and a
backwards clock jump counts as expired instead of waiting out the gap.

Persisting the dummy client snapshot made the next boot see a populated
cache and skip recovery. The save listener now skips the dummy, and a
previously persisted dummy is treated as missing on load.
@nikosdouvlis nikosdouvlis changed the title fix: harden native session-minter token path fix(clerk-js,expo): harden native session-minter token path Aug 7, 2026
The dummy-client save guard skipped the whole listener block, including the
SessionJWTCache.remove() that wipes the offline JWT fallback on a
sessionless emission. getToken falls back to that cache on network errors
regardless of which client is active, so the wipe is load-bearing and is
now restored for dummy emissions.

The environment cache had the symmetric hole the client guard closed: an
offline boot with only the environment missing persisted the dummy
environment snapshot, and the next boot saw both caches populated and never
scheduled recovery. The save listener now skips dummy environment
snapshots and a previously persisted one is treated as missing on load.
…ardening

# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

shouldKeepExistingLastActiveToken moves to tokenFreshness next to the
primitives it composes, and Client.fromJSON assigns the carried-forward
token directly, dropping the one-caller __internal method. The redundant
normalizeOrgId wrap goes; tokenOrgId already returns an empty string.

refreshJsClientFromNativeState reports 'refreshed' or 'restored' instead
of taking a controller ref, so the 401 caller owns the restored-token
push to native and the shared helper stays policy-free. The rollback
write branch collapses into one syncNativeDeviceTokenToCache call.

Dummy detection gets isDummyClient/isDummyEnvironment predicates beside
the constants; the environment guard now runs before the snapshot it
used to discard.

Tests: the 401 foreign-client push assertion folds into the existing
recovery test, the piggyback guard is covered by a direct Client.fromJSON
test instead of 45 lines of fake-clerk wiring, and the three hand-rolled
resource-cache stubs become one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant