@workos-inc/authkit-nextjs 2.4.6 → 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/components/tokenStore.js +220 -0
- package/dist/esm/components/tokenStore.js.map +1 -0
- package/dist/esm/components/useAccessToken.js +62 -147
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/types/components/tokenStore.d.ts +35 -0
- package/dist/esm/types/components/useAccessToken.d.ts +26 -5
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/workos.js +1 -1
- package/package.json +1 -1
- package/src/components/tokenStore.ts +268 -0
- package/src/components/useAccessToken.ts +90 -172
- package/src/workos.ts +1 -1
|
@@ -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,
|
|
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 {
|
|
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
|
|
8
|
+
const { user, sessionId } = useAuth();
|
|
57
9
|
const userId = user === null || user === void 0 ? void 0 : user.id;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
}, [userId, sessionId, clearRefreshTimeout]);
|
|
71
|
+
return tokenStore.refreshToken();
|
|
72
|
+
}, []);
|
|
159
73
|
return {
|
|
160
|
-
accessToken:
|
|
161
|
-
loading:
|
|
162
|
-
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":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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;
|
package/dist/esm/workos.js
CHANGED
|
@@ -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
|
+
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
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
153
|
-
|
|
59
|
+
prevSessionRef.current = sessionId;
|
|
60
|
+
prevUserIdRef.current = userId;
|
|
154
61
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
73
|
+
/* istanbul ignore next */
|
|
74
|
+
const refreshIfNeeded = () => {
|
|
75
|
+
tokenStore.getAccessTokenSilently().catch(() => {
|
|
76
|
+
// Error is handled in the store
|
|
77
|
+
});
|
|
78
|
+
};
|
|
162
79
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
80
|
+
/* istanbul ignore next */
|
|
81
|
+
const handleWake = (event: Event) => {
|
|
82
|
+
if (event.type !== 'visibilitychange' || document.visibilityState === 'visible') {
|
|
83
|
+
refreshIfNeeded();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
166
86
|
|
|
167
|
-
|
|
87
|
+
document.addEventListener('visibilitychange', handleWake);
|
|
88
|
+
window.addEventListener('focus', handleWake);
|
|
89
|
+
window.addEventListener('online', handleWake);
|
|
90
|
+
window.addEventListener('pageshow', handleWake);
|
|
168
91
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
104
|
+
return tokenStore.getAccessToken();
|
|
105
|
+
}, []);
|
|
186
106
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return;
|
|
107
|
+
// Stable refresh function
|
|
108
|
+
const refresh = useCallback(async (): Promise<string | undefined> => {
|
|
109
|
+
if (!userRef.current) {
|
|
110
|
+
return undefined;
|
|
192
111
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return clearRefreshTimeout;
|
|
196
|
-
}, [userId, sessionId, clearRefreshTimeout]);
|
|
112
|
+
return tokenStore.refreshToken();
|
|
113
|
+
}, []);
|
|
197
114
|
|
|
198
115
|
return {
|
|
199
|
-
accessToken:
|
|
200
|
-
loading:
|
|
201
|
-
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.
|
|
5
|
+
export const VERSION = '2.5.0';
|
|
6
6
|
|
|
7
7
|
const options = {
|
|
8
8
|
apiHostname: WORKOS_API_HOSTNAME,
|