@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.
@@ -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 DEFAULT_TOKEN_EXPIRY_SECONDS = 900;
12
- var DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
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 { useCallback, useRef as useRef2, useState as useState2 } from "react";
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: currentSession.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 tokens = await rawLoginWithPopup(config, options);
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 tokens = await rawLoginWithEmbedded(config);
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
- await rawLoginWithRedirect(config, options);
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
- rawLogoutWithRedirect(config);
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
- * Shared constants for @imtbl/auth-next-client
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
- * Default token expiry in seconds (15 minutes)
26
- * Used as fallback when exp claim cannot be extracted from JWT
27
- */
28
- export declare const DEFAULT_TOKEN_EXPIRY_SECONDS = 900;
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;
@@ -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
- * Extended session type with Immutable token data
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
- export interface ImmutableSession extends Session {
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: LoginConfig, options?: StandaloneLoginOptions) => Promise<void>;
95
+ loginWithPopup: (config?: LoginConfig, options?: StandaloneLoginOptions) => Promise<void>;
77
96
  /** Start login with embedded modal flow */
78
- loginWithEmbedded: (config: LoginConfig) => Promise<void>;
97
+ loginWithEmbedded: (config?: LoginConfig) => Promise<void>;
79
98
  /** Start login with redirect flow (navigates away from page) */
80
- loginWithRedirect: (config: LoginConfig, options?: StandaloneLoginOptions) => Promise<void>;
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 is passed at call time to allow different configurations for different
95
- * login methods (e.g., different redirectUri vs popupRedirectUri).
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
- * const config = {
104
- * clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
105
- * redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
106
- * popupRedirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback/popup`,
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={() => loginWithPopup(config)} disabled={isLoggingIn}>
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
- * @param config - Logout configuration with clientId and optional redirectUri
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: LogoutConfig) => Promise<void>;
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
- * const logoutConfig = {
165
- * clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
166
- * logoutRedirectUri: process.env.NEXT_PUBLIC_BASE_URL!,
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 onClick={() => logout(logoutConfig)} disabled={isLoggingOut}>
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;
@@ -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.1",
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.1",
31
- "@imtbl/auth-next-server": "2.12.7-alpha.1"
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.1.6",
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,