-
Notifications
You must be signed in to change notification settings - Fork 422
feat(repo): Make gettoken callable outside of react #7325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c05e291
4b34e3c
5723b14
7548863
177dd06
2fab555
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| --- | ||
| '@clerk/tanstack-react-start': minor | ||
| '@clerk/react-router': minor | ||
| '@clerk/clerk-js': minor | ||
| '@clerk/nextjs': minor | ||
| '@clerk/shared': minor | ||
| '@clerk/astro': minor | ||
| '@clerk/react': minor | ||
| '@clerk/nuxt': minor | ||
| '@clerk/vue': minor | ||
| --- | ||
|
|
||
| Add standalone `getToken()` function for retrieving session tokens outside of framework component trees. | ||
|
|
||
| This function is safe to call from anywhere in the browser, such as API interceptors, data fetching layers (e.g., React Query, SWR), or vanilla JavaScript code. It automatically waits for Clerk to initialize before returning the token. | ||
|
|
||
| import { getToken } from '@clerk/nextjs'; // or any framework package | ||
|
|
||
| // Example: Axios interceptor | ||
| axios.interceptors.request.use(async (config) => { | ||
| const token = await getToken(); | ||
| if (token) { | ||
| config.headers.Authorization = `Bearer ${token}`; | ||
| } | ||
| return config; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { updateClerkOptions } from '../internal/create-clerk-instance'; | ||
| export * from '../stores/external'; | ||
| export { getToken } from '@clerk/shared/getToken'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,9 +14,26 @@ const __BUILD_DISABLE_RHC__: string; | |
| const __BUILD_VARIANT_CHANNEL__: boolean; | ||
| const __BUILD_VARIANT_CHIPS__: boolean; | ||
|
|
||
| /** | ||
| * A promise used for coordination between standalone getToken() and clerk-js initialization. | ||
| * The __resolve and __reject callbacks allow external resolution. | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||
| interface ClerkReadyPromise extends Promise<import('@clerk/shared/types').LoadedClerk> { | ||
| // eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||
| __resolve?: (clerk: import('@clerk/shared/types').LoadedClerk) => void; | ||
| __reject?: (error: Error) => void; | ||
| } | ||
|
Comment on lines
+17
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid adding a new named global interface ( Since this is a Proposed change (keeps behavior, avoids new global name)-/**
- * A promise used for coordination between standalone getToken() and clerk-js initialization.
- * The __resolve and __reject callbacks allow external resolution.
- */
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
-interface ClerkReadyPromise extends Promise<import('@clerk/shared/types').LoadedClerk> {
- // eslint-disable-next-line @typescript-eslint/consistent-type-imports
- __resolve?: (clerk: import('@clerk/shared/types').LoadedClerk) => void;
- __reject?: (error: Error) => void;
-}
-
interface Window {
@@
/**
@@
*/
- __clerk_internal_ready?: ClerkReadyPromise;
+ __clerk_internal_ready?: Promise<import('@clerk/shared/types').LoadedClerk> & {
+ __resolve?: (clerk: import('@clerk/shared/types').LoadedClerk) => void;
+ __reject?: (error: Error) => void;
+ };
}Also applies to: 33-38 🤖 Prompt for AI Agents |
||
|
|
||
| interface Window { | ||
| __internal_onBeforeSetActive: (intent?: 'sign-out') => Promise<void> | void; | ||
| __internal_onAfterSetActive: () => Promise<void> | void; | ||
| // eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||
| __internal_ClerkUiCtor?: import('@clerk/shared/types').ClerkUiConstructor; | ||
| /** | ||
| * Promise used for coordination between standalone getToken() from @clerk/shared and clerk-js. | ||
| * When getToken() is called before Clerk loads, it creates this promise with __resolve/__reject callbacks. | ||
| * When Clerk reaches ready/degraded/error status, it resolves/rejects this promise. | ||
| */ | ||
| __clerk_internal_ready?: ClerkReadyPromise; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { createRouteMatcher } from './routeMatcher'; | ||
| export { updateClerkOptions } from '@clerk/vue'; | ||
| export { getToken } from '@clerk/shared/getToken'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { ClerkRuntimeError } from '../errors/clerkRuntimeError'; | ||
| import { getToken } from '../getToken'; | ||
|
|
||
| describe('getToken', () => { | ||
| const originalWindow = global.window; | ||
|
|
||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| vi.restoreAllMocks(); | ||
| global.window = originalWindow; | ||
| }); | ||
|
|
||
| describe('when Clerk is already ready', () => { | ||
| it('should return token immediately', async () => { | ||
| const mockToken = 'mock-jwt-token'; | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue(mockToken), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| const token = await getToken(); | ||
| expect(token).toBe(mockToken); | ||
| expect(mockClerk.session.getToken).toHaveBeenCalledWith(undefined); | ||
| }); | ||
|
|
||
| it('should pass options to session.getToken', async () => { | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue('token'), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| await getToken({ template: 'custom-template' }); | ||
| expect(mockClerk.session.getToken).toHaveBeenCalledWith({ template: 'custom-template' }); | ||
| }); | ||
|
|
||
| it('should pass organizationId option to session.getToken', async () => { | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue('token'), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| await getToken({ organizationId: 'org_123' }); | ||
| expect(mockClerk.session.getToken).toHaveBeenCalledWith({ organizationId: 'org_123' }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when Clerk is not yet ready', () => { | ||
| it('should wait for promise resolution when clerk-js resolves the global promise', async () => { | ||
| const mockToken = 'delayed-token'; | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue(mockToken), | ||
| }, | ||
| }; | ||
|
|
||
| // Start with empty window (no Clerk) | ||
| global.window = {} as any; | ||
|
|
||
| const tokenPromise = getToken(); | ||
|
|
||
| // Simulate clerk-js loading and resolving the promise | ||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| // Resolve the promise that getToken created | ||
| const readyPromise = (global.window as any).__clerk_internal_ready; | ||
| expect(readyPromise).toBeDefined(); | ||
| expect(readyPromise.__resolve).toBeDefined(); | ||
|
|
||
| // Simulate clerk-js calling __resolve | ||
| readyPromise.__resolve(mockClerk); | ||
|
|
||
| const token = await tokenPromise; | ||
| expect(token).toBe(mockToken); | ||
| }); | ||
|
|
||
| it('should resolve when clerk-js resolves with degraded status', async () => { | ||
| const mockToken = 'degraded-token'; | ||
| const mockClerk = { | ||
| status: 'degraded', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue(mockToken), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = {} as any; | ||
|
|
||
| const tokenPromise = getToken(); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| const readyPromise = (global.window as any).__clerk_internal_ready; | ||
| readyPromise.__resolve(mockClerk); | ||
|
|
||
| const token = await tokenPromise; | ||
| expect(token).toBe(mockToken); | ||
| }); | ||
|
|
||
| it('should reject when clerk-js rejects the global promise', async () => { | ||
| global.window = {} as any; | ||
|
|
||
| const tokenPromise = getToken(); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| const readyPromise = (global.window as any).__clerk_internal_ready; | ||
| readyPromise.__reject(new Error('Clerk failed to initialize')); | ||
|
|
||
| await expect(tokenPromise).rejects.toThrow('Clerk failed to initialize'); | ||
| }); | ||
|
|
||
| it('should throw ClerkRuntimeError if promise is never resolved (timeout)', async () => { | ||
| global.window = {} as any; | ||
|
|
||
| let caughtError: unknown; | ||
| const tokenPromise = getToken().catch(e => { | ||
| caughtError = e; | ||
| }); | ||
|
|
||
| // Fast-forward past timeout (10 seconds) | ||
| await vi.advanceTimersByTimeAsync(15000); | ||
| await tokenPromise; | ||
|
|
||
| expect(caughtError).toBeInstanceOf(ClerkRuntimeError); | ||
| expect((caughtError as ClerkRuntimeError).code).toBe('clerk_runtime_load_timeout'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('multiple concurrent getToken calls', () => { | ||
| it('should share the same promise for concurrent calls', async () => { | ||
| const mockToken = 'shared-token'; | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue(mockToken), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = {} as any; | ||
|
|
||
| const tokenPromise1 = getToken(); | ||
| const tokenPromise2 = getToken(); | ||
| const tokenPromise3 = getToken(); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| const readyPromise = (global.window as any).__clerk_internal_ready; | ||
| readyPromise.__resolve(mockClerk); | ||
|
|
||
| const [token1, token2, token3] = await Promise.all([tokenPromise1, tokenPromise2, tokenPromise3]); | ||
|
|
||
| expect(token1).toBe(mockToken); | ||
| expect(token2).toBe(mockToken); | ||
| expect(token3).toBe(mockToken); | ||
| expect(mockClerk.session.getToken).toHaveBeenCalledTimes(3); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when user is not signed in', () => { | ||
| it('should return null when session is null', async () => { | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: null, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| const token = await getToken(); | ||
| expect(token).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null when session is undefined', async () => { | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: undefined, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| const token = await getToken(); | ||
| expect(token).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when Clerk status is degraded', () => { | ||
| it('should still return token', async () => { | ||
| const mockToken = 'degraded-token'; | ||
| const mockClerk = { | ||
| status: 'degraded', | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue(mockToken), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| const token = await getToken(); | ||
| expect(token).toBe(mockToken); | ||
| }); | ||
| }); | ||
|
|
||
| describe('in non-browser environment', () => { | ||
| it('should throw ClerkRuntimeError when window is undefined', async () => { | ||
| global.window = undefined as any; | ||
|
|
||
| await expect(getToken()).rejects.toThrow(ClerkRuntimeError); | ||
| await expect(getToken()).rejects.toMatchObject({ | ||
| code: 'clerk_runtime_not_browser', | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when session.getToken throws', () => { | ||
| it('should propagate the error', async () => { | ||
| const mockClerk = { | ||
| status: 'ready', | ||
| session: { | ||
| getToken: vi.fn().mockRejectedValue(new Error('Token fetch failed')), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| await expect(getToken()).rejects.toThrow('Token fetch failed'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('fallback for older clerk-js versions', () => { | ||
| it('should resolve when clerk.loaded is true but status is undefined', async () => { | ||
| const mockToken = 'legacy-token'; | ||
| const mockClerk = { | ||
| loaded: true, | ||
| status: undefined, | ||
| session: { | ||
| getToken: vi.fn().mockResolvedValue(mockToken), | ||
| }, | ||
| }; | ||
|
|
||
| global.window = { Clerk: mockClerk } as any; | ||
|
|
||
| const token = await getToken(); | ||
| expect(token).toBe(mockToken); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard
windowusage in the constructor status listener to avoid non-browser ReferenceError.The new handler directly references
window(Lines 443-450). IfClerkis constructed wherewindowis undefined, this will crash before anyinBrowser()checks can help.Proposed fix
this.#publicEventBus.on(clerkEvents.Status, status => { + if (typeof window === 'undefined') { + return; + } if (status === 'ready' || status === 'degraded') { if (window.__clerk_internal_ready?.__resolve && this.#isLoaded()) { window.__clerk_internal_ready.__resolve(this); } } else if (status === 'error') { if (window.__clerk_internal_ready?.__reject) { window.__clerk_internal_ready.__reject( new ClerkRuntimeError('Clerk failed to initialize.', { code: 'clerk_init_failed' }), ); } } });📝 Committable suggestion
🤖 Prompt for AI Agents