@workos-inc/authkit-nextjs 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/auth.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use server';
2
- import { revalidatePath, revalidateTag } from 'next/cache.js';
3
- import { cookies, headers } from 'next/headers.js';
4
- import { redirect } from 'next/navigation.js';
2
+ import { revalidatePath, revalidateTag } from 'next/cache';
3
+ import { cookies, headers } from 'next/headers';
4
+ import { redirect } from 'next/navigation';
5
5
  import { WORKOS_COOKIE_NAME } from './env-variables.js';
6
6
  import { getCookieOptions } from './cookie.js';
7
7
  import { getAuthorizationUrl } from './get-authorization-url.js';
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { getAccessTokenAction, refreshAccessTokenAction } from '../actions.js';
2
+ import { decodeJwt } from '../jwt.js';
3
+ const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
4
+ const MIN_REFRESH_DELAY_SECONDS = 15;
5
+ const MAX_REFRESH_DELAY_SECONDS = 24 * 60 * 60;
6
+ const RETRY_DELAY_SECONDS = 300; // 5 minutes for retry on error
7
+ class TokenStore {
8
+ constructor() {
9
+ this.state = {
10
+ token: undefined,
11
+ loading: false,
12
+ error: null,
13
+ };
14
+ this.listeners = new Set();
15
+ this.refreshPromise = null;
16
+ this.subscribe = (listener) => {
17
+ this.listeners.add(listener);
18
+ return () => {
19
+ this.listeners.delete(listener);
20
+ if (this.listeners.size === 0 && this.refreshTimeout) {
21
+ clearTimeout(this.refreshTimeout);
22
+ this.refreshTimeout = undefined;
23
+ }
24
+ };
25
+ };
26
+ this.getSnapshot = () => this.state;
27
+ this.getServerSnapshot = () => TokenStore.SERVER_SNAPSHOT;
28
+ }
29
+ notify() {
30
+ this.listeners.forEach((listener) => listener());
31
+ }
32
+ setState(updates) {
33
+ this.state = { ...this.state, ...updates };
34
+ this.notify();
35
+ }
36
+ scheduleRefresh(timeUntilExpiry) {
37
+ if (this.refreshTimeout) {
38
+ clearTimeout(this.refreshTimeout);
39
+ this.refreshTimeout = undefined;
40
+ }
41
+ const delay = typeof timeUntilExpiry === 'undefined' ? RETRY_DELAY_SECONDS * 1000 : this.getRefreshDelay(timeUntilExpiry);
42
+ this.refreshTimeout = setTimeout(() => {
43
+ /* istanbul ignore next */
44
+ void this.getAccessTokenSilently().catch(() => { });
45
+ }, delay);
46
+ }
47
+ getRefreshDelay(timeUntilExpiry) {
48
+ if (timeUntilExpiry <= TOKEN_EXPIRY_BUFFER_SECONDS) {
49
+ return 0; // Immediate refresh
50
+ }
51
+ const idealDelay = (timeUntilExpiry - TOKEN_EXPIRY_BUFFER_SECONDS) * 1000;
52
+ return Math.min(Math.max(idealDelay, MIN_REFRESH_DELAY_SECONDS * 1000), MAX_REFRESH_DELAY_SECONDS * 1000);
53
+ }
54
+ parseToken(token) {
55
+ if (!token)
56
+ return null;
57
+ try {
58
+ const { payload } = decodeJwt(token);
59
+ const now = Math.floor(Date.now() / 1000);
60
+ if (typeof payload.exp !== 'number') {
61
+ return null;
62
+ }
63
+ const timeUntilExpiry = payload.exp - now;
64
+ // For short-lived tokens (< 5 minutes), use a 30-second buffer
65
+ // This prevents constant refreshing when tokens only last 60 seconds
66
+ let bufferSeconds = TOKEN_EXPIRY_BUFFER_SECONDS;
67
+ const totalTokenLifetime = payload.exp - (payload.iat || now);
68
+ if (totalTokenLifetime <= 300) {
69
+ // Token lifetime is 5 minutes or less - use 30 second buffer
70
+ bufferSeconds = 30;
71
+ }
72
+ const isExpiring = payload.exp < now + bufferSeconds;
73
+ return {
74
+ payload,
75
+ expiresAt: payload.exp,
76
+ isExpiring,
77
+ timeUntilExpiry,
78
+ };
79
+ }
80
+ catch (_a) {
81
+ return null;
82
+ }
83
+ }
84
+ isRefreshing() {
85
+ return this.refreshPromise !== null;
86
+ }
87
+ clearToken() {
88
+ this.setState({ token: undefined, error: null, loading: false });
89
+ if (this.refreshTimeout) {
90
+ clearTimeout(this.refreshTimeout);
91
+ this.refreshTimeout = undefined;
92
+ }
93
+ }
94
+ async getAccessToken() {
95
+ const tokenData = this.parseToken(this.state.token);
96
+ // If we have a valid JWT that's not expiring, return it
97
+ if (tokenData && !tokenData.isExpiring) {
98
+ return this.state.token;
99
+ }
100
+ // If we have an opaque token (can't parse as JWT), return it as-is
101
+ if (this.state.token && !tokenData) {
102
+ return this.state.token;
103
+ }
104
+ // Otherwise refresh (no token or expiring JWT)
105
+ return this.refreshTokenSilently();
106
+ }
107
+ async getAccessTokenSilently() {
108
+ const tokenData = this.parseToken(this.state.token);
109
+ // If we have a valid JWT that's not expiring, return it
110
+ if (tokenData && !tokenData.isExpiring) {
111
+ // Valid non-expiring JWT - return cached token without server call
112
+ return this.state.token;
113
+ }
114
+ // If we have an opaque token (can't parse as JWT), return it as-is
115
+ if (this.state.token && !tokenData) {
116
+ // Opaque token - return cached token without server call
117
+ return this.state.token;
118
+ }
119
+ // Otherwise refresh (no token or expiring JWT)
120
+ return this.refreshTokenSilently();
121
+ }
122
+ async refreshToken() {
123
+ return this._refreshToken(false);
124
+ }
125
+ async refreshTokenSilently() {
126
+ return this._refreshToken(true);
127
+ }
128
+ async _refreshToken(silent) {
129
+ if (this.refreshPromise) {
130
+ return this.refreshPromise;
131
+ }
132
+ const previousToken = this.state.token;
133
+ // Only set loading for user-initiated refreshes, not background refreshes
134
+ if (!silent) {
135
+ this.setState({ loading: true, error: null });
136
+ }
137
+ else {
138
+ // Clear error for silent refreshes but don't set loading
139
+ this.setState({ error: null });
140
+ }
141
+ this.refreshPromise = (async () => {
142
+ try {
143
+ // For manual refresh, always call refreshAccessTokenAction
144
+ // For silent refresh, try to get existing first, then refresh if needed
145
+ let token;
146
+ if (!silent) {
147
+ // Manual refresh - always force refresh
148
+ token = await refreshAccessTokenAction();
149
+ }
150
+ else {
151
+ // Silent refresh - only fetch from server if we don't have a local token
152
+ if (!previousToken) {
153
+ // No local token, need to check server
154
+ token = await getAccessTokenAction();
155
+ const tokenData = this.parseToken(token);
156
+ // Set the token even if it's expiring, to preserve it in case refresh fails
157
+ if (token && token !== previousToken) {
158
+ this.setState({
159
+ token,
160
+ loading: false,
161
+ error: null,
162
+ });
163
+ }
164
+ // If the token from server is expiring, refresh it
165
+ if (!token || (tokenData && tokenData.isExpiring)) {
166
+ const refreshedToken = await refreshAccessTokenAction();
167
+ if (refreshedToken) {
168
+ token = refreshedToken;
169
+ }
170
+ }
171
+ }
172
+ else {
173
+ // We have a local token that needs refreshing (already checked by getAccessTokenSilently)
174
+ token = await refreshAccessTokenAction();
175
+ }
176
+ }
177
+ // Only update state if token actually changed or if loading was true
178
+ if (token !== previousToken || !silent) {
179
+ this.setState({
180
+ token,
181
+ loading: false,
182
+ error: null,
183
+ });
184
+ }
185
+ const tokenData = this.parseToken(token);
186
+ if (tokenData) {
187
+ this.scheduleRefresh(tokenData.timeUntilExpiry);
188
+ }
189
+ // If token is opaque (not a JWT), we don't schedule automatic refreshes
190
+ return token;
191
+ }
192
+ catch (error) {
193
+ // Don't clear the token immediately - keep the stale one while retrying
194
+ this.setState({
195
+ loading: false,
196
+ error: error instanceof Error ? error : new Error(String(error)),
197
+ });
198
+ // Schedule a retry after delay
199
+ this.scheduleRefresh();
200
+ throw error;
201
+ }
202
+ finally {
203
+ this.refreshPromise = null;
204
+ }
205
+ })();
206
+ return this.refreshPromise;
207
+ }
208
+ reset() {
209
+ this.state = { token: undefined, loading: false, error: null };
210
+ this.refreshPromise = null;
211
+ if (this.refreshTimeout) {
212
+ clearTimeout(this.refreshTimeout);
213
+ this.refreshTimeout = undefined;
214
+ }
215
+ this.listeners.clear();
216
+ }
217
+ }
218
+ TokenStore.SERVER_SNAPSHOT = { token: undefined, loading: false, error: null };
219
+ export const tokenStore = new TokenStore();
220
+ //# sourceMappingURL=tokenStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenStore.js","sourceRoot":"","sources":["../../../src/components/tokenStore.ts"],"names":[],"mappings":"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"}
@@ -1,166 +1,81 @@
1
- import { useCallback, useEffect, useReducer, useRef } from 'react';
2
- import { getAccessTokenAction, refreshAccessTokenAction } from '../actions.js';
1
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';
3
2
  import { useAuth } from './authkit-provider.js';
4
- import { decodeJwt } from '../jwt.js';
5
- const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
6
- const MIN_REFRESH_DELAY_SECONDS = 15; // minimum delay before refreshing token
7
- const MAX_REFRESH_DELAY_SECONDS = 24 * 60 * 60; // 24 hours
8
- const RETRY_DELAY_SECONDS = 300; // 5 minutes
9
- function tokenReducer(state, action) {
10
- switch (action.type) {
11
- case 'FETCH_START':
12
- return { ...state, loading: true, error: null };
13
- case 'FETCH_SUCCESS':
14
- return { ...state, loading: false, token: action.token, error: null };
15
- case 'FETCH_ERROR':
16
- return { ...state, loading: false, error: action.error };
17
- case 'RESET':
18
- return { ...state, token: undefined, loading: false, error: null };
19
- // istanbul ignore next
20
- default:
21
- return state;
22
- }
23
- }
24
- function getRefreshDelay(timeUntilExpiry) {
25
- const idealDelay = (timeUntilExpiry - TOKEN_EXPIRY_BUFFER_SECONDS) * 1000;
26
- return Math.min(Math.max(idealDelay, MIN_REFRESH_DELAY_SECONDS * 1000), MAX_REFRESH_DELAY_SECONDS * 1000);
27
- }
28
- function parseTokenPayload(token) {
29
- // istanbul ignore next
30
- if (!token) {
31
- return null;
32
- }
33
- try {
34
- const { payload } = decodeJwt(token);
35
- const now = Math.floor(Date.now() / 1000);
36
- // istanbul ignore next - if the token does not have an exp claim, we cannot determine expiry
37
- if (typeof payload.exp !== 'number') {
38
- return null;
39
- }
40
- return {
41
- payload,
42
- expiresAt: payload.exp,
43
- isExpiring: payload.exp < now + TOKEN_EXPIRY_BUFFER_SECONDS,
44
- timeUntilExpiry: payload.exp - now,
45
- };
46
- }
47
- catch (_a) {
48
- // istanbul ignore next
49
- return null;
50
- }
51
- }
3
+ import { tokenStore } from './tokenStore.js';
52
4
  /**
53
5
  * A hook that manages access tokens with automatic refresh.
54
6
  */
55
7
  export function useAccessToken() {
56
- const { user, sessionId, refreshAuth } = useAuth();
8
+ const { user, sessionId } = useAuth();
57
9
  const userId = user === null || user === void 0 ? void 0 : user.id;
58
- const [state, dispatch] = useReducer(tokenReducer, {
59
- token: undefined,
60
- loading: false,
61
- error: null,
62
- });
63
- const refreshTimeoutRef = useRef();
64
- const fetchingRef = useRef(false);
65
- const clearRefreshTimeout = useCallback(() => {
66
- if (refreshTimeoutRef.current) {
67
- clearTimeout(refreshTimeoutRef.current);
68
- refreshTimeoutRef.current = undefined;
69
- }
70
- }, []);
71
- // Store the current token in a ref to avoid stale closures
72
- const currentTokenRef = useRef(state.token);
73
- currentTokenRef.current = state.token;
74
- // Store updateToken in a ref to break circular dependency
75
- const updateTokenRef = useRef();
76
- // Centralized timer scheduling function
77
- const scheduleNextRefresh = useCallback((delay) => {
78
- clearRefreshTimeout();
79
- refreshTimeoutRef.current = setTimeout(() => {
80
- if (updateTokenRef.current) {
81
- updateTokenRef.current();
82
- }
83
- }, delay);
84
- }, [clearRefreshTimeout]);
85
- const updateToken = useCallback(async () => {
86
- // istanbul ignore next - safety guard against concurrent fetches
87
- if (fetchingRef.current) {
10
+ const userRef = useRef(user);
11
+ userRef.current = user;
12
+ const prevSessionRef = useRef(sessionId);
13
+ const prevUserIdRef = useRef(userId);
14
+ const tokenState = useSyncExternalStore(tokenStore.subscribe, tokenStore.getSnapshot, tokenStore.getServerSnapshot);
15
+ useEffect(() => {
16
+ if (!user) {
17
+ tokenStore.clearToken();
88
18
  return;
89
19
  }
90
- fetchingRef.current = true;
91
- try {
92
- let token = await getAccessTokenAction();
93
- if (token) {
94
- const tokenData = parseTokenPayload(token);
95
- if (!tokenData || tokenData.isExpiring) {
96
- token = await refreshAccessTokenAction();
97
- }
98
- }
99
- // Only update state if token has changed
100
- if (token !== currentTokenRef.current) {
101
- dispatch({ type: 'FETCH_SUCCESS', token });
102
- }
103
- if (token) {
104
- const tokenData = parseTokenPayload(token);
105
- if (tokenData) {
106
- const delay = getRefreshDelay(tokenData.timeUntilExpiry);
107
- scheduleNextRefresh(delay);
108
- }
109
- }
110
- return token;
111
- }
112
- catch (error) {
113
- dispatch({ type: 'FETCH_ERROR', error: error instanceof Error ? error : new Error(String(error)) });
114
- scheduleNextRefresh(RETRY_DELAY_SECONDS * 1000);
20
+ // Only clear token if user or session actually changed (not on initial mount)
21
+ const sessionChanged = prevSessionRef.current !== undefined && prevSessionRef.current !== sessionId;
22
+ const userChanged = prevUserIdRef.current !== undefined && prevUserIdRef.current !== userId;
23
+ if (sessionChanged || userChanged) {
24
+ tokenStore.clearToken();
115
25
  }
116
- finally {
117
- fetchingRef.current = false;
118
- }
119
- }, [scheduleNextRefresh]);
120
- // Assign updateToken to ref for use in scheduleNextRefresh
121
- updateTokenRef.current = updateToken;
122
- const refresh = useCallback(async () => {
123
- if (fetchingRef.current) {
26
+ prevSessionRef.current = sessionId;
27
+ prevUserIdRef.current = userId;
28
+ /* istanbul ignore next */
29
+ tokenStore.getAccessTokenSilently().catch(() => {
30
+ // Error is handled in the store
31
+ });
32
+ }, [userId, sessionId]);
33
+ useEffect(() => {
34
+ if (!user || typeof document === 'undefined') {
124
35
  return;
125
36
  }
126
- fetchingRef.current = true;
127
- dispatch({ type: 'FETCH_START' });
128
- try {
129
- await refreshAuth();
130
- const token = await getAccessTokenAction();
131
- dispatch({ type: 'FETCH_SUCCESS', token });
132
- if (token) {
133
- const tokenData = parseTokenPayload(token);
134
- if (tokenData) {
135
- const delay = getRefreshDelay(tokenData.timeUntilExpiry);
136
- scheduleNextRefresh(delay);
137
- }
37
+ /* istanbul ignore next */
38
+ const refreshIfNeeded = () => {
39
+ tokenStore.getAccessTokenSilently().catch(() => {
40
+ // Error is handled in the store
41
+ });
42
+ };
43
+ /* istanbul ignore next */
44
+ const handleWake = (event) => {
45
+ if (event.type !== 'visibilitychange' || document.visibilityState === 'visible') {
46
+ refreshIfNeeded();
138
47
  }
139
- return token;
140
- }
141
- catch (error) {
142
- const typedError = error instanceof Error ? error : new Error(String(error));
143
- dispatch({ type: 'FETCH_ERROR', error: typedError });
144
- scheduleNextRefresh(RETRY_DELAY_SECONDS * 1000);
145
- }
146
- finally {
147
- fetchingRef.current = false;
48
+ };
49
+ document.addEventListener('visibilitychange', handleWake);
50
+ window.addEventListener('focus', handleWake);
51
+ window.addEventListener('online', handleWake);
52
+ window.addEventListener('pageshow', handleWake);
53
+ return () => {
54
+ document.removeEventListener('visibilitychange', handleWake);
55
+ window.removeEventListener('focus', handleWake);
56
+ window.removeEventListener('online', handleWake);
57
+ window.removeEventListener('pageshow', handleWake);
58
+ };
59
+ }, [userId, sessionId]);
60
+ const getAccessToken = useCallback(async () => {
61
+ if (!userRef.current) {
62
+ return undefined;
148
63
  }
149
- }, [refreshAuth, scheduleNextRefresh, updateToken]);
150
- useEffect(() => {
151
- if (!user) {
152
- dispatch({ type: 'RESET' });
153
- clearRefreshTimeout();
154
- return;
64
+ return tokenStore.getAccessToken();
65
+ }, []);
66
+ // Stable refresh function
67
+ const refresh = useCallback(async () => {
68
+ if (!userRef.current) {
69
+ return undefined;
155
70
  }
156
- updateToken();
157
- return clearRefreshTimeout;
158
- }, [userId, sessionId, clearRefreshTimeout]);
71
+ return tokenStore.refreshToken();
72
+ }, []);
159
73
  return {
160
- accessToken: state.token,
161
- loading: state.loading,
162
- error: state.error,
74
+ accessToken: tokenState.token,
75
+ loading: tokenState.loading,
76
+ error: tokenState.error,
163
77
  refresh,
78
+ getAccessToken,
164
79
  };
165
80
  }
166
81
  //# sourceMappingURL=useAccessToken.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAccessToken.js","sourceRoot":"","sources":["../../../src/components/useAccessToken.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAccessToken.js","sourceRoot":"","sources":["../../../src/components/useAccessToken.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ interface TokenState {
2
+ token: string | undefined;
3
+ loading: boolean;
4
+ error: Error | null;
5
+ }
6
+ declare class TokenStore {
7
+ private static readonly SERVER_SNAPSHOT;
8
+ private state;
9
+ private listeners;
10
+ private refreshPromise;
11
+ private refreshTimeout;
12
+ subscribe: (listener: () => void) => () => void;
13
+ getSnapshot: () => TokenState;
14
+ getServerSnapshot: () => TokenState;
15
+ private notify;
16
+ private setState;
17
+ private scheduleRefresh;
18
+ private getRefreshDelay;
19
+ parseToken(token: string | undefined): {
20
+ payload: Partial<import("../jwt.js").JWTPayload & Record<string, unknown>>;
21
+ expiresAt: number;
22
+ isExpiring: boolean;
23
+ timeUntilExpiry: number;
24
+ } | null;
25
+ isRefreshing(): boolean;
26
+ clearToken(): void;
27
+ getAccessToken(): Promise<string | undefined>;
28
+ getAccessTokenSilently(): Promise<string | undefined>;
29
+ refreshToken(): Promise<string | undefined>;
30
+ private refreshTokenSilently;
31
+ private _refreshToken;
32
+ reset(): void;
33
+ }
34
+ export declare const tokenStore: TokenStore;
35
+ export {};
@@ -1,9 +1,30 @@
1
- /**
2
- * A hook that manages access tokens with automatic refresh.
3
- */
4
- export declare function useAccessToken(): {
1
+ export interface UseAccessTokenReturn {
2
+ /**
3
+ * Current access token. May be stale when tab is inactive.
4
+ * Use this for display purposes or where eventual consistency is acceptable.
5
+ */
5
6
  accessToken: string | undefined;
7
+ /**
8
+ * Loading state for initial token fetch
9
+ */
6
10
  loading: boolean;
11
+ /**
12
+ * Error from the last token operation
13
+ */
7
14
  error: Error | null;
15
+ /**
16
+ * Manually trigger a token refresh
17
+ */
8
18
  refresh: () => Promise<string | undefined>;
9
- };
19
+ /**
20
+ * Get a guaranteed fresh access token. Automatically refreshes if needed.
21
+ * Use this for API calls where token freshness is critical.
22
+ * @returns Promise resolving to fresh token or undefined if not authenticated
23
+ * @throws Error if refresh fails
24
+ */
25
+ getAccessToken: () => Promise<string | undefined>;
26
+ }
27
+ /**
28
+ * A hook that manages access tokens with automatic refresh.
29
+ */
30
+ export declare function useAccessToken(): UseAccessTokenReturn;
@@ -1,5 +1,5 @@
1
1
  import { WorkOS } from '@workos-inc/node';
2
- export declare const VERSION = "2.4.5";
2
+ export declare const VERSION = "2.5.0";
3
3
  /**
4
4
  * Create a WorkOS instance with the provided API key and options.
5
5
  * If an instance already exists, it returns the existing instance.
@@ -1,7 +1,7 @@
1
1
  import { WorkOS } from '@workos-inc/node';
2
2
  import { WORKOS_API_HOSTNAME, WORKOS_API_KEY, WORKOS_API_HTTPS, WORKOS_API_PORT } from './env-variables.js';
3
3
  import { lazy } from './utils.js';
4
- export const VERSION = '2.4.5';
4
+ export const VERSION = '2.5.0';
5
5
  const options = {
6
6
  apiHostname: WORKOS_API_HOSTNAME,
7
7
  https: WORKOS_API_HTTPS ? WORKOS_API_HTTPS === 'true' : true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos-inc/authkit-nextjs",
3
- "version": "2.4.5",
3
+ "version": "2.5.0",
4
4
  "description": "Authentication and session helpers for using WorkOS & AuthKit with Next.js",
5
5
  "sideEffects": false,
6
6
  "type": "module",
package/src/auth.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  'use server';
2
2
 
3
- import { revalidatePath, revalidateTag } from 'next/cache.js';
4
- import { cookies, headers } from 'next/headers.js';
5
- import { redirect } from 'next/navigation.js';
3
+ import { revalidatePath, revalidateTag } from 'next/cache';
4
+ import { cookies, headers } from 'next/headers';
5
+ import { redirect } from 'next/navigation';
6
6
  import { WORKOS_COOKIE_NAME } from './env-variables.js';
7
7
  import { getCookieOptions } from './cookie.js';
8
8
  import { getAuthorizationUrl } from './get-authorization-url.js';
@@ -0,0 +1,268 @@
1
+ import { getAccessTokenAction, refreshAccessTokenAction } from '../actions.js';
2
+ import { decodeJwt } from '../jwt.js';
3
+
4
+ interface TokenState {
5
+ token: string | undefined;
6
+ loading: boolean;
7
+ error: Error | null;
8
+ }
9
+
10
+ const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
11
+ const MIN_REFRESH_DELAY_SECONDS = 15;
12
+ const MAX_REFRESH_DELAY_SECONDS = 24 * 60 * 60;
13
+ const RETRY_DELAY_SECONDS = 300; // 5 minutes for retry on error
14
+
15
+ class TokenStore {
16
+ private static readonly SERVER_SNAPSHOT: TokenState = { token: undefined, loading: false, error: null };
17
+
18
+ private state: TokenState = {
19
+ token: undefined,
20
+ loading: false,
21
+ error: null,
22
+ };
23
+
24
+ private listeners = new Set<() => void>();
25
+ private refreshPromise: Promise<string | undefined> | null = null;
26
+ private refreshTimeout: ReturnType<typeof setTimeout> | undefined;
27
+
28
+ subscribe = (listener: () => void) => {
29
+ this.listeners.add(listener);
30
+ return () => {
31
+ this.listeners.delete(listener);
32
+ if (this.listeners.size === 0 && this.refreshTimeout) {
33
+ clearTimeout(this.refreshTimeout);
34
+ this.refreshTimeout = undefined;
35
+ }
36
+ };
37
+ };
38
+
39
+ getSnapshot = () => this.state;
40
+
41
+ getServerSnapshot = () => TokenStore.SERVER_SNAPSHOT;
42
+
43
+ private notify() {
44
+ this.listeners.forEach((listener) => listener());
45
+ }
46
+
47
+ private setState(updates: Partial<TokenState>) {
48
+ this.state = { ...this.state, ...updates };
49
+ this.notify();
50
+ }
51
+
52
+ private scheduleRefresh(timeUntilExpiry?: number) {
53
+ if (this.refreshTimeout) {
54
+ clearTimeout(this.refreshTimeout);
55
+ this.refreshTimeout = undefined;
56
+ }
57
+
58
+ const delay =
59
+ typeof timeUntilExpiry === 'undefined' ? RETRY_DELAY_SECONDS * 1000 : this.getRefreshDelay(timeUntilExpiry);
60
+
61
+ this.refreshTimeout = setTimeout(() => {
62
+ /* istanbul ignore next */
63
+ void this.getAccessTokenSilently().catch(() => {});
64
+ }, delay);
65
+ }
66
+
67
+ private getRefreshDelay(timeUntilExpiry: number) {
68
+ if (timeUntilExpiry <= TOKEN_EXPIRY_BUFFER_SECONDS) {
69
+ return 0; // Immediate refresh
70
+ }
71
+
72
+ const idealDelay = (timeUntilExpiry - TOKEN_EXPIRY_BUFFER_SECONDS) * 1000;
73
+
74
+ return Math.min(Math.max(idealDelay, MIN_REFRESH_DELAY_SECONDS * 1000), MAX_REFRESH_DELAY_SECONDS * 1000);
75
+ }
76
+
77
+ parseToken(token: string | undefined) {
78
+ if (!token) return null;
79
+
80
+ try {
81
+ const { payload } = decodeJwt(token);
82
+ const now = Math.floor(Date.now() / 1000);
83
+
84
+ if (typeof payload.exp !== 'number') {
85
+ return null;
86
+ }
87
+
88
+ const timeUntilExpiry = payload.exp - now;
89
+
90
+ // For short-lived tokens (< 5 minutes), use a 30-second buffer
91
+ // This prevents constant refreshing when tokens only last 60 seconds
92
+ let bufferSeconds = TOKEN_EXPIRY_BUFFER_SECONDS;
93
+ const totalTokenLifetime = payload.exp - (payload.iat || now);
94
+
95
+ if (totalTokenLifetime <= 300) {
96
+ // Token lifetime is 5 minutes or less - use 30 second buffer
97
+ bufferSeconds = 30;
98
+ }
99
+
100
+ const isExpiring = payload.exp < now + bufferSeconds;
101
+
102
+ return {
103
+ payload,
104
+ expiresAt: payload.exp,
105
+ isExpiring,
106
+ timeUntilExpiry,
107
+ };
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ isRefreshing(): boolean {
114
+ return this.refreshPromise !== null;
115
+ }
116
+
117
+ clearToken() {
118
+ this.setState({ token: undefined, error: null, loading: false });
119
+ if (this.refreshTimeout) {
120
+ clearTimeout(this.refreshTimeout);
121
+ this.refreshTimeout = undefined;
122
+ }
123
+ }
124
+
125
+ async getAccessToken(): Promise<string | undefined> {
126
+ const tokenData = this.parseToken(this.state.token);
127
+
128
+ // If we have a valid JWT that's not expiring, return it
129
+ if (tokenData && !tokenData.isExpiring) {
130
+ return this.state.token;
131
+ }
132
+
133
+ // If we have an opaque token (can't parse as JWT), return it as-is
134
+ if (this.state.token && !tokenData) {
135
+ return this.state.token;
136
+ }
137
+
138
+ // Otherwise refresh (no token or expiring JWT)
139
+ return this.refreshTokenSilently();
140
+ }
141
+
142
+ async getAccessTokenSilently(): Promise<string | undefined> {
143
+ const tokenData = this.parseToken(this.state.token);
144
+
145
+ // If we have a valid JWT that's not expiring, return it
146
+ if (tokenData && !tokenData.isExpiring) {
147
+ // Valid non-expiring JWT - return cached token without server call
148
+ return this.state.token;
149
+ }
150
+
151
+ // If we have an opaque token (can't parse as JWT), return it as-is
152
+ if (this.state.token && !tokenData) {
153
+ // Opaque token - return cached token without server call
154
+ return this.state.token;
155
+ }
156
+
157
+ // Otherwise refresh (no token or expiring JWT)
158
+ return this.refreshTokenSilently();
159
+ }
160
+
161
+ async refreshToken(): Promise<string | undefined> {
162
+ return this._refreshToken(false);
163
+ }
164
+
165
+ private async refreshTokenSilently(): Promise<string | undefined> {
166
+ return this._refreshToken(true);
167
+ }
168
+
169
+ private async _refreshToken(silent: boolean): Promise<string | undefined> {
170
+ if (this.refreshPromise) {
171
+ return this.refreshPromise;
172
+ }
173
+
174
+ const previousToken = this.state.token;
175
+
176
+ // Only set loading for user-initiated refreshes, not background refreshes
177
+ if (!silent) {
178
+ this.setState({ loading: true, error: null });
179
+ } else {
180
+ // Clear error for silent refreshes but don't set loading
181
+ this.setState({ error: null });
182
+ }
183
+
184
+ this.refreshPromise = (async () => {
185
+ try {
186
+ // For manual refresh, always call refreshAccessTokenAction
187
+ // For silent refresh, try to get existing first, then refresh if needed
188
+ let token: string | undefined;
189
+
190
+ if (!silent) {
191
+ // Manual refresh - always force refresh
192
+ token = await refreshAccessTokenAction();
193
+ } else {
194
+ // Silent refresh - only fetch from server if we don't have a local token
195
+ if (!previousToken) {
196
+ // No local token, need to check server
197
+ token = await getAccessTokenAction();
198
+ const tokenData = this.parseToken(token);
199
+
200
+ // Set the token even if it's expiring, to preserve it in case refresh fails
201
+ if (token && token !== previousToken) {
202
+ this.setState({
203
+ token,
204
+ loading: false,
205
+ error: null,
206
+ });
207
+ }
208
+
209
+ // If the token from server is expiring, refresh it
210
+ if (!token || (tokenData && tokenData.isExpiring)) {
211
+ const refreshedToken = await refreshAccessTokenAction();
212
+ if (refreshedToken) {
213
+ token = refreshedToken;
214
+ }
215
+ }
216
+ } else {
217
+ // We have a local token that needs refreshing (already checked by getAccessTokenSilently)
218
+ token = await refreshAccessTokenAction();
219
+ }
220
+ }
221
+
222
+ // Only update state if token actually changed or if loading was true
223
+ if (token !== previousToken || !silent) {
224
+ this.setState({
225
+ token,
226
+ loading: false,
227
+ error: null,
228
+ });
229
+ }
230
+
231
+ const tokenData = this.parseToken(token);
232
+ if (tokenData) {
233
+ this.scheduleRefresh(tokenData.timeUntilExpiry);
234
+ }
235
+ // If token is opaque (not a JWT), we don't schedule automatic refreshes
236
+
237
+ return token;
238
+ } catch (error) {
239
+ // Don't clear the token immediately - keep the stale one while retrying
240
+ this.setState({
241
+ loading: false,
242
+ error: error instanceof Error ? error : new Error(String(error)),
243
+ });
244
+
245
+ // Schedule a retry after delay
246
+ this.scheduleRefresh();
247
+
248
+ throw error;
249
+ } finally {
250
+ this.refreshPromise = null;
251
+ }
252
+ })();
253
+
254
+ return this.refreshPromise;
255
+ }
256
+
257
+ reset() {
258
+ this.state = { token: undefined, loading: false, error: null };
259
+ this.refreshPromise = null;
260
+ if (this.refreshTimeout) {
261
+ clearTimeout(this.refreshTimeout);
262
+ this.refreshTimeout = undefined;
263
+ }
264
+ this.listeners.clear();
265
+ }
266
+ }
267
+
268
+ export const tokenStore = new TokenStore();
@@ -1,204 +1,122 @@
1
- import { useCallback, useEffect, useReducer, useRef } from 'react';
2
- import { getAccessTokenAction, refreshAccessTokenAction } from '../actions.js';
1
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';
3
2
  import { useAuth } from './authkit-provider.js';
4
- import { decodeJwt } from '../jwt.js';
5
-
6
- const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
7
- const MIN_REFRESH_DELAY_SECONDS = 15; // minimum delay before refreshing token
8
- const MAX_REFRESH_DELAY_SECONDS = 24 * 60 * 60; // 24 hours
9
- const RETRY_DELAY_SECONDS = 300; // 5 minutes
10
-
11
- interface TokenState {
12
- token: string | undefined;
3
+ import { tokenStore } from './tokenStore.js';
4
+
5
+ export interface UseAccessTokenReturn {
6
+ /**
7
+ * Current access token. May be stale when tab is inactive.
8
+ * Use this for display purposes or where eventual consistency is acceptable.
9
+ */
10
+ accessToken: string | undefined;
11
+ /**
12
+ * Loading state for initial token fetch
13
+ */
13
14
  loading: boolean;
15
+ /**
16
+ * Error from the last token operation
17
+ */
14
18
  error: Error | null;
15
- }
16
-
17
- type TokenAction =
18
- | { type: 'FETCH_START' }
19
- | { type: 'FETCH_SUCCESS'; token: string | undefined }
20
- | { type: 'FETCH_ERROR'; error: Error }
21
- | { type: 'RESET' };
22
-
23
- function tokenReducer(state: TokenState, action: TokenAction): TokenState {
24
- switch (action.type) {
25
- case 'FETCH_START':
26
- return { ...state, loading: true, error: null };
27
- case 'FETCH_SUCCESS':
28
- return { ...state, loading: false, token: action.token, error: null };
29
- case 'FETCH_ERROR':
30
- return { ...state, loading: false, error: action.error };
31
- case 'RESET':
32
- return { ...state, token: undefined, loading: false, error: null };
33
- // istanbul ignore next
34
- default:
35
- return state;
36
- }
37
- }
38
-
39
- function getRefreshDelay(timeUntilExpiry: number) {
40
- const idealDelay = (timeUntilExpiry - TOKEN_EXPIRY_BUFFER_SECONDS) * 1000;
41
- return Math.min(Math.max(idealDelay, MIN_REFRESH_DELAY_SECONDS * 1000), MAX_REFRESH_DELAY_SECONDS * 1000);
42
- }
43
-
44
- function parseTokenPayload(token: string | undefined) {
45
- // istanbul ignore next
46
- if (!token) {
47
- return null;
48
- }
49
-
50
- try {
51
- const { payload } = decodeJwt(token);
52
- const now = Math.floor(Date.now() / 1000);
53
-
54
- // istanbul ignore next - if the token does not have an exp claim, we cannot determine expiry
55
- if (typeof payload.exp !== 'number') {
56
- return null;
57
- }
58
-
59
- return {
60
- payload,
61
- expiresAt: payload.exp,
62
- isExpiring: payload.exp < now + TOKEN_EXPIRY_BUFFER_SECONDS,
63
- timeUntilExpiry: payload.exp - now,
64
- };
65
- } catch {
66
- // istanbul ignore next
67
- return null;
68
- }
19
+ /**
20
+ * Manually trigger a token refresh
21
+ */
22
+ refresh: () => Promise<string | undefined>;
23
+ /**
24
+ * Get a guaranteed fresh access token. Automatically refreshes if needed.
25
+ * Use this for API calls where token freshness is critical.
26
+ * @returns Promise resolving to fresh token or undefined if not authenticated
27
+ * @throws Error if refresh fails
28
+ */
29
+ getAccessToken: () => Promise<string | undefined>;
69
30
  }
70
31
 
71
32
  /**
72
33
  * A hook that manages access tokens with automatic refresh.
73
34
  */
74
- export function useAccessToken() {
75
- const { user, sessionId, refreshAuth } = useAuth();
35
+ export function useAccessToken(): UseAccessTokenReturn {
36
+ const { user, sessionId } = useAuth();
76
37
  const userId = user?.id;
77
- const [state, dispatch] = useReducer(tokenReducer, {
78
- token: undefined,
79
- loading: false,
80
- error: null,
81
- });
82
-
83
- const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
84
- const fetchingRef = useRef(false);
85
-
86
- const clearRefreshTimeout = useCallback(() => {
87
- if (refreshTimeoutRef.current) {
88
- clearTimeout(refreshTimeoutRef.current);
89
- refreshTimeoutRef.current = undefined;
90
- }
91
- }, []);
38
+ const userRef = useRef(user);
39
+ userRef.current = user;
40
+ const prevSessionRef = useRef(sessionId);
41
+ const prevUserIdRef = useRef(userId);
92
42
 
93
- // Store the current token in a ref to avoid stale closures
94
- const currentTokenRef = useRef<string | undefined>(state.token);
95
- currentTokenRef.current = state.token;
96
-
97
- // Store updateToken in a ref to break circular dependency
98
- const updateTokenRef = useRef<() => Promise<string | undefined>>();
99
-
100
- // Centralized timer scheduling function
101
- const scheduleNextRefresh = useCallback(
102
- (delay: number) => {
103
- clearRefreshTimeout();
104
- refreshTimeoutRef.current = setTimeout(() => {
105
- if (updateTokenRef.current) {
106
- updateTokenRef.current();
107
- }
108
- }, delay);
109
- },
110
- [clearRefreshTimeout],
111
- );
112
-
113
- const updateToken = useCallback(async () => {
114
- // istanbul ignore next - safety guard against concurrent fetches
115
- if (fetchingRef.current) {
43
+ const tokenState = useSyncExternalStore(tokenStore.subscribe, tokenStore.getSnapshot, tokenStore.getServerSnapshot);
44
+
45
+ useEffect(() => {
46
+ if (!user) {
47
+ tokenStore.clearToken();
116
48
  return;
117
49
  }
118
50
 
119
- fetchingRef.current = true;
51
+ // Only clear token if user or session actually changed (not on initial mount)
52
+ const sessionChanged = prevSessionRef.current !== undefined && prevSessionRef.current !== sessionId;
53
+ const userChanged = prevUserIdRef.current !== undefined && prevUserIdRef.current !== userId;
120
54
 
121
- try {
122
- let token = await getAccessTokenAction();
123
- if (token) {
124
- const tokenData = parseTokenPayload(token);
125
- if (!tokenData || tokenData.isExpiring) {
126
- token = await refreshAccessTokenAction();
127
- }
128
- }
129
-
130
- // Only update state if token has changed
131
- if (token !== currentTokenRef.current) {
132
- dispatch({ type: 'FETCH_SUCCESS', token });
133
- }
134
-
135
- if (token) {
136
- const tokenData = parseTokenPayload(token);
137
- if (tokenData) {
138
- const delay = getRefreshDelay(tokenData.timeUntilExpiry);
139
- scheduleNextRefresh(delay);
140
- }
141
- }
142
-
143
- return token;
144
- } catch (error) {
145
- dispatch({ type: 'FETCH_ERROR', error: error instanceof Error ? error : new Error(String(error)) });
146
- scheduleNextRefresh(RETRY_DELAY_SECONDS * 1000);
147
- } finally {
148
- fetchingRef.current = false;
55
+ if (sessionChanged || userChanged) {
56
+ tokenStore.clearToken();
149
57
  }
150
- }, [scheduleNextRefresh]);
151
58
 
152
- // Assign updateToken to ref for use in scheduleNextRefresh
153
- updateTokenRef.current = updateToken;
59
+ prevSessionRef.current = sessionId;
60
+ prevUserIdRef.current = userId;
154
61
 
155
- const refresh = useCallback(async () => {
156
- if (fetchingRef.current) {
62
+ /* istanbul ignore next */
63
+ tokenStore.getAccessTokenSilently().catch(() => {
64
+ // Error is handled in the store
65
+ });
66
+ }, [userId, sessionId]);
67
+
68
+ useEffect(() => {
69
+ if (!user || typeof document === 'undefined') {
157
70
  return;
158
71
  }
159
72
 
160
- fetchingRef.current = true;
161
- dispatch({ type: 'FETCH_START' });
73
+ /* istanbul ignore next */
74
+ const refreshIfNeeded = () => {
75
+ tokenStore.getAccessTokenSilently().catch(() => {
76
+ // Error is handled in the store
77
+ });
78
+ };
162
79
 
163
- try {
164
- await refreshAuth();
165
- const token = await getAccessTokenAction();
80
+ /* istanbul ignore next */
81
+ const handleWake = (event: Event) => {
82
+ if (event.type !== 'visibilitychange' || document.visibilityState === 'visible') {
83
+ refreshIfNeeded();
84
+ }
85
+ };
166
86
 
167
- dispatch({ type: 'FETCH_SUCCESS', token });
87
+ document.addEventListener('visibilitychange', handleWake);
88
+ window.addEventListener('focus', handleWake);
89
+ window.addEventListener('online', handleWake);
90
+ window.addEventListener('pageshow', handleWake);
168
91
 
169
- if (token) {
170
- const tokenData = parseTokenPayload(token);
171
- if (tokenData) {
172
- const delay = getRefreshDelay(tokenData.timeUntilExpiry);
173
- scheduleNextRefresh(delay);
174
- }
175
- }
92
+ return () => {
93
+ document.removeEventListener('visibilitychange', handleWake);
94
+ window.removeEventListener('focus', handleWake);
95
+ window.removeEventListener('online', handleWake);
96
+ window.removeEventListener('pageshow', handleWake);
97
+ };
98
+ }, [userId, sessionId]);
176
99
 
177
- return token;
178
- } catch (error) {
179
- const typedError = error instanceof Error ? error : new Error(String(error));
180
- dispatch({ type: 'FETCH_ERROR', error: typedError });
181
- scheduleNextRefresh(RETRY_DELAY_SECONDS * 1000);
182
- } finally {
183
- fetchingRef.current = false;
100
+ const getAccessToken = useCallback(async (): Promise<string | undefined> => {
101
+ if (!userRef.current) {
102
+ return undefined;
184
103
  }
185
- }, [refreshAuth, scheduleNextRefresh, updateToken]);
104
+ return tokenStore.getAccessToken();
105
+ }, []);
186
106
 
187
- useEffect(() => {
188
- if (!user) {
189
- dispatch({ type: 'RESET' });
190
- clearRefreshTimeout();
191
- return;
107
+ // Stable refresh function
108
+ const refresh = useCallback(async (): Promise<string | undefined> => {
109
+ if (!userRef.current) {
110
+ return undefined;
192
111
  }
193
- updateToken();
194
-
195
- return clearRefreshTimeout;
196
- }, [userId, sessionId, clearRefreshTimeout]);
112
+ return tokenStore.refreshToken();
113
+ }, []);
197
114
 
198
115
  return {
199
- accessToken: state.token,
200
- loading: state.loading,
201
- error: state.error,
116
+ accessToken: tokenState.token,
117
+ loading: tokenState.loading,
118
+ error: tokenState.error,
202
119
  refresh,
120
+ getAccessToken,
203
121
  };
204
122
  }
package/src/workos.ts CHANGED
@@ -2,7 +2,7 @@ import { WorkOS } from '@workos-inc/node';
2
2
  import { WORKOS_API_HOSTNAME, WORKOS_API_KEY, WORKOS_API_HTTPS, WORKOS_API_PORT } from './env-variables.js';
3
3
  import { lazy } from './utils.js';
4
4
 
5
- export const VERSION = '2.4.5';
5
+ export const VERSION = '2.5.0';
6
6
 
7
7
  const options = {
8
8
  apiHostname: WORKOS_API_HOSTNAME,