From 260061f7083e6c807658f99f6c622c3643d9a23f Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 24 May 2026 12:17:35 +0300 Subject: [PATCH] feat(js,shared): always send the session-minter request shape for /tokens The `auth_config.session_minter` gate in clerk-js no longer adds value: the FAPI proxy strips `token` and `force_origin` from the /tokens body unconditionally, and the Go backend has a matching strip-and-capture safety net. So clerk-js can send the minter shape for every instance without breaking the ones the Minter hasn't activated yet. This also lets the next iteration drop the remaining Go-side flag without a coordinated rollout. - `AuthConfigResource.sessionMinter` and `AuthConfigJSON.session_minter` are removed - The `expired_token` retry on 422 `missing_expired_token` was the fallback for non-minter clients; it goes away with the gate - Tests updated; the legacy-retry describe block is deleted --- .changeset/drop-session-minter-gate.md | 8 + .../clerk-js/src/core/resources/AuthConfig.ts | 3 - .../clerk-js/src/core/resources/Session.ts | 28 +-- .../resources/__tests__/AuthConfig.test.ts | 1 - .../core/resources/__tests__/Session.test.ts | 190 +----------------- packages/shared/src/types/authConfig.ts | 1 - packages/shared/src/types/json.ts | 1 - 7 files changed, 15 insertions(+), 217 deletions(-) create mode 100644 .changeset/drop-session-minter-gate.md diff --git a/.changeset/drop-session-minter-gate.md b/.changeset/drop-session-minter-gate.md new file mode 100644 index 00000000000..e3acf679fd4 --- /dev/null +++ b/.changeset/drop-session-minter-gate.md @@ -0,0 +1,8 @@ +--- +"@clerk/clerk-js": minor +"@clerk/shared": minor +--- + +Always use the Session Minter request shape for `/tokens` calls. The previous gate, sourced from `auth_config.session_minter` on the environment payload, is removed so all instances send the prior session token in the request body and `forceOrigin=true` when `skipCache` is set. The FAPI proxy strips both fields when no minter is reachable, so behavior is unchanged for instances not yet enrolled. The legacy `expired_token` retry path on 422 `missing_expired_token` is no longer needed and has been deleted. + +`AuthConfigResource.sessionMinter` and `AuthConfigJSON.session_minter` are removed. diff --git a/packages/clerk-js/src/core/resources/AuthConfig.ts b/packages/clerk-js/src/core/resources/AuthConfig.ts index 3bfc61dbacf..b95dfaf5ba2 100644 --- a/packages/clerk-js/src/core/resources/AuthConfig.ts +++ b/packages/clerk-js/src/core/resources/AuthConfig.ts @@ -8,7 +8,6 @@ export class AuthConfig extends BaseResource implements AuthConfigResource { reverification: boolean = false; singleSessionMode: boolean = false; preferredChannels: Record | null = null; - sessionMinter: boolean = false; public constructor(data: Partial | null = null) { super(); @@ -24,7 +23,6 @@ export class AuthConfig extends BaseResource implements AuthConfigResource { this.reverification = this.withDefault(data.reverification, this.reverification); this.singleSessionMode = this.withDefault(data.single_session_mode, this.singleSessionMode); this.preferredChannels = this.withDefault(data.preferred_channels, this.preferredChannels); - this.sessionMinter = this.withDefault(data.session_minter, this.sessionMinter); return this; } @@ -35,7 +33,6 @@ export class AuthConfig extends BaseResource implements AuthConfigResource { object: 'auth_config', reverification: this.reverification, single_session_mode: this.singleSessionMode, - session_minter: this.sessionMinter, }; } } diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 068dfe1ea41..60dfeeb6c18 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,13 +1,6 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; import { isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; -import { - ClerkOfflineError, - ClerkRuntimeError, - ClerkWebAuthnError, - is4xxError, - is429Error, - MissingExpiredTokenError, -} from '@clerk/shared/error'; +import { ClerkOfflineError, ClerkRuntimeError, ClerkWebAuthnError, is4xxError, is429Error } from '@clerk/shared/error'; import { convertJSONToPublicKeyRequestOptions, serializePublicKeyCredentialAssertion, @@ -483,28 +476,15 @@ export class Session extends BaseResource implements SessionResource { ): Promise { const path = template ? `${this.path()}/tokens/${template}` : `${this.path()}/tokens`; // TODO: update template endpoint to accept organizationId - const sessionMinterEnabled = Session.clerk?.__internal_environment?.authConfig?.sessionMinter; const params: Record = template ? {} : { organizationId: organizationId ?? null, - ...(sessionMinterEnabled && this.lastActiveToken ? { token: this.lastActiveToken.getRawString() } : {}), - ...(sessionMinterEnabled && skipCache ? { forceOrigin: 'true' } : {}), + ...(this.lastActiveToken ? { token: this.lastActiveToken.getRawString() } : {}), + ...(skipCache ? { forceOrigin: 'true' } : {}), }; - if (sessionMinterEnabled) { - // Session Minter sends the token in the body, no expired_token retry needed - return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined); - } - - // TODO: Remove this expired_token retry flow when the sessionMinter flag is removed - const lastActiveToken = this.lastActiveToken?.getRawString(); - return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined).catch(e => { - if (MissingExpiredTokenError.is(e) && lastActiveToken) { - return Token.create(path, { ...params }, { expired_token: lastActiveToken }); - } - throw e; - }); + return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined); } #dispatchTokenEvents(token: TokenResource, shouldDispatch: boolean): void { diff --git a/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts b/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts index 3d16c8d430b..bfce1d5c021 100644 --- a/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts @@ -46,7 +46,6 @@ describe('AuthConfig', () => { id: '', reverification: true, single_session_mode: true, - session_minter: false, }); }); }); diff --git a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts b/packages/clerk-js/src/core/resources/__tests__/Session.test.ts index aee7f42f614..824e4ce11c2 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Session.test.ts @@ -1,4 +1,4 @@ -import { ClerkAPIResponseError, ClerkOfflineError } from '@clerk/shared/error'; +import { ClerkOfflineError } from '@clerk/shared/error'; import type { InstanceType, OrganizationJSON, SessionJSON } from '@clerk/shared/types'; import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; @@ -1624,11 +1624,7 @@ describe('Session', () => { beforeEach(() => { dispatchSpy = vi.spyOn(eventBus, 'emit'); fetchSpy = vi.spyOn(BaseResource, '_fetch' as any); - BaseResource.clerk = clerkMock({ - __internal_environment: { - authConfig: { sessionMinter: true }, - }, - }) as any; + BaseResource.clerk = clerkMock() as any; }); afterEach(() => { @@ -1748,11 +1744,7 @@ describe('Session', () => { beforeEach(() => { dispatchSpy = vi.spyOn(eventBus, 'emit'); fetchSpy = vi.spyOn(BaseResource, '_fetch' as any); - BaseResource.clerk = clerkMock({ - __internal_environment: { - authConfig: { sessionMinter: true }, - }, - }) as any; + BaseResource.clerk = clerkMock() as any; }); afterEach(() => { @@ -1812,182 +1804,6 @@ describe('Session', () => { expect(fetchSpy).toHaveBeenCalledTimes(1); expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('forceOrigin'); }); - - it('does not include forceOrigin when sessionMinter is false even with skipCache true', async () => { - BaseResource.clerk = clerkMock({ - __internal_environment: { - authConfig: { sessionMinter: false }, - }, - }) as any; - - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as SessionJSON); - - SessionTokenCache.clear(); - - fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt }); - - await session.getToken({ skipCache: true }); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('forceOrigin'); - }); - }); - - describe('origin outage mode fallback', () => { - let dispatchSpy: ReturnType; - let fetchSpy: ReturnType; - - beforeEach(() => { - SessionTokenCache.clear(); - dispatchSpy = vi.spyOn(eventBus, 'emit'); - fetchSpy = vi.spyOn(BaseResource, '_fetch' as any); - BaseResource.clerk = clerkMock() as any; - }); - - afterEach(() => { - dispatchSpy?.mockRestore(); - fetchSpy?.mockRestore(); - BaseResource.clerk = null as any; - }); - - it('should retry with expired token when API returns 422 with missing_expired_token error', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Missing expired token', { - data: [ - { code: 'missing_expired_token', message: 'Missing expired token', long_message: 'Missing expired token' }, - ], - status: 422, - }); - fetchSpy.mockRejectedValueOnce(errorResponse); - - fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt }); - - await session.getToken(); - - expect(fetchSpy).toHaveBeenCalledTimes(2); - - expect(fetchSpy.mock.calls[0][0]).toMatchObject({ - path: '/client/sessions/session_1/tokens', - method: 'POST', - body: { organizationId: null }, - }); - - expect(fetchSpy.mock.calls[1][0]).toMatchObject({ - path: '/client/sessions/session_1/tokens', - method: 'POST', - body: { organizationId: null }, - search: { expired_token: mockJwt }, - }); - }); - - it('should not retry with expired token when lastActiveToken is not available', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: null, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as unknown as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Missing expired token', { - data: [ - { code: 'missing_expired_token', message: 'Missing expired token', long_message: 'Missing expired token' }, - ], - status: 422, - }); - fetchSpy.mockRejectedValue(errorResponse); - - await expect(session.getToken()).rejects.toMatchObject({ - status: 422, - errors: [{ code: 'missing_expired_token' }], - }); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - - it('should not retry with expired token for non-422 errors', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Bad request', { - data: [{ code: 'bad_request', message: 'Bad request', long_message: 'Bad request' }], - status: 400, - }); - fetchSpy.mockRejectedValueOnce(errorResponse); - - await expect(session.getToken()).rejects.toThrow(ClerkAPIResponseError); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - - it('should not retry with expired token when error code is different', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as unknown as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Validation failed', { - data: [{ code: 'validation_error', message: 'Validation failed', long_message: 'Validation failed' }], - status: 422, - }); - fetchSpy.mockRejectedValue(errorResponse); - - await expect(session.getToken()).rejects.toMatchObject({ - status: 422, - errors: [{ code: 'validation_error' }], - }); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); }); describe('agent', () => { diff --git a/packages/shared/src/types/authConfig.ts b/packages/shared/src/types/authConfig.ts index e0919100434..9a1fd5479f0 100644 --- a/packages/shared/src/types/authConfig.ts +++ b/packages/shared/src/types/authConfig.ts @@ -21,6 +21,5 @@ export interface AuthConfigResource extends ClerkResource { * Preferred channels for phone code providers. */ preferredChannels: Record | null; - sessionMinter: boolean; __internal_toSnapshot: () => AuthConfigJSONSnapshot; } diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts index 7c91ed39498..6a201d0bced 100644 --- a/packages/shared/src/types/json.ts +++ b/packages/shared/src/types/json.ts @@ -334,7 +334,6 @@ export interface AuthConfigJSON extends ClerkResourceJSON { claimed_at: number | null; reverification: boolean; preferred_channels?: Record; - session_minter?: boolean; } export interface VerificationJSON extends ClerkResourceJSON {