@workos-inc/authkit-nextjs 2.4.6 → 2.6.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 +18 -5
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/components/tokenStore.js +220 -0
- package/dist/esm/components/tokenStore.js.map +1 -0
- package/dist/esm/components/useAccessToken.js +85 -145
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/get-authorization-url.js +2 -1
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/session.js +1 -1
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/types/auth.d.ts +5 -3
- package/dist/esm/types/components/tokenStore.d.ts +35 -0
- package/dist/esm/types/components/useAccessToken.d.ts +26 -5
- package/dist/esm/types/interfaces.d.ts +1 -0
- package/dist/esm/types/session.d.ts +1 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/workos.js +1 -1
- package/package.json +2 -2
- package/src/auth.ts +19 -6
- package/src/components/tokenStore.ts +268 -0
- package/src/components/useAccessToken.ts +114 -167
- package/src/get-authorization-url.ts +2 -0
- package/src/interfaces.ts +1 -0
- package/src/session.ts +1 -1
- package/src/workos.ts +1 -1
|
@@ -1,204 +1,151 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
2
|
-
import { getAccessTokenAction, refreshAccessTokenAction } from '../actions.js';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
2
|
import { useAuth } from './authkit-provider.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
35
|
+
export function useAccessToken(): UseAccessTokenReturn {
|
|
36
|
+
const { user, sessionId } = useAuth();
|
|
76
37
|
const userId = user?.id;
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
38
|
+
const userRef = useRef(user);
|
|
39
|
+
userRef.current = user;
|
|
40
|
+
const prevSessionRef = useRef(sessionId);
|
|
41
|
+
const prevUserIdRef = useRef(userId);
|
|
42
|
+
|
|
43
|
+
const tokenState = useSyncExternalStore(tokenStore.subscribe, tokenStore.getSnapshot, tokenStore.getServerSnapshot);
|
|
44
|
+
|
|
45
|
+
// Track if we're waiting for the initial token fetch for the current user
|
|
46
|
+
// Initialize synchronously to prevent first-paint flash
|
|
47
|
+
const [isInitialTokenLoading, setIsInitialTokenLoading] = useState(() => {
|
|
48
|
+
// Only show loading if we have a user but no token yet
|
|
49
|
+
return Boolean(user && !tokenState.token && !tokenState.error);
|
|
81
50
|
});
|
|
82
51
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (refreshTimeoutRef.current) {
|
|
88
|
-
clearTimeout(refreshTimeoutRef.current);
|
|
89
|
-
refreshTimeoutRef.current = undefined;
|
|
90
|
-
}
|
|
91
|
-
}, []);
|
|
92
|
-
|
|
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) {
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!user) {
|
|
54
|
+
tokenStore.clearToken();
|
|
55
|
+
setIsInitialTokenLoading(false);
|
|
116
56
|
return;
|
|
117
57
|
}
|
|
118
58
|
|
|
119
|
-
|
|
59
|
+
// Only clear token if user or session actually changed (not on initial mount)
|
|
60
|
+
const sessionChanged = prevSessionRef.current !== undefined && prevSessionRef.current !== sessionId;
|
|
61
|
+
const userChanged = prevUserIdRef.current !== undefined && prevUserIdRef.current !== userId;
|
|
120
62
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const tokenData = parseTokenPayload(token);
|
|
125
|
-
if (!tokenData || tokenData.isExpiring) {
|
|
126
|
-
token = await refreshAccessTokenAction();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
63
|
+
if (sessionChanged || userChanged) {
|
|
64
|
+
tokenStore.clearToken();
|
|
65
|
+
}
|
|
129
66
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
dispatch({ type: 'FETCH_SUCCESS', token });
|
|
133
|
-
}
|
|
67
|
+
prevSessionRef.current = sessionId;
|
|
68
|
+
prevUserIdRef.current = userId;
|
|
134
69
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
scheduleNextRefresh(delay);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
70
|
+
// Check if getAccessTokenSilently will actually fetch (not just return cached)
|
|
71
|
+
const currentToken = tokenStore.getSnapshot().token;
|
|
72
|
+
const tokenData = currentToken ? tokenStore.parseToken(currentToken) : null;
|
|
73
|
+
const willActuallyFetch = !currentToken || (tokenData && tokenData.isExpiring);
|
|
142
74
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
scheduleNextRefresh(RETRY_DELAY_SECONDS * 1000);
|
|
147
|
-
} finally {
|
|
148
|
-
fetchingRef.current = false;
|
|
75
|
+
// Only show loading if we're actually going to fetch
|
|
76
|
+
if (willActuallyFetch) {
|
|
77
|
+
setIsInitialTokenLoading(true);
|
|
149
78
|
}
|
|
150
|
-
}, [scheduleNextRefresh]);
|
|
151
79
|
|
|
152
|
-
|
|
153
|
-
|
|
80
|
+
/* istanbul ignore next */
|
|
81
|
+
tokenStore
|
|
82
|
+
.getAccessTokenSilently()
|
|
83
|
+
.catch(() => {
|
|
84
|
+
// Error is handled in the store
|
|
85
|
+
})
|
|
86
|
+
.finally(() => {
|
|
87
|
+
// Only clear loading if we were actually loading
|
|
88
|
+
if (willActuallyFetch) {
|
|
89
|
+
setIsInitialTokenLoading(false);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}, [userId, sessionId]);
|
|
154
93
|
|
|
155
|
-
|
|
156
|
-
if (
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!user || typeof document === 'undefined') {
|
|
157
96
|
return;
|
|
158
97
|
}
|
|
159
98
|
|
|
160
|
-
|
|
161
|
-
|
|
99
|
+
/* istanbul ignore next */
|
|
100
|
+
const refreshIfNeeded = () => {
|
|
101
|
+
tokenStore.getAccessTokenSilently().catch(() => {
|
|
102
|
+
// Error is handled in the store
|
|
103
|
+
});
|
|
104
|
+
};
|
|
162
105
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
106
|
+
/* istanbul ignore next */
|
|
107
|
+
const handleWake = (event: Event) => {
|
|
108
|
+
if (event.type !== 'visibilitychange' || document.visibilityState === 'visible') {
|
|
109
|
+
refreshIfNeeded();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
166
112
|
|
|
167
|
-
|
|
113
|
+
document.addEventListener('visibilitychange', handleWake);
|
|
114
|
+
window.addEventListener('focus', handleWake);
|
|
115
|
+
window.addEventListener('online', handleWake);
|
|
116
|
+
window.addEventListener('pageshow', handleWake);
|
|
168
117
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
118
|
+
return () => {
|
|
119
|
+
document.removeEventListener('visibilitychange', handleWake);
|
|
120
|
+
window.removeEventListener('focus', handleWake);
|
|
121
|
+
window.removeEventListener('online', handleWake);
|
|
122
|
+
window.removeEventListener('pageshow', handleWake);
|
|
123
|
+
};
|
|
124
|
+
}, [userId, sessionId]);
|
|
176
125
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
dispatch({ type: 'FETCH_ERROR', error: typedError });
|
|
181
|
-
scheduleNextRefresh(RETRY_DELAY_SECONDS * 1000);
|
|
182
|
-
} finally {
|
|
183
|
-
fetchingRef.current = false;
|
|
126
|
+
const getAccessToken = useCallback(async (): Promise<string | undefined> => {
|
|
127
|
+
if (!userRef.current) {
|
|
128
|
+
return undefined;
|
|
184
129
|
}
|
|
185
|
-
|
|
130
|
+
return tokenStore.getAccessToken();
|
|
131
|
+
}, []);
|
|
186
132
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return;
|
|
133
|
+
// Stable refresh function
|
|
134
|
+
const refresh = useCallback(async (): Promise<string | undefined> => {
|
|
135
|
+
if (!userRef.current) {
|
|
136
|
+
return undefined;
|
|
192
137
|
}
|
|
193
|
-
|
|
138
|
+
return tokenStore.refreshToken();
|
|
139
|
+
}, []);
|
|
194
140
|
|
|
195
|
-
|
|
196
|
-
|
|
141
|
+
// Combine loading states: initial token fetch OR token store is loading
|
|
142
|
+
const isLoading = isInitialTokenLoading || tokenState.loading;
|
|
197
143
|
|
|
198
144
|
return {
|
|
199
|
-
accessToken:
|
|
200
|
-
loading:
|
|
201
|
-
error:
|
|
145
|
+
accessToken: tokenState.token,
|
|
146
|
+
loading: isLoading,
|
|
147
|
+
error: tokenState.error,
|
|
202
148
|
refresh,
|
|
149
|
+
getAccessToken,
|
|
203
150
|
};
|
|
204
151
|
}
|
|
@@ -11,6 +11,7 @@ async function getAuthorizationUrl(options: GetAuthURLOptions = {}) {
|
|
|
11
11
|
organizationId,
|
|
12
12
|
redirectUri = headersList.get('x-redirect-uri'),
|
|
13
13
|
loginHint,
|
|
14
|
+
prompt,
|
|
14
15
|
} = options;
|
|
15
16
|
|
|
16
17
|
return getWorkOS().userManagement.getAuthorizationUrl({
|
|
@@ -21,6 +22,7 @@ async function getAuthorizationUrl(options: GetAuthURLOptions = {}) {
|
|
|
21
22
|
screenHint,
|
|
22
23
|
organizationId,
|
|
23
24
|
loginHint,
|
|
25
|
+
prompt,
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
|
package/src/interfaces.ts
CHANGED
package/src/session.ts
CHANGED
|
@@ -415,7 +415,7 @@ async function verifyAccessToken(accessToken: string) {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
async function getSessionFromCookie(request?: NextRequest) {
|
|
418
|
+
export async function getSessionFromCookie(request?: NextRequest) {
|
|
419
419
|
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
420
420
|
let cookie;
|
|
421
421
|
|
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.
|
|
5
|
+
export const VERSION = '2.6.0';
|
|
6
6
|
|
|
7
7
|
const options = {
|
|
8
8
|
apiHostname: WORKOS_API_HOSTNAME,
|