@imtbl/auth-next-client 2.12.7-alpha.1 → 2.12.7-alpha.11
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/README.md +197 -20
- package/dist/node/index.cjs +160 -9
- package/dist/node/index.js +158 -10
- package/dist/types/constants.d.ts +8 -25
- package/dist/types/defaultConfig.d.ts +8 -0
- package/dist/types/hooks.d.ts +104 -23
- package/dist/types/idTokenStorage.d.ts +26 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +7 -5
- package/src/callback.tsx +7 -0
- package/src/constants.ts +8 -31
- package/src/defaultConfig.ts +19 -0
- package/src/hooks.test.tsx +321 -0
- package/src/hooks.tsx +304 -40
- package/src/idTokenStorage.ts +56 -0
- package/src/index.ts +12 -0
package/dist/node/index.js
CHANGED
|
@@ -7,9 +7,42 @@ import { signIn } from "next-auth/react";
|
|
|
7
7
|
import { handleLoginCallback as handleAuthCallback } from "@imtbl/auth";
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
|
+
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
11
|
+
var DEFAULT_AUDIENCE = "platform_api";
|
|
12
|
+
var DEFAULT_SCOPE = "openid profile email offline_access transact";
|
|
10
13
|
var IMMUTABLE_PROVIDER_ID = "immutable";
|
|
11
|
-
var
|
|
12
|
-
var
|
|
14
|
+
var DEFAULT_SANDBOX_CLIENT_ID = "mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo";
|
|
15
|
+
var DEFAULT_REDIRECT_URI_PATH = "/callback";
|
|
16
|
+
var DEFAULT_LOGOUT_REDIRECT_URI_PATH = "/";
|
|
17
|
+
var TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
18
|
+
|
|
19
|
+
// src/idTokenStorage.ts
|
|
20
|
+
var ID_TOKEN_STORAGE_KEY = "imtbl_id_token";
|
|
21
|
+
function storeIdToken(idToken) {
|
|
22
|
+
try {
|
|
23
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
24
|
+
window.localStorage.setItem(ID_TOKEN_STORAGE_KEY, idToken);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getStoredIdToken() {
|
|
30
|
+
try {
|
|
31
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
32
|
+
return window.localStorage.getItem(ID_TOKEN_STORAGE_KEY) ?? void 0;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
function clearStoredIdToken() {
|
|
39
|
+
try {
|
|
40
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
41
|
+
window.localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
13
46
|
|
|
14
47
|
// src/callback.tsx
|
|
15
48
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -60,6 +93,9 @@ function CallbackPage({
|
|
|
60
93
|
window.close();
|
|
61
94
|
} else {
|
|
62
95
|
const tokenData = mapTokensToSignInData(tokens);
|
|
96
|
+
if (tokens.idToken) {
|
|
97
|
+
storeIdToken(tokens.idToken);
|
|
98
|
+
}
|
|
63
99
|
const result = await signIn(IMMUTABLE_PROVIDER_ID, {
|
|
64
100
|
tokens: JSON.stringify(tokenData),
|
|
65
101
|
redirect: false
|
|
@@ -149,7 +185,12 @@ function CallbackPage({
|
|
|
149
185
|
}
|
|
150
186
|
|
|
151
187
|
// src/hooks.tsx
|
|
152
|
-
import {
|
|
188
|
+
import {
|
|
189
|
+
useCallback,
|
|
190
|
+
useEffect as useEffect2,
|
|
191
|
+
useRef as useRef2,
|
|
192
|
+
useState as useState2
|
|
193
|
+
} from "react";
|
|
153
194
|
import { useSession, signIn as signIn2, signOut } from "next-auth/react";
|
|
154
195
|
import {
|
|
155
196
|
loginWithPopup as rawLoginWithPopup,
|
|
@@ -157,6 +198,51 @@ import {
|
|
|
157
198
|
loginWithRedirect as rawLoginWithRedirect,
|
|
158
199
|
logoutWithRedirect as rawLogoutWithRedirect
|
|
159
200
|
} from "@imtbl/auth";
|
|
201
|
+
|
|
202
|
+
// src/defaultConfig.ts
|
|
203
|
+
function deriveDefaultRedirectUri() {
|
|
204
|
+
if (typeof window === "undefined") {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"[auth-next-client] deriveDefaultRedirectUri requires window. Login hooks run in the browser when the user triggers login."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return `${window.location.origin}${DEFAULT_REDIRECT_URI_PATH}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/hooks.tsx
|
|
213
|
+
var pendingRefresh = null;
|
|
214
|
+
function deduplicatedUpdate(update) {
|
|
215
|
+
if (!pendingRefresh) {
|
|
216
|
+
pendingRefresh = update().finally(() => {
|
|
217
|
+
pendingRefresh = null;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return pendingRefresh;
|
|
221
|
+
}
|
|
222
|
+
function getSandboxLoginConfig() {
|
|
223
|
+
const redirectUri = deriveDefaultRedirectUri();
|
|
224
|
+
return {
|
|
225
|
+
clientId: DEFAULT_SANDBOX_CLIENT_ID,
|
|
226
|
+
redirectUri,
|
|
227
|
+
popupRedirectUri: redirectUri,
|
|
228
|
+
scope: DEFAULT_SCOPE,
|
|
229
|
+
audience: DEFAULT_AUDIENCE,
|
|
230
|
+
authenticationDomain: DEFAULT_AUTH_DOMAIN
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function getSandboxLogoutConfig() {
|
|
234
|
+
if (typeof window === "undefined") {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"[auth-next-client] getSandboxLogoutConfig requires window. Logout runs in the browser when the user triggers it."
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const logoutRedirectUri = window.location.origin + DEFAULT_LOGOUT_REDIRECT_URI_PATH;
|
|
240
|
+
return {
|
|
241
|
+
clientId: DEFAULT_SANDBOX_CLIENT_ID,
|
|
242
|
+
logoutRedirectUri,
|
|
243
|
+
authenticationDomain: DEFAULT_AUTH_DOMAIN
|
|
244
|
+
};
|
|
245
|
+
}
|
|
160
246
|
function useImmutableSession() {
|
|
161
247
|
const { data: sessionData, status, update } = useSession();
|
|
162
248
|
const [isRefreshing, setIsRefreshing] = useState2(false);
|
|
@@ -173,6 +259,18 @@ function useImmutableSession() {
|
|
|
173
259
|
updateRef.current = update;
|
|
174
260
|
const setIsRefreshingRef = useRef2(setIsRefreshing);
|
|
175
261
|
setIsRefreshingRef.current = setIsRefreshing;
|
|
262
|
+
useEffect2(() => {
|
|
263
|
+
if (!session?.accessTokenExpires) return;
|
|
264
|
+
const timeUntilExpiry = session.accessTokenExpires - Date.now() - TOKEN_EXPIRY_BUFFER_MS;
|
|
265
|
+
if (timeUntilExpiry <= 0) {
|
|
266
|
+
deduplicatedUpdate(() => updateRef.current());
|
|
267
|
+
}
|
|
268
|
+
}, [session?.accessTokenExpires]);
|
|
269
|
+
useEffect2(() => {
|
|
270
|
+
if (session?.idToken) {
|
|
271
|
+
storeIdToken(session.idToken);
|
|
272
|
+
}
|
|
273
|
+
}, [session?.idToken]);
|
|
176
274
|
const getUser = useCallback(async (forceRefresh) => {
|
|
177
275
|
let currentSession;
|
|
178
276
|
if (forceRefresh) {
|
|
@@ -182,6 +280,9 @@ function useImmutableSession() {
|
|
|
182
280
|
currentSession = updatedSession;
|
|
183
281
|
if (currentSession) {
|
|
184
282
|
sessionRef.current = currentSession;
|
|
283
|
+
if (currentSession.idToken) {
|
|
284
|
+
storeIdToken(currentSession.idToken);
|
|
285
|
+
}
|
|
185
286
|
}
|
|
186
287
|
} catch (error) {
|
|
187
288
|
console.error("[auth-next-client] Force refresh failed:", error);
|
|
@@ -189,6 +290,17 @@ function useImmutableSession() {
|
|
|
189
290
|
} finally {
|
|
190
291
|
setIsRefreshingRef.current(false);
|
|
191
292
|
}
|
|
293
|
+
} else if (pendingRefresh) {
|
|
294
|
+
const refreshed = await pendingRefresh;
|
|
295
|
+
if (refreshed) {
|
|
296
|
+
currentSession = refreshed;
|
|
297
|
+
sessionRef.current = currentSession;
|
|
298
|
+
if (currentSession.idToken) {
|
|
299
|
+
storeIdToken(currentSession.idToken);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
currentSession = sessionRef.current;
|
|
303
|
+
}
|
|
192
304
|
} else {
|
|
193
305
|
currentSession = sessionRef.current;
|
|
194
306
|
}
|
|
@@ -202,7 +314,9 @@ function useImmutableSession() {
|
|
|
202
314
|
return {
|
|
203
315
|
accessToken: currentSession.accessToken,
|
|
204
316
|
refreshToken: currentSession.refreshToken,
|
|
205
|
-
idToken
|
|
317
|
+
// Prefer session idToken (fresh after sign-in or refresh, before useEffect
|
|
318
|
+
// stores it), fall back to localStorage for normal reads (cookie has no idToken).
|
|
319
|
+
idToken: currentSession.idToken || getStoredIdToken(),
|
|
206
320
|
profile: {
|
|
207
321
|
sub: currentSession.user?.sub ?? "",
|
|
208
322
|
email: currentSession.user?.email ?? void 0,
|
|
@@ -211,19 +325,40 @@ function useImmutableSession() {
|
|
|
211
325
|
zkEvm: currentSession.zkEvm
|
|
212
326
|
};
|
|
213
327
|
}, []);
|
|
328
|
+
const getAccessToken = useCallback(async () => {
|
|
329
|
+
const currentSession = sessionRef.current;
|
|
330
|
+
if (currentSession?.accessToken && currentSession.accessTokenExpires && Date.now() < currentSession.accessTokenExpires - TOKEN_EXPIRY_BUFFER_MS && !currentSession.error) {
|
|
331
|
+
return currentSession.accessToken;
|
|
332
|
+
}
|
|
333
|
+
const refreshed = await deduplicatedUpdate(
|
|
334
|
+
() => updateRef.current()
|
|
335
|
+
);
|
|
336
|
+
if (!refreshed?.accessToken || refreshed.error) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`[auth-next-client] Failed to get access token: ${refreshed?.error || "no session"}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
sessionRef.current = refreshed;
|
|
342
|
+
return refreshed.accessToken;
|
|
343
|
+
}, []);
|
|
344
|
+
const publicSession = session;
|
|
214
345
|
return {
|
|
215
|
-
session,
|
|
346
|
+
session: publicSession,
|
|
216
347
|
status,
|
|
217
348
|
isLoading,
|
|
218
349
|
isAuthenticated,
|
|
219
350
|
isRefreshing,
|
|
220
|
-
getUser
|
|
351
|
+
getUser,
|
|
352
|
+
getAccessToken
|
|
221
353
|
};
|
|
222
354
|
}
|
|
223
355
|
function useLogin() {
|
|
224
356
|
const [isLoggingIn, setIsLoggingIn] = useState2(false);
|
|
225
357
|
const [error, setError] = useState2(null);
|
|
226
358
|
const signInWithTokens = useCallback(async (tokens) => {
|
|
359
|
+
if (tokens.idToken) {
|
|
360
|
+
storeIdToken(tokens.idToken);
|
|
361
|
+
}
|
|
227
362
|
const result = await signIn2(IMMUTABLE_PROVIDER_ID, {
|
|
228
363
|
tokens: JSON.stringify(tokens),
|
|
229
364
|
redirect: false
|
|
@@ -239,7 +374,8 @@ function useLogin() {
|
|
|
239
374
|
setIsLoggingIn(true);
|
|
240
375
|
setError(null);
|
|
241
376
|
try {
|
|
242
|
-
const
|
|
377
|
+
const fullConfig = config ?? getSandboxLoginConfig();
|
|
378
|
+
const tokens = await rawLoginWithPopup(fullConfig, options);
|
|
243
379
|
await signInWithTokens(tokens);
|
|
244
380
|
} catch (err) {
|
|
245
381
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
@@ -253,7 +389,8 @@ function useLogin() {
|
|
|
253
389
|
setIsLoggingIn(true);
|
|
254
390
|
setError(null);
|
|
255
391
|
try {
|
|
256
|
-
const
|
|
392
|
+
const fullConfig = config ?? getSandboxLoginConfig();
|
|
393
|
+
const tokens = await rawLoginWithEmbedded(fullConfig);
|
|
257
394
|
await signInWithTokens(tokens);
|
|
258
395
|
} catch (err) {
|
|
259
396
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
@@ -267,7 +404,8 @@ function useLogin() {
|
|
|
267
404
|
setIsLoggingIn(true);
|
|
268
405
|
setError(null);
|
|
269
406
|
try {
|
|
270
|
-
|
|
407
|
+
const fullConfig = config ?? getSandboxLoginConfig();
|
|
408
|
+
await rawLoginWithRedirect(fullConfig, options);
|
|
271
409
|
} catch (err) {
|
|
272
410
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
273
411
|
setError(errorMessage);
|
|
@@ -290,8 +428,10 @@ function useLogout() {
|
|
|
290
428
|
setIsLoggingOut(true);
|
|
291
429
|
setError(null);
|
|
292
430
|
try {
|
|
431
|
+
clearStoredIdToken();
|
|
293
432
|
await signOut({ redirect: false });
|
|
294
|
-
|
|
433
|
+
const fullConfig = config ?? getSandboxLogoutConfig();
|
|
434
|
+
rawLogoutWithRedirect(fullConfig);
|
|
295
435
|
} catch (err) {
|
|
296
436
|
const errorMessage = err instanceof Error ? err.message : "Logout failed";
|
|
297
437
|
setError(errorMessage);
|
|
@@ -310,7 +450,15 @@ function useLogout() {
|
|
|
310
450
|
import { MarketingConsentStatus } from "@imtbl/auth";
|
|
311
451
|
export {
|
|
312
452
|
CallbackPage,
|
|
453
|
+
DEFAULT_AUDIENCE,
|
|
454
|
+
DEFAULT_AUTH_DOMAIN,
|
|
455
|
+
DEFAULT_LOGOUT_REDIRECT_URI_PATH,
|
|
456
|
+
DEFAULT_REDIRECT_URI_PATH,
|
|
457
|
+
DEFAULT_SANDBOX_CLIENT_ID,
|
|
458
|
+
DEFAULT_SCOPE,
|
|
459
|
+
IMMUTABLE_PROVIDER_ID,
|
|
313
460
|
MarketingConsentStatus,
|
|
461
|
+
deriveDefaultRedirectUri,
|
|
314
462
|
useImmutableSession,
|
|
315
463
|
useLogin,
|
|
316
464
|
useLogout
|
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Default Immutable authentication domain
|
|
2
|
+
* Client-side constants for @imtbl/auth-next-client.
|
|
3
|
+
* Defined locally to avoid importing from auth-next-server (which uses next/server).
|
|
4
|
+
* Values must stay in sync with auth-next-server constants.
|
|
6
5
|
*/
|
|
7
6
|
export declare const DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
8
|
-
/**
|
|
9
|
-
* Default OAuth audience
|
|
10
|
-
*/
|
|
11
7
|
export declare const DEFAULT_AUDIENCE = "platform_api";
|
|
12
|
-
/**
|
|
13
|
-
* Default OAuth scopes
|
|
14
|
-
*/
|
|
15
8
|
export declare const DEFAULT_SCOPE = "openid profile email offline_access transact";
|
|
16
|
-
/**
|
|
17
|
-
* NextAuth credentials provider ID for Immutable
|
|
18
|
-
*/
|
|
19
9
|
export declare const IMMUTABLE_PROVIDER_ID = "immutable";
|
|
20
|
-
/**
|
|
21
|
-
* Default NextAuth API base path
|
|
22
|
-
*/
|
|
23
10
|
export declare const DEFAULT_NEXTAUTH_BASE_PATH = "/api/auth";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export declare const
|
|
29
|
-
/**
|
|
30
|
-
* Default token expiry in milliseconds
|
|
31
|
-
*/
|
|
32
|
-
export declare const DEFAULT_TOKEN_EXPIRY_MS: number;
|
|
11
|
+
export declare const DEFAULT_SANDBOX_CLIENT_ID = "mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo";
|
|
12
|
+
export declare const DEFAULT_REDIRECT_URI_PATH = "/callback";
|
|
13
|
+
export declare const DEFAULT_LOGOUT_REDIRECT_URI_PATH = "/";
|
|
14
|
+
export declare const DEFAULT_TOKEN_EXPIRY_MS = 900000;
|
|
15
|
+
export declare const TOKEN_EXPIRY_BUFFER_MS = 60000;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox default redirect URI for zero-config mode.
|
|
3
|
+
* Defined locally to avoid importing from auth-next-server (which uses next/server).
|
|
4
|
+
* OAuth requires an absolute URL; this runs in the browser when login is invoked.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function deriveDefaultRedirectUri(): string;
|
package/dist/types/hooks.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { Session } from 'next-auth';
|
|
|
2
2
|
import type { User, LoginConfig, StandaloneLoginOptions, LogoutConfig } from '@imtbl/auth';
|
|
3
3
|
import type { ZkEvmInfo } from './types';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Internal session type with full token data (not exported).
|
|
6
|
+
* Used internally by the hook for token validation, refresh logic, and getUser/getAccessToken.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
+
interface ImmutableSessionInternal extends Session {
|
|
8
9
|
accessToken: string;
|
|
9
10
|
refreshToken?: string;
|
|
10
11
|
idToken?: string;
|
|
@@ -12,6 +13,14 @@ export interface ImmutableSession extends Session {
|
|
|
12
13
|
zkEvm?: ZkEvmInfo;
|
|
13
14
|
error?: string;
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Public session type exposed to consumers.
|
|
18
|
+
*
|
|
19
|
+
* Does **not** include `accessToken` -- consumers must use the `getAccessToken()`
|
|
20
|
+
* function returned by `useImmutableSession()` to obtain a guaranteed-fresh token.
|
|
21
|
+
* This prevents accidental use of stale/expired tokens.
|
|
22
|
+
*/
|
|
23
|
+
export type ImmutableSession = Omit<ImmutableSessionInternal, 'accessToken'>;
|
|
15
24
|
/**
|
|
16
25
|
* Return type for useImmutableSession hook
|
|
17
26
|
*/
|
|
@@ -35,6 +44,13 @@ export interface UseImmutableSessionReturn {
|
|
|
35
44
|
* The refreshed session will include updated zkEvm data if available.
|
|
36
45
|
*/
|
|
37
46
|
getUser: (forceRefresh?: boolean) => Promise<User | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Get a guaranteed-fresh access token.
|
|
49
|
+
* Returns immediately if the current token is valid.
|
|
50
|
+
* If expired, triggers a refresh and blocks (awaits) until the fresh token is available.
|
|
51
|
+
* Throws if the user is not authenticated or if refresh fails.
|
|
52
|
+
*/
|
|
53
|
+
getAccessToken: () => Promise<string>;
|
|
38
54
|
}
|
|
39
55
|
/**
|
|
40
56
|
* Hook to access Immutable session with a getUser function for wallet integration.
|
|
@@ -70,53 +86,87 @@ export interface UseImmutableSessionReturn {
|
|
|
70
86
|
export declare function useImmutableSession(): UseImmutableSessionReturn;
|
|
71
87
|
/**
|
|
72
88
|
* Return type for useLogin hook
|
|
89
|
+
*
|
|
90
|
+
* Config is optional - when omitted, defaults are auto-derived (clientId, redirectUri, etc.).
|
|
91
|
+
* When provided, must be a complete LoginConfig.
|
|
73
92
|
*/
|
|
74
93
|
export interface UseLoginReturn {
|
|
75
94
|
/** Start login with popup flow */
|
|
76
|
-
loginWithPopup: (config
|
|
95
|
+
loginWithPopup: (config?: LoginConfig, options?: StandaloneLoginOptions) => Promise<void>;
|
|
77
96
|
/** Start login with embedded modal flow */
|
|
78
|
-
loginWithEmbedded: (config
|
|
97
|
+
loginWithEmbedded: (config?: LoginConfig) => Promise<void>;
|
|
79
98
|
/** Start login with redirect flow (navigates away from page) */
|
|
80
|
-
loginWithRedirect: (config
|
|
99
|
+
loginWithRedirect: (config?: LoginConfig, options?: StandaloneLoginOptions) => Promise<void>;
|
|
81
100
|
/** Whether login is currently in progress */
|
|
82
101
|
isLoggingIn: boolean;
|
|
83
102
|
/** Error message from the last login attempt, or null if none */
|
|
84
103
|
error: string | null;
|
|
85
104
|
}
|
|
86
105
|
/**
|
|
87
|
-
* Hook to handle Immutable authentication login flows.
|
|
106
|
+
* Hook to handle Immutable authentication login flows with automatic defaults.
|
|
88
107
|
*
|
|
89
108
|
* Provides login functions that:
|
|
90
109
|
* 1. Handle OAuth authentication via popup, embedded modal, or redirect
|
|
91
110
|
* 2. Automatically sign in to NextAuth after successful authentication
|
|
92
111
|
* 3. Track loading and error states
|
|
112
|
+
* 4. Auto-detect clientId and redirectUri if not provided (uses defaults)
|
|
93
113
|
*
|
|
94
|
-
* Config
|
|
95
|
-
*
|
|
114
|
+
* Config can be passed at call time or omitted to use sensible defaults:
|
|
115
|
+
* - `clientId`: Auto-detected based on environment (sandbox vs production)
|
|
116
|
+
* - `redirectUri`: Auto-derived from `window.location.origin + '/callback'`
|
|
117
|
+
* - `popupRedirectUri`: Auto-derived from `window.location.origin + '/callback'` (same as redirectUri)
|
|
118
|
+
* - `logoutRedirectUri`: Auto-derived from `window.location.origin`
|
|
119
|
+
* - `scope`: `'openid profile email offline_access transact'`
|
|
120
|
+
* - `audience`: `'platform_api'`
|
|
121
|
+
* - `authenticationDomain`: `'https://auth.immutable.com'`
|
|
96
122
|
*
|
|
97
123
|
* Must be used within a SessionProvider from next-auth/react.
|
|
98
124
|
*
|
|
99
|
-
* @example
|
|
125
|
+
* @example Minimal usage (uses all defaults)
|
|
100
126
|
* ```tsx
|
|
101
127
|
* import { useLogin, useImmutableSession } from '@imtbl/auth-next-client';
|
|
102
128
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
129
|
+
* function LoginButton() {
|
|
130
|
+
* const { isAuthenticated } = useImmutableSession();
|
|
131
|
+
* const { loginWithPopup, isLoggingIn, error } = useLogin();
|
|
132
|
+
*
|
|
133
|
+
* if (isAuthenticated) {
|
|
134
|
+
* return <p>You are logged in!</p>;
|
|
135
|
+
* }
|
|
136
|
+
*
|
|
137
|
+
* return (
|
|
138
|
+
* <>
|
|
139
|
+
* <button onClick={() => loginWithPopup()} disabled={isLoggingIn}>
|
|
140
|
+
* {isLoggingIn ? 'Signing in...' : 'Sign In'}
|
|
141
|
+
* </button>
|
|
142
|
+
* {error && <p style={{ color: 'red' }}>{error}</p>}
|
|
143
|
+
* </>
|
|
144
|
+
* );
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @example With custom configuration
|
|
149
|
+
* ```tsx
|
|
150
|
+
* import { useLogin, useImmutableSession } from '@imtbl/auth-next-client';
|
|
108
151
|
*
|
|
109
152
|
* function LoginButton() {
|
|
110
153
|
* const { isAuthenticated } = useImmutableSession();
|
|
111
154
|
* const { loginWithPopup, isLoggingIn, error } = useLogin();
|
|
112
155
|
*
|
|
156
|
+
* const handleLogin = () => {
|
|
157
|
+
* loginWithPopup({
|
|
158
|
+
* clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID,
|
|
159
|
+
* redirectUri: `${window.location.origin}/callback`,
|
|
160
|
+
* });
|
|
161
|
+
* };
|
|
162
|
+
*
|
|
113
163
|
* if (isAuthenticated) {
|
|
114
164
|
* return <p>You are logged in!</p>;
|
|
115
165
|
* }
|
|
116
166
|
*
|
|
117
167
|
* return (
|
|
118
168
|
* <>
|
|
119
|
-
* <button onClick={
|
|
169
|
+
* <button onClick={handleLogin} disabled={isLoggingIn}>
|
|
120
170
|
* {isLoggingIn ? 'Signing in...' : 'Sign In'}
|
|
121
171
|
* </button>
|
|
122
172
|
* {error && <p style={{ color: 'red' }}>{error}</p>}
|
|
@@ -136,9 +186,11 @@ export interface UseLogoutReturn {
|
|
|
136
186
|
* This ensures that when the user logs in again, they will be prompted to select
|
|
137
187
|
* an account instead of being automatically logged in with the previous account.
|
|
138
188
|
*
|
|
139
|
-
*
|
|
189
|
+
* Config is optional - defaults will be auto-derived if not provided.
|
|
190
|
+
*
|
|
191
|
+
* @param config - Optional logout configuration with clientId and optional redirectUri
|
|
140
192
|
*/
|
|
141
|
-
logout: (config
|
|
193
|
+
logout: (config?: LogoutConfig) => Promise<void>;
|
|
142
194
|
/** Whether logout is currently in progress */
|
|
143
195
|
isLoggingOut: boolean;
|
|
144
196
|
/** Error message from the last logout attempt, or null if none */
|
|
@@ -155,16 +207,38 @@ export interface UseLogoutReturn {
|
|
|
155
207
|
* an account (for social logins like Google) instead of being automatically logged
|
|
156
208
|
* in with the previous account.
|
|
157
209
|
*
|
|
210
|
+
* Config is optional - defaults will be auto-derived if not provided:
|
|
211
|
+
* - `clientId`: Auto-detected based on environment (sandbox vs production)
|
|
212
|
+
* - `logoutRedirectUri`: Auto-derived from `window.location.origin`
|
|
213
|
+
*
|
|
158
214
|
* Must be used within a SessionProvider from next-auth/react.
|
|
159
215
|
*
|
|
160
|
-
* @example
|
|
216
|
+
* @example Minimal usage (uses all defaults)
|
|
161
217
|
* ```tsx
|
|
162
218
|
* import { useLogout, useImmutableSession } from '@imtbl/auth-next-client';
|
|
163
219
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
220
|
+
* function LogoutButton() {
|
|
221
|
+
* const { isAuthenticated } = useImmutableSession();
|
|
222
|
+
* const { logout, isLoggingOut, error } = useLogout();
|
|
223
|
+
*
|
|
224
|
+
* if (!isAuthenticated) {
|
|
225
|
+
* return null;
|
|
226
|
+
* }
|
|
227
|
+
*
|
|
228
|
+
* return (
|
|
229
|
+
* <>
|
|
230
|
+
* <button onClick={() => logout()} disabled={isLoggingOut}>
|
|
231
|
+
* {isLoggingOut ? 'Signing out...' : 'Sign Out'}
|
|
232
|
+
* </button>
|
|
233
|
+
* {error && <p style={{ color: 'red' }}>{error}</p>}
|
|
234
|
+
* </>
|
|
235
|
+
* );
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* @example With custom configuration
|
|
240
|
+
* ```tsx
|
|
241
|
+
* import { useLogout, useImmutableSession } from '@imtbl/auth-next-client';
|
|
168
242
|
*
|
|
169
243
|
* function LogoutButton() {
|
|
170
244
|
* const { isAuthenticated } = useImmutableSession();
|
|
@@ -176,7 +250,13 @@ export interface UseLogoutReturn {
|
|
|
176
250
|
*
|
|
177
251
|
* return (
|
|
178
252
|
* <>
|
|
179
|
-
* <button
|
|
253
|
+
* <button
|
|
254
|
+
* onClick={() => logout({
|
|
255
|
+
* clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID,
|
|
256
|
+
* logoutRedirectUri: `${window.location.origin}/custom-logout`,
|
|
257
|
+
* })}
|
|
258
|
+
* disabled={isLoggingOut}
|
|
259
|
+
* >
|
|
180
260
|
* {isLoggingOut ? 'Signing out...' : 'Sign Out'}
|
|
181
261
|
* </button>
|
|
182
262
|
* {error && <p style={{ color: 'red' }}>{error}</p>}
|
|
@@ -186,3 +266,4 @@ export interface UseLogoutReturn {
|
|
|
186
266
|
* ```
|
|
187
267
|
*/
|
|
188
268
|
export declare function useLogout(): UseLogoutReturn;
|
|
269
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for persisting idToken in localStorage.
|
|
3
|
+
*
|
|
4
|
+
* The idToken is stripped from the NextAuth session cookie (via a custom
|
|
5
|
+
* jwt.encode in @imtbl/auth-next-server) to keep cookie size under CDN header
|
|
6
|
+
* limits (CloudFront 20 KB). Instead, the client stores idToken in
|
|
7
|
+
* localStorage so that wallet operations (e.g., MagicTEESigner) can still
|
|
8
|
+
* access it via getUser().
|
|
9
|
+
*
|
|
10
|
+
* All functions are safe to call during SSR or in restricted environments
|
|
11
|
+
* (e.g., incognito mode with localStorage disabled) -- they silently no-op.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Store the idToken in localStorage.
|
|
15
|
+
* @param idToken - The raw ID token JWT string
|
|
16
|
+
*/
|
|
17
|
+
export declare function storeIdToken(idToken: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Retrieve the idToken from localStorage.
|
|
20
|
+
* @returns The stored idToken, or undefined if not available.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getStoredIdToken(): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Remove the idToken from localStorage (e.g., on logout).
|
|
25
|
+
*/
|
|
26
|
+
export declare function clearStoredIdToken(): void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -28,3 +28,5 @@ export type { ImmutableUserClient, ImmutableTokenDataClient, ZkEvmInfo, } from '
|
|
|
28
28
|
export type { ImmutableAuthConfig, ImmutableTokenData, ImmutableUser, AuthProps, AuthPropsWithData, ProtectedAuthProps, ProtectedAuthPropsWithData, } from '@imtbl/auth-next-server';
|
|
29
29
|
export type { LoginConfig, StandaloneLoginOptions, DirectLoginOptions, LogoutConfig, } from '@imtbl/auth';
|
|
30
30
|
export { MarketingConsentStatus } from '@imtbl/auth';
|
|
31
|
+
export { DEFAULT_AUTH_DOMAIN, DEFAULT_AUDIENCE, DEFAULT_SCOPE, IMMUTABLE_PROVIDER_ID, DEFAULT_SANDBOX_CLIENT_ID, DEFAULT_REDIRECT_URI_PATH, DEFAULT_LOGOUT_REDIRECT_URI_PATH, } from './constants';
|
|
32
|
+
export { deriveDefaultRedirectUri } from './defaultConfig';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imtbl/auth-next-client",
|
|
3
|
-
"version": "2.12.7-alpha.
|
|
3
|
+
"version": "2.12.7-alpha.11",
|
|
4
4
|
"description": "Immutable Auth.js v5 integration for Next.js - Client-side components",
|
|
5
5
|
"author": "Immutable",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@imtbl/auth": "2.12.7-alpha.
|
|
31
|
-
"@imtbl/auth-next-server": "2.12.7-alpha.
|
|
30
|
+
"@imtbl/auth": "2.12.7-alpha.11",
|
|
31
|
+
"@imtbl/auth-next-server": "2.12.7-alpha.11"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"next": "^15.0.0",
|
|
34
|
+
"next": "^14.0.0 || ^15.0.0",
|
|
35
35
|
"next-auth": "^5.0.0-beta.25",
|
|
36
36
|
"react": "^18.2.0 || ^19.0.0"
|
|
37
37
|
},
|
|
@@ -49,12 +49,14 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@swc/core": "^1.4.2",
|
|
51
51
|
"@swc/jest": "^0.2.37",
|
|
52
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
53
|
+
"@testing-library/react": "^13.4.0",
|
|
52
54
|
"@types/jest": "^29.5.12",
|
|
53
55
|
"@types/node": "^22.10.7",
|
|
54
56
|
"@types/react": "^18.3.5",
|
|
55
57
|
"eslint": "^8.56.0",
|
|
56
58
|
"jest": "^29.7.0",
|
|
57
|
-
"next": "^15.
|
|
59
|
+
"next": "^15.2.6",
|
|
58
60
|
"next-auth": "^5.0.0-beta.30",
|
|
59
61
|
"react": "^18.2.0",
|
|
60
62
|
"tsup": "^8.3.0",
|
package/src/callback.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { signIn } from 'next-auth/react';
|
|
|
6
6
|
import { handleLoginCallback as handleAuthCallback, type TokenResponse } from '@imtbl/auth';
|
|
7
7
|
import type { ImmutableUserClient } from './types';
|
|
8
8
|
import { IMMUTABLE_PROVIDER_ID } from './constants';
|
|
9
|
+
import { storeIdToken } from './idTokenStorage';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Config for CallbackPage - matches LoginConfig from @imtbl/auth
|
|
@@ -159,6 +160,12 @@ export function CallbackPage({
|
|
|
159
160
|
// Not in a popup - sign in to NextAuth with the tokens
|
|
160
161
|
const tokenData = mapTokensToSignInData(tokens);
|
|
161
162
|
|
|
163
|
+
// Persist idToken to localStorage before signIn so it's available
|
|
164
|
+
// immediately. The cookie won't contain idToken (stripped by jwt.encode).
|
|
165
|
+
if (tokens.idToken) {
|
|
166
|
+
storeIdToken(tokens.idToken);
|
|
167
|
+
}
|
|
168
|
+
|
|
162
169
|
const result = await signIn(IMMUTABLE_PROVIDER_ID, {
|
|
163
170
|
tokens: JSON.stringify(tokenData),
|
|
164
171
|
redirect: false,
|