Skip to content
5 changes: 5 additions & 0 deletions .changeset/old-wombats-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Added \_clerk_skip_cache query string param to token requests initiated with skipCache option
5 changes: 3 additions & 2 deletions packages/clerk-js/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export const PRESERVED_QUERYSTRING_PARAMS = [
];

export const CLERK_MODAL_STATE = '__clerk_modal_state';
export const CLERK_SYNCED = '__clerk_synced';
export const CLERK_SUFFIXED_COOKIES = 'suffixed_cookies';
export const CLERK_SATELLITE_URL = '__clerk_satellite_url';
export const CLERK_SKIP_CACHE = '_clerk_skip_cache';
export const CLERK_SUFFIXED_COOKIES = 'suffixed_cookies';
export const CLERK_SYNCED = '__clerk_synced';
export const ERROR_CODES = {
FORM_IDENTIFIER_NOT_FOUND: 'form_identifier_not_found',
FORM_PASSWORD_INCORRECT: 'form_password_incorrect',
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ export class Session extends BaseResource implements SessionResource {
// TODO: update template endpoint to accept organizationId
const params: Record<string, string | null> = template ? {} : { organizationId };

const tokenResolver = Token.create(path, params);
const tokenResolver = Token.create(path, params, skipCache);

// Cache the promise immediately to prevent concurrent calls from triggering duplicate requests
SessionTokenCache.set({ tokenId, tokenResolver });
Expand Down
15 changes: 10 additions & 5 deletions packages/clerk-js/src/core/resources/Token.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import type { JWT, TokenJSON, TokenJSONSnapshot, TokenResource } from '@clerk/shared/types';

import { decode } from '../../utils';
import { BaseResource } from './internal';
import { CLERK_SKIP_CACHE } from '@/core/constants';
import { decode } from '@/utils';

import { BaseResource } from './Base';

export class Token extends BaseResource implements TokenResource {
pathRoot = 'tokens';

jwt?: JWT;

static async create(path: string, body: any = {}): Promise<TokenResource> {
static async create(path: string, body: any = {}, skipCache = false): Promise<TokenResource> {
const search = skipCache ? `${CLERK_SKIP_CACHE}=true` : undefined;

const json = (await BaseResource._fetch<TokenJSON>({
path,
method: 'POST',
body,
method: 'POST',
path,
search,
})) as unknown as TokenJSON;

return new Token(json, path);
Expand Down
55 changes: 52 additions & 3 deletions packages/clerk-js/src/core/resources/__tests__/Token.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { InstanceType } from '@clerk/shared/types';
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest';

import { mockFetch, mockNetworkFailedFetch } from '@/test/core-fixtures';
import { mockFetch, mockJwt, mockNetworkFailedFetch } from '@/test/core-fixtures';
import { debugLogger } from '@/utils/debug';

import { SUPPORTED_FAPI_VERSION } from '../../constants';
import { CLERK_SKIP_CACHE, SUPPORTED_FAPI_VERSION } from '../../constants';
import { createFapiClient } from '../../fapiClient';
import { BaseResource } from '../internal';
import { Token } from '../Token';
Expand Down Expand Up @@ -44,7 +44,7 @@ describe('Token', () => {
});

describe('with offline browser and network failure', () => {
let warnSpy;
let warnSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
Object.defineProperty(window.navigator, 'onLine', {
Expand Down Expand Up @@ -103,5 +103,54 @@ describe('Token', () => {
});
});
});

it('creates token successfully with valid response', async () => {
mockFetch(true, 200, { jwt: mockJwt });
BaseResource.clerk = { getFapiClient: () => createFapiClient(baseFapiClientOptions) } as any;

const token = await Token.create('/path/to/tokens', { organizationId: 'org_123' });

expect(global.fetch).toHaveBeenCalledTimes(1);
const [url, options] = (global.fetch as Mock).mock.calls[0];
expect(url.toString()).toContain('https://clerk.example.com/v1/path/to/tokens');
expect(url.toString()).not.toContain(CLERK_SKIP_CACHE);
expect(options).toMatchObject({
body: 'organization_id=org_123',
credentials: 'include',
method: 'POST',
});
expect(token).toBeInstanceOf(Token);
expect(token.jwt).toBeDefined();
});

it('creates token with skipCache=false by default', async () => {
mockFetch(true, 200, { jwt: mockJwt });
BaseResource.clerk = { getFapiClient: () => createFapiClient(baseFapiClientOptions) } as any;

await Token.create('/path/to/tokens');

const [url] = (global.fetch as Mock).mock.calls[0];
expect(url.toString()).not.toContain(CLERK_SKIP_CACHE);
});

it('creates token with skipCache=true and includes query parameter', async () => {
mockFetch(true, 200, { jwt: mockJwt });
BaseResource.clerk = { getFapiClient: () => createFapiClient(baseFapiClientOptions) } as any;

await Token.create('/path/to/tokens', {}, true);

const [url] = (global.fetch as Mock).mock.calls[0];
expect(url.toString()).toContain(`${CLERK_SKIP_CACHE}=true`);
});

it('creates token with skipCache=false explicitly and excludes query parameter', async () => {
mockFetch(true, 200, { jwt: mockJwt });
BaseResource.clerk = { getFapiClient: () => createFapiClient(baseFapiClientOptions) } as any;

await Token.create('/path/to/tokens', {}, false);

const [url] = (global.fetch as Mock).mock.calls[0];
expect(url.toString()).not.toContain(CLERK_SKIP_CACHE);
});
});
});
Loading