@imtbl/auth-next-client 2.12.5-alpha.13

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.
@@ -0,0 +1,547 @@
1
+ 'use client';
2
+
3
+ import React, {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ useCallback,
10
+ useMemo,
11
+ } from 'react';
12
+ import {
13
+ SessionProvider, useSession, signIn, signOut,
14
+ } from 'next-auth/react';
15
+ import type { Session } from 'next-auth';
16
+ import {
17
+ Auth, AuthEvents, type User, type LoginOptions, type UserRemovedReason,
18
+ } from '@imtbl/auth';
19
+ import type {
20
+ ImmutableAuthProviderProps,
21
+ UseImmutableAuthReturn,
22
+ ImmutableUserClient,
23
+ ImmutableTokenDataClient,
24
+ } from './types';
25
+ import { getTokenExpiry } from './utils/token';
26
+ import {
27
+ DEFAULT_AUTH_DOMAIN,
28
+ DEFAULT_AUDIENCE,
29
+ DEFAULT_SCOPE,
30
+ DEFAULT_NEXTAUTH_BASE_PATH,
31
+ IMMUTABLE_PROVIDER_ID,
32
+ } from './constants';
33
+
34
+ /**
35
+ * Internal context for Immutable auth state
36
+ */
37
+ interface ImmutableAuthContextValue {
38
+ auth: Auth | null;
39
+ config: ImmutableAuthProviderProps['config'];
40
+ basePath: string;
41
+ }
42
+
43
+ const ImmutableAuthContext = createContext<ImmutableAuthContextValue | null>(null);
44
+
45
+ /**
46
+ * Internal provider that manages Auth instance
47
+ */
48
+ function ImmutableAuthInner({
49
+ children,
50
+ config,
51
+ basePath,
52
+ }: {
53
+ children: React.ReactNode;
54
+ config: ImmutableAuthProviderProps['config'];
55
+ basePath: string;
56
+ }) {
57
+ // Use state instead of ref so changes trigger re-renders and update context consumers
58
+ const [auth, setAuth] = useState<Auth | null>(null);
59
+ const prevConfigRef = useRef<string | null>(null);
60
+ // Track auth instance in a ref to check if it's still valid synchronously
61
+ // This is needed for React 18 Strict Mode compatibility
62
+ const authInstanceRef = useRef<Auth | null>(null);
63
+ const [isAuthReady, setIsAuthReady] = useState(false);
64
+ const { data: session, update: updateSession } = useSession();
65
+
66
+ // Initialize/reinitialize Auth instance when config changes (e.g., environment switch)
67
+ useEffect(() => {
68
+ if (typeof window === 'undefined') return undefined;
69
+
70
+ // Create a config key to detect changes - include all properties used in Auth constructor
71
+ // to ensure the Auth instance is recreated when any config property changes
72
+ const configKey = [
73
+ config.clientId,
74
+ config.redirectUri,
75
+ config.popupRedirectUri || '',
76
+ config.logoutRedirectUri || '',
77
+ config.audience || DEFAULT_AUDIENCE,
78
+ config.scope || DEFAULT_SCOPE,
79
+ config.authenticationDomain || DEFAULT_AUTH_DOMAIN,
80
+ config.passportDomain || '',
81
+ ].join(':');
82
+
83
+ // Only skip recreation if BOTH:
84
+ // 1. Config hasn't changed (same configKey)
85
+ // 2. Auth instance still exists (wasn't nullified by cleanup)
86
+ // This handles React 18 Strict Mode where effects run twice:
87
+ // setup → cleanup → setup. After cleanup, authInstanceRef is null,
88
+ // so we correctly recreate Auth on the second setup.
89
+ if (prevConfigRef.current === configKey && authInstanceRef.current !== null) {
90
+ return undefined;
91
+ }
92
+ prevConfigRef.current = configKey;
93
+
94
+ // Create new Auth instance with current config
95
+ const newAuth = new Auth({
96
+ clientId: config.clientId,
97
+ redirectUri: config.redirectUri,
98
+ popupRedirectUri: config.popupRedirectUri,
99
+ logoutRedirectUri: config.logoutRedirectUri,
100
+ audience: config.audience || DEFAULT_AUDIENCE,
101
+ scope: config.scope || DEFAULT_SCOPE,
102
+ authenticationDomain: config.authenticationDomain || DEFAULT_AUTH_DOMAIN,
103
+ passportDomain: config.passportDomain,
104
+ });
105
+
106
+ authInstanceRef.current = newAuth;
107
+ setAuth(newAuth);
108
+ setIsAuthReady(true);
109
+
110
+ // Cleanup function: When config changes or component unmounts,
111
+ // clear the Auth instance to prevent memory leaks.
112
+ // The Auth class holds a UserManager from oidc-client-ts which may register
113
+ // window event listeners (storage, message). By setting auth to null,
114
+ // we allow garbage collection.
115
+ return () => {
116
+ authInstanceRef.current = null;
117
+ setAuth(null);
118
+ setIsAuthReady(false);
119
+ };
120
+ }, [config]);
121
+
122
+ // Listen for Auth events to sync tokens to NextAuth
123
+ useEffect(() => {
124
+ if (!auth || !isAuthReady) return undefined;
125
+
126
+ const handleLoggedIn = async (authUser: User) => {
127
+ // When Auth refreshes tokens, sync to NextAuth session
128
+ if (session?.accessToken && authUser.accessToken !== session.accessToken) {
129
+ await updateSession({
130
+ accessToken: authUser.accessToken,
131
+ refreshToken: authUser.refreshToken,
132
+ idToken: authUser.idToken,
133
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
134
+ zkEvm: authUser.zkEvm,
135
+ });
136
+ }
137
+ };
138
+
139
+ // Handle client-side token refresh - critical for refresh token rotation.
140
+ // When Auth refreshes tokens via signinSilent(), we must sync the new tokens
141
+ // (especially the new refresh token) to the NextAuth session. Without this,
142
+ // the server-side JWT callback may use a stale refresh token that Auth0 has
143
+ // already invalidated, causing "Unknown or invalid refresh token" errors.
144
+ const handleTokenRefreshed = async (authUser: User) => {
145
+ await updateSession({
146
+ accessToken: authUser.accessToken,
147
+ refreshToken: authUser.refreshToken,
148
+ idToken: authUser.idToken,
149
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
150
+ zkEvm: authUser.zkEvm,
151
+ });
152
+ };
153
+
154
+ // Handle user removal from Auth due to permanent auth errors
155
+ // (e.g., invalid_grant, login_required - refresh token is truly invalid).
156
+ // Transient errors (network, timeout, server errors) do NOT trigger this.
157
+ // When this happens, we must clear the NextAuth session to keep them in sync.
158
+ const handleUserRemoved = async (payload: { reason: UserRemovedReason; error?: string }) => {
159
+ // eslint-disable-next-line no-console
160
+ console.warn('[auth-next-client] User removed from Auth SDK:', payload.reason, payload.error);
161
+ // Sign out from NextAuth to clear the session cookie
162
+ // This prevents the state mismatch where session exists but Auth has no user
163
+ await signOut({ redirect: false });
164
+ };
165
+
166
+ // Handle explicit logout from Auth SDK (e.g., via auth.logout() or auth.getLogoutUrl())
167
+ // This ensures NextAuth session is always in sync with Auth SDK state
168
+ const handleLoggedOut = async () => {
169
+ // Sign out from NextAuth to clear the session cookie
170
+ await signOut({ redirect: false });
171
+ };
172
+
173
+ auth.eventEmitter.on(AuthEvents.LOGGED_IN, handleLoggedIn);
174
+ auth.eventEmitter.on(AuthEvents.TOKEN_REFRESHED, handleTokenRefreshed);
175
+ auth.eventEmitter.on(AuthEvents.USER_REMOVED, handleUserRemoved);
176
+ auth.eventEmitter.on(AuthEvents.LOGGED_OUT, handleLoggedOut);
177
+
178
+ return () => {
179
+ auth.eventEmitter.removeListener(AuthEvents.LOGGED_IN, handleLoggedIn);
180
+ auth.eventEmitter.removeListener(AuthEvents.TOKEN_REFRESHED, handleTokenRefreshed);
181
+ auth.eventEmitter.removeListener(AuthEvents.USER_REMOVED, handleUserRemoved);
182
+ auth.eventEmitter.removeListener(AuthEvents.LOGGED_OUT, handleLoggedOut);
183
+ };
184
+ }, [auth, isAuthReady, session, updateSession]);
185
+
186
+ const contextValue = useMemo(
187
+ () => ({ auth, config, basePath }),
188
+ [auth, config, basePath],
189
+ );
190
+
191
+ return (
192
+ <ImmutableAuthContext.Provider value={contextValue}>
193
+ {children}
194
+ </ImmutableAuthContext.Provider>
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Provider component for Immutable authentication with Auth.js v5
200
+ *
201
+ * Wraps your app to provide authentication state via useImmutableAuth hook.
202
+ *
203
+ * @example App Router (recommended)
204
+ * ```tsx
205
+ * // app/providers.tsx
206
+ * "use client";
207
+ * import { ImmutableAuthProvider } from "@imtbl/auth-next-client";
208
+ *
209
+ * const config = {
210
+ * clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
211
+ * redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
212
+ * };
213
+ *
214
+ * export function Providers({ children }: { children: React.ReactNode }) {
215
+ * return (
216
+ * <ImmutableAuthProvider config={config}>
217
+ * {children}
218
+ * </ImmutableAuthProvider>
219
+ * );
220
+ * }
221
+ * ```
222
+ */
223
+ export function ImmutableAuthProvider({
224
+ children,
225
+ config,
226
+ session,
227
+ basePath = DEFAULT_NEXTAUTH_BASE_PATH,
228
+ }: ImmutableAuthProviderProps) {
229
+ return (
230
+ <SessionProvider session={session as Session | null | undefined} basePath={basePath}>
231
+ <ImmutableAuthInner config={config} basePath={basePath}>{children}</ImmutableAuthInner>
232
+ </SessionProvider>
233
+ );
234
+ }
235
+
236
+ /**
237
+ * Hook to access Immutable authentication state and methods
238
+ *
239
+ * Must be used within an ImmutableAuthProvider.
240
+ */
241
+ export function useImmutableAuth(): UseImmutableAuthReturn {
242
+ const context = useContext(ImmutableAuthContext);
243
+ const { data: sessionData, status } = useSession();
244
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
245
+
246
+ if (!context) {
247
+ throw new Error('useImmutableAuth must be used within ImmutableAuthProvider');
248
+ }
249
+
250
+ // Cast session to our augmented Session type
251
+ const session = sessionData as Session | null;
252
+
253
+ const { auth } = context;
254
+ const isLoading = status === 'loading';
255
+ const isAuthenticated = status === 'authenticated' && !!session;
256
+
257
+ // Extract user from session
258
+ const user: ImmutableUserClient | null = session?.user
259
+ ? {
260
+ sub: session.user.sub,
261
+ email: session.user.email,
262
+ nickname: session.user.nickname,
263
+ }
264
+ : null;
265
+
266
+ // Sign in with Immutable popup
267
+ const handleSignIn = useCallback(async (options?: LoginOptions) => {
268
+ if (!auth) {
269
+ throw new Error('Auth not initialized');
270
+ }
271
+
272
+ setIsLoggingIn(true);
273
+ try {
274
+ // Open popup login with optional login options
275
+ const authUser = await auth.login(options);
276
+ if (!authUser) {
277
+ throw new Error('Login failed');
278
+ }
279
+
280
+ // Build token data for NextAuth
281
+ const tokenData: ImmutableTokenDataClient = {
282
+ accessToken: authUser.accessToken,
283
+ refreshToken: authUser.refreshToken,
284
+ idToken: authUser.idToken,
285
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
286
+ profile: {
287
+ sub: authUser.profile.sub,
288
+ email: authUser.profile.email,
289
+ nickname: authUser.profile.nickname,
290
+ },
291
+ zkEvm: authUser.zkEvm,
292
+ };
293
+
294
+ // Sign in to NextAuth with the tokens
295
+ const result = await signIn(IMMUTABLE_PROVIDER_ID, {
296
+ tokens: JSON.stringify(tokenData),
297
+ redirect: false,
298
+ });
299
+
300
+ // signIn with redirect: false returns a result object instead of throwing
301
+ if (result?.error) {
302
+ throw new Error(`NextAuth sign-in failed: ${result.error}`);
303
+ }
304
+ if (!result?.ok) {
305
+ throw new Error('NextAuth sign-in failed: unknown error');
306
+ }
307
+ } finally {
308
+ setIsLoggingIn(false);
309
+ }
310
+ }, [auth]);
311
+
312
+ // Sign out from both NextAuth and Immutable
313
+ const handleSignOut = useCallback(async () => {
314
+ if (auth) {
315
+ try {
316
+ // Clear local Auth state - this emits LOGGED_OUT event which triggers
317
+ // handleLoggedOut listener to call signOut() from NextAuth
318
+ await auth.getLogoutUrl();
319
+ } catch (error) {
320
+ // If getLogoutUrl fails, fall back to direct signOut
321
+ // eslint-disable-next-line no-console
322
+ console.warn('[auth-next-client] Logout cleanup error:', error);
323
+ await signOut({ redirect: false });
324
+ }
325
+ } else {
326
+ // No auth instance, just sign out from NextAuth directly
327
+ await signOut({ redirect: false });
328
+ }
329
+ }, [auth]);
330
+
331
+ // Get access token (refreshes if needed)
332
+ const getAccessToken = useCallback(async (): Promise<string> => {
333
+ // First try to get from Auth instance (most up-to-date)
334
+ if (auth) {
335
+ try {
336
+ const token = await auth.getAccessToken();
337
+ if (token) {
338
+ return token;
339
+ }
340
+ } catch {
341
+ // Fall through to session
342
+ }
343
+ }
344
+
345
+ // Fall back to session token, but check for errors first.
346
+ // Session errors indicate authentication issues that require user action:
347
+ // - "TokenExpired": Access token expired and Auth instance couldn't refresh
348
+ // (this happens if localStorage was cleared but session cookie remains)
349
+ // - "RefreshTokenError": Refresh token is invalid/expired, need re-login
350
+ if (session?.error) {
351
+ throw new Error(
352
+ session.error === 'TokenExpired'
353
+ ? 'Session expired. Please log in again.'
354
+ : `Authentication error: ${session.error}`,
355
+ );
356
+ }
357
+
358
+ if (session?.accessToken) {
359
+ return session.accessToken;
360
+ }
361
+
362
+ throw new Error('No access token available');
363
+ }, [auth, session]);
364
+
365
+ return {
366
+ user,
367
+ session,
368
+ isLoading,
369
+ isLoggingIn,
370
+ isAuthenticated,
371
+ signIn: handleSignIn,
372
+ signOut: handleSignOut,
373
+ getAccessToken,
374
+ auth,
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Hook to get a function that returns a valid access token
380
+ */
381
+ export function useAccessToken(): () => Promise<string> {
382
+ const { getAccessToken } = useImmutableAuth();
383
+ return getAccessToken;
384
+ }
385
+
386
+ /**
387
+ * Result from useHydratedData hook
388
+ */
389
+ export interface UseHydratedDataResult<T> {
390
+ data: T | null;
391
+ isLoading: boolean;
392
+ error: Error | null;
393
+ refetch: () => Promise<void>;
394
+ }
395
+
396
+ /**
397
+ * Props for useHydratedData hook - matches AuthPropsWithData from server
398
+ */
399
+ export interface HydratedDataProps<T> {
400
+ session: Session | null;
401
+ ssr: boolean;
402
+ data: T | null;
403
+ fetchError?: string;
404
+ authError?: string;
405
+ }
406
+
407
+ /**
408
+ * Hook for hydrating server-fetched data with automatic client-side fallback.
409
+ *
410
+ * This is the recommended pattern for components that receive data from `getAuthenticatedData`:
411
+ * - When `ssr: true` and `data` exists: Uses pre-fetched server data immediately (no loading state)
412
+ * - When `ssr: false`: Refreshes token client-side and fetches data
413
+ * - When `fetchError` exists: Retries fetch client-side
414
+ */
415
+ export function useHydratedData<T>(
416
+ props: HydratedDataProps<T>,
417
+ fetcher: (accessToken: string) => Promise<T>,
418
+ ): UseHydratedDataResult<T> {
419
+ const { getAccessToken, auth } = useImmutableAuth();
420
+ const {
421
+ ssr,
422
+ data: serverData,
423
+ fetchError,
424
+ } = props;
425
+
426
+ // Determine if we need to fetch client-side:
427
+ // 1. SSR was skipped (token expired) - need to refresh token and fetch
428
+ // 2. Server fetch failed - retry on client
429
+ // Note: We intentionally do NOT check serverData === null here.
430
+ // When ssr=true and no fetchError, null is a valid response (e.g., "no results found")
431
+ // and should not trigger a client-side refetch.
432
+ const needsClientFetch = !ssr || Boolean(fetchError);
433
+
434
+ // Initialize state with server data if available
435
+ const [data, setData] = useState<T | null>(serverData);
436
+ const [isLoading, setIsLoading] = useState(needsClientFetch);
437
+ const [error, setError] = useState<Error | null>(
438
+ fetchError ? new Error(fetchError) : null,
439
+ );
440
+
441
+ // Track if we've already started fetching to prevent duplicate calls
442
+ const hasFetchedRef = useRef(false);
443
+
444
+ // Fetch ID counter to detect stale fetches.
445
+ // When props change, we increment this counter. In-flight fetches compare their
446
+ // captured ID against the current counter and ignore results if they don't match.
447
+ // This prevents race conditions where a slow client-side fetch overwrites
448
+ // fresh server data that arrived via prop changes (e.g., during soft navigation).
449
+ const fetchIdRef = useRef(0);
450
+
451
+ // Track previous props to detect changes (for navigation between routes)
452
+ const prevPropsRef = useRef({ serverData, ssr, fetchError });
453
+
454
+ // Sync state when props change (e.g., navigating between routes with same component).
455
+ // useState only uses initial value on mount - subsequent prop changes are ignored
456
+ // unless we explicitly sync them.
457
+ useEffect(() => {
458
+ const prevProps = prevPropsRef.current;
459
+ const propsChanged = prevProps.serverData !== serverData
460
+ || prevProps.ssr !== ssr
461
+ || prevProps.fetchError !== fetchError;
462
+
463
+ if (propsChanged) {
464
+ prevPropsRef.current = { serverData, ssr, fetchError };
465
+ // Reset fetch guard to allow fetching with new props
466
+ hasFetchedRef.current = false;
467
+ // Increment fetch ID to invalidate any in-flight fetches
468
+ fetchIdRef.current += 1;
469
+
470
+ // Sync state from new props
471
+ if (ssr && !fetchError) {
472
+ // SSR succeeded: use server data directly (even if null - that's valid)
473
+ setData(serverData);
474
+ setIsLoading(false);
475
+ setError(null);
476
+ } else {
477
+ // Need client-side fetch: reset state to trigger fetch
478
+ setData(null);
479
+ setIsLoading(true);
480
+ setError(fetchError ? new Error(fetchError) : null);
481
+ }
482
+ }
483
+ }, [serverData, ssr, fetchError]);
484
+
485
+ const fetchData = useCallback(async () => {
486
+ // Capture current fetch ID to detect staleness after async operations
487
+ const currentFetchId = fetchIdRef.current;
488
+
489
+ setIsLoading(true);
490
+ setError(null);
491
+
492
+ try {
493
+ // Always get the current valid token via getAccessToken()
494
+ // This handles token refresh and uses the live session from useSession()
495
+ // rather than stale props.session which doesn't update after client-side refresh
496
+ const token = await getAccessToken();
497
+ const result = await fetcher(token);
498
+
499
+ // Only update state if this fetch is still current.
500
+ // If props changed while we were fetching, fetchIdRef will have been incremented
501
+ // and our captured ID will be stale - discard results to avoid overwriting
502
+ // fresh server data with stale client-fetched results.
503
+ if (fetchIdRef.current === currentFetchId) {
504
+ setData(result);
505
+ }
506
+ } catch (err) {
507
+ // Only update error state if this fetch is still current
508
+ if (fetchIdRef.current === currentFetchId) {
509
+ setError(err instanceof Error ? err : new Error(String(err)));
510
+ }
511
+ } finally {
512
+ // Only update loading state if this fetch is still current
513
+ if (fetchIdRef.current === currentFetchId) {
514
+ setIsLoading(false);
515
+ }
516
+ }
517
+ }, [fetcher, getAccessToken]);
518
+
519
+ // Fetch client-side data when needed
520
+ // When ssr is false (token expired server-side), we must wait for the Auth instance
521
+ // to be initialized before fetching. Auth is created in a parent component's effect
522
+ // which runs AFTER this (child) effect due to React's bottom-up effect execution.
523
+ // Without waiting for auth, getAccessToken() would find auth === null, fall through
524
+ // to check the session (which has an error because token is expired), and throw
525
+ // "Session expired" instead of properly refreshing the token via Auth.
526
+ useEffect(() => {
527
+ // Already fetched, don't fetch again
528
+ if (hasFetchedRef.current) return;
529
+
530
+ // Don't need client fetch
531
+ if (!needsClientFetch) return;
532
+
533
+ // When ssr is false, we need Auth to refresh the expired token.
534
+ // Wait for it to initialize before attempting to fetch.
535
+ if (!ssr && !auth) return;
536
+
537
+ hasFetchedRef.current = true;
538
+ fetchData();
539
+ }, [needsClientFetch, ssr, auth, fetchData]);
540
+
541
+ return {
542
+ data,
543
+ isLoading,
544
+ error,
545
+ refetch: fetchData,
546
+ };
547
+ }
package/src/types.ts ADDED
@@ -0,0 +1,148 @@
1
+ import type { DefaultSession, Session } from 'next-auth';
2
+
3
+ // Re-export types from auth-next-server for convenience
4
+ export type {
5
+ ImmutableAuthConfig,
6
+ ImmutableTokenData,
7
+ ZkEvmUser,
8
+ ImmutableUser,
9
+ } from '@imtbl/auth-next-server';
10
+
11
+ /**
12
+ * zkEVM wallet information
13
+ */
14
+ export interface ZkEvmInfo {
15
+ ethAddress: string;
16
+ userAdminAddress: string;
17
+ }
18
+
19
+ /**
20
+ * Auth.js v5 module augmentation to add Immutable-specific fields
21
+ */
22
+ declare module 'next-auth' {
23
+ // eslint-disable-next-line @typescript-eslint/no-shadow
24
+ interface Session extends DefaultSession {
25
+ user: {
26
+ sub: string;
27
+ email?: string;
28
+ nickname?: string;
29
+ } & DefaultSession['user'];
30
+ accessToken: string;
31
+ refreshToken?: string;
32
+ idToken?: string;
33
+ accessTokenExpires: number;
34
+ zkEvm?: ZkEvmInfo;
35
+ error?: string;
36
+ }
37
+
38
+ interface User {
39
+ id: string;
40
+ sub: string;
41
+ email?: string | null;
42
+ nickname?: string;
43
+ accessToken: string;
44
+ refreshToken?: string;
45
+ idToken?: string;
46
+ accessTokenExpires: number;
47
+ zkEvm?: ZkEvmInfo;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Props for ImmutableAuthProvider
53
+ */
54
+ export interface ImmutableAuthProviderProps {
55
+ children: React.ReactNode;
56
+ /**
57
+ * Immutable auth configuration
58
+ */
59
+ config: {
60
+ clientId: string;
61
+ redirectUri: string;
62
+ popupRedirectUri?: string;
63
+ logoutRedirectUri?: string;
64
+ audience?: string;
65
+ scope?: string;
66
+ authenticationDomain?: string;
67
+ passportDomain?: string;
68
+ };
69
+ /**
70
+ * Initial session from server (for SSR hydration)
71
+ * Can be Session from auth() or any compatible session object
72
+ */
73
+ session?: Session | DefaultSession | null;
74
+ /**
75
+ * Custom base path for Auth.js API routes
76
+ * Use this when you have multiple auth endpoints (e.g., per environment)
77
+ * @default "/api/auth"
78
+ */
79
+ basePath?: string;
80
+ }
81
+
82
+ /**
83
+ * User profile from Immutable (local definition for client)
84
+ */
85
+ export interface ImmutableUserClient {
86
+ sub: string;
87
+ email?: string;
88
+ nickname?: string;
89
+ }
90
+
91
+ /**
92
+ * Token data passed from client to Auth.js credentials provider
93
+ */
94
+ export interface ImmutableTokenDataClient {
95
+ accessToken: string;
96
+ refreshToken?: string;
97
+ idToken?: string;
98
+ accessTokenExpires: number;
99
+ profile: {
100
+ sub: string;
101
+ email?: string;
102
+ nickname?: string;
103
+ };
104
+ zkEvm?: ZkEvmInfo;
105
+ }
106
+
107
+ /**
108
+ * Return type of useImmutableAuth hook
109
+ */
110
+ export interface UseImmutableAuthReturn {
111
+ /**
112
+ * Current user profile (null if not authenticated)
113
+ */
114
+ user: ImmutableUserClient | null;
115
+ /**
116
+ * Full Auth.js session with tokens
117
+ */
118
+ session: Session | null;
119
+ /**
120
+ * Whether authentication state is loading (initial session fetch)
121
+ */
122
+ isLoading: boolean;
123
+ /**
124
+ * Whether a login flow is in progress (popup open, waiting for OAuth callback)
125
+ */
126
+ isLoggingIn: boolean;
127
+ /**
128
+ * Whether user is authenticated
129
+ */
130
+ isAuthenticated: boolean;
131
+ /**
132
+ * Sign in with Immutable (opens popup)
133
+ * @param options - Optional login options (cached session, silent login, redirect flow, direct login)
134
+ */
135
+ signIn: (options?: import('@imtbl/auth').LoginOptions) => Promise<void>;
136
+ /**
137
+ * Sign out from both Auth.js and Immutable
138
+ */
139
+ signOut: () => Promise<void>;
140
+ /**
141
+ * Get a valid access token (refreshes if needed)
142
+ */
143
+ getAccessToken: () => Promise<string>;
144
+ /**
145
+ * The underlying Auth instance (for advanced use)
146
+ */
147
+ auth: import('@imtbl/auth').Auth | null;
148
+ }