@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,486 @@
1
+ 'use client';
2
+
3
+ // src/provider.tsx
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ useCallback,
11
+ useMemo
12
+ } from "react";
13
+ import {
14
+ SessionProvider,
15
+ useSession,
16
+ signIn,
17
+ signOut
18
+ } from "next-auth/react";
19
+ import {
20
+ Auth,
21
+ AuthEvents
22
+ } from "@imtbl/auth";
23
+
24
+ // src/utils/token.ts
25
+ import { decodeJwtPayload } from "@imtbl/auth";
26
+
27
+ // src/constants.ts
28
+ var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
29
+ var DEFAULT_AUDIENCE = "platform_api";
30
+ var DEFAULT_SCOPE = "openid profile email offline_access transact";
31
+ var IMMUTABLE_PROVIDER_ID = "immutable";
32
+ var DEFAULT_NEXTAUTH_BASE_PATH = "/api/auth";
33
+ var DEFAULT_TOKEN_EXPIRY_SECONDS = 900;
34
+ var DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
35
+
36
+ // src/utils/token.ts
37
+ function getTokenExpiry(accessToken) {
38
+ if (!accessToken) {
39
+ return Date.now() + DEFAULT_TOKEN_EXPIRY_MS;
40
+ }
41
+ try {
42
+ const payload = decodeJwtPayload(accessToken);
43
+ if (payload.exp && typeof payload.exp === "number") {
44
+ return payload.exp * 1e3;
45
+ }
46
+ return Date.now() + DEFAULT_TOKEN_EXPIRY_MS;
47
+ } catch {
48
+ return Date.now() + DEFAULT_TOKEN_EXPIRY_MS;
49
+ }
50
+ }
51
+
52
+ // src/provider.tsx
53
+ import { jsx } from "react/jsx-runtime";
54
+ var ImmutableAuthContext = createContext(null);
55
+ function ImmutableAuthInner({
56
+ children,
57
+ config,
58
+ basePath
59
+ }) {
60
+ const [auth, setAuth] = useState(null);
61
+ const prevConfigRef = useRef(null);
62
+ const authInstanceRef = useRef(null);
63
+ const [isAuthReady, setIsAuthReady] = useState(false);
64
+ const { data: session, update: updateSession } = useSession();
65
+ useEffect(() => {
66
+ if (typeof window === "undefined") return void 0;
67
+ const configKey = [
68
+ config.clientId,
69
+ config.redirectUri,
70
+ config.popupRedirectUri || "",
71
+ config.logoutRedirectUri || "",
72
+ config.audience || DEFAULT_AUDIENCE,
73
+ config.scope || DEFAULT_SCOPE,
74
+ config.authenticationDomain || DEFAULT_AUTH_DOMAIN,
75
+ config.passportDomain || ""
76
+ ].join(":");
77
+ if (prevConfigRef.current === configKey && authInstanceRef.current !== null) {
78
+ return void 0;
79
+ }
80
+ prevConfigRef.current = configKey;
81
+ const newAuth = new Auth({
82
+ clientId: config.clientId,
83
+ redirectUri: config.redirectUri,
84
+ popupRedirectUri: config.popupRedirectUri,
85
+ logoutRedirectUri: config.logoutRedirectUri,
86
+ audience: config.audience || DEFAULT_AUDIENCE,
87
+ scope: config.scope || DEFAULT_SCOPE,
88
+ authenticationDomain: config.authenticationDomain || DEFAULT_AUTH_DOMAIN,
89
+ passportDomain: config.passportDomain
90
+ });
91
+ authInstanceRef.current = newAuth;
92
+ setAuth(newAuth);
93
+ setIsAuthReady(true);
94
+ return () => {
95
+ authInstanceRef.current = null;
96
+ setAuth(null);
97
+ setIsAuthReady(false);
98
+ };
99
+ }, [config]);
100
+ useEffect(() => {
101
+ if (!auth || !isAuthReady) return void 0;
102
+ const handleLoggedIn = async (authUser) => {
103
+ if (session?.accessToken && authUser.accessToken !== session.accessToken) {
104
+ await updateSession({
105
+ accessToken: authUser.accessToken,
106
+ refreshToken: authUser.refreshToken,
107
+ idToken: authUser.idToken,
108
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
109
+ zkEvm: authUser.zkEvm
110
+ });
111
+ }
112
+ };
113
+ const handleTokenRefreshed = async (authUser) => {
114
+ await updateSession({
115
+ accessToken: authUser.accessToken,
116
+ refreshToken: authUser.refreshToken,
117
+ idToken: authUser.idToken,
118
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
119
+ zkEvm: authUser.zkEvm
120
+ });
121
+ };
122
+ const handleUserRemoved = async (payload) => {
123
+ console.warn("[auth-next-client] User removed from Auth SDK:", payload.reason, payload.error);
124
+ await signOut({ redirect: false });
125
+ };
126
+ const handleLoggedOut = async () => {
127
+ await signOut({ redirect: false });
128
+ };
129
+ auth.eventEmitter.on(AuthEvents.LOGGED_IN, handleLoggedIn);
130
+ auth.eventEmitter.on(AuthEvents.TOKEN_REFRESHED, handleTokenRefreshed);
131
+ auth.eventEmitter.on(AuthEvents.USER_REMOVED, handleUserRemoved);
132
+ auth.eventEmitter.on(AuthEvents.LOGGED_OUT, handleLoggedOut);
133
+ return () => {
134
+ auth.eventEmitter.removeListener(AuthEvents.LOGGED_IN, handleLoggedIn);
135
+ auth.eventEmitter.removeListener(AuthEvents.TOKEN_REFRESHED, handleTokenRefreshed);
136
+ auth.eventEmitter.removeListener(AuthEvents.USER_REMOVED, handleUserRemoved);
137
+ auth.eventEmitter.removeListener(AuthEvents.LOGGED_OUT, handleLoggedOut);
138
+ };
139
+ }, [auth, isAuthReady, session, updateSession]);
140
+ const contextValue = useMemo(
141
+ () => ({ auth, config, basePath }),
142
+ [auth, config, basePath]
143
+ );
144
+ return /* @__PURE__ */ jsx(ImmutableAuthContext.Provider, { value: contextValue, children });
145
+ }
146
+ function ImmutableAuthProvider({
147
+ children,
148
+ config,
149
+ session,
150
+ basePath = DEFAULT_NEXTAUTH_BASE_PATH
151
+ }) {
152
+ return /* @__PURE__ */ jsx(SessionProvider, { session, basePath, children: /* @__PURE__ */ jsx(ImmutableAuthInner, { config, basePath, children }) });
153
+ }
154
+ function useImmutableAuth() {
155
+ const context = useContext(ImmutableAuthContext);
156
+ const { data: sessionData, status } = useSession();
157
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
158
+ if (!context) {
159
+ throw new Error("useImmutableAuth must be used within ImmutableAuthProvider");
160
+ }
161
+ const session = sessionData;
162
+ const { auth } = context;
163
+ const isLoading = status === "loading";
164
+ const isAuthenticated = status === "authenticated" && !!session;
165
+ const user = session?.user ? {
166
+ sub: session.user.sub,
167
+ email: session.user.email,
168
+ nickname: session.user.nickname
169
+ } : null;
170
+ const handleSignIn = useCallback(async (options) => {
171
+ if (!auth) {
172
+ throw new Error("Auth not initialized");
173
+ }
174
+ setIsLoggingIn(true);
175
+ try {
176
+ const authUser = await auth.login(options);
177
+ if (!authUser) {
178
+ throw new Error("Login failed");
179
+ }
180
+ const tokenData = {
181
+ accessToken: authUser.accessToken,
182
+ refreshToken: authUser.refreshToken,
183
+ idToken: authUser.idToken,
184
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
185
+ profile: {
186
+ sub: authUser.profile.sub,
187
+ email: authUser.profile.email,
188
+ nickname: authUser.profile.nickname
189
+ },
190
+ zkEvm: authUser.zkEvm
191
+ };
192
+ const result = await signIn(IMMUTABLE_PROVIDER_ID, {
193
+ tokens: JSON.stringify(tokenData),
194
+ redirect: false
195
+ });
196
+ if (result?.error) {
197
+ throw new Error(`NextAuth sign-in failed: ${result.error}`);
198
+ }
199
+ if (!result?.ok) {
200
+ throw new Error("NextAuth sign-in failed: unknown error");
201
+ }
202
+ } finally {
203
+ setIsLoggingIn(false);
204
+ }
205
+ }, [auth]);
206
+ const handleSignOut = useCallback(async () => {
207
+ if (auth) {
208
+ try {
209
+ await auth.getLogoutUrl();
210
+ } catch (error) {
211
+ console.warn("[auth-next-client] Logout cleanup error:", error);
212
+ await signOut({ redirect: false });
213
+ }
214
+ } else {
215
+ await signOut({ redirect: false });
216
+ }
217
+ }, [auth]);
218
+ const getAccessToken = useCallback(async () => {
219
+ if (auth) {
220
+ try {
221
+ const token = await auth.getAccessToken();
222
+ if (token) {
223
+ return token;
224
+ }
225
+ } catch {
226
+ }
227
+ }
228
+ if (session?.error) {
229
+ throw new Error(
230
+ session.error === "TokenExpired" ? "Session expired. Please log in again." : `Authentication error: ${session.error}`
231
+ );
232
+ }
233
+ if (session?.accessToken) {
234
+ return session.accessToken;
235
+ }
236
+ throw new Error("No access token available");
237
+ }, [auth, session]);
238
+ return {
239
+ user,
240
+ session,
241
+ isLoading,
242
+ isLoggingIn,
243
+ isAuthenticated,
244
+ signIn: handleSignIn,
245
+ signOut: handleSignOut,
246
+ getAccessToken,
247
+ auth
248
+ };
249
+ }
250
+ function useAccessToken() {
251
+ const { getAccessToken } = useImmutableAuth();
252
+ return getAccessToken;
253
+ }
254
+ function useHydratedData(props, fetcher) {
255
+ const { getAccessToken, auth } = useImmutableAuth();
256
+ const {
257
+ ssr,
258
+ data: serverData,
259
+ fetchError
260
+ } = props;
261
+ const needsClientFetch = !ssr || Boolean(fetchError);
262
+ const [data, setData] = useState(serverData);
263
+ const [isLoading, setIsLoading] = useState(needsClientFetch);
264
+ const [error, setError] = useState(
265
+ fetchError ? new Error(fetchError) : null
266
+ );
267
+ const hasFetchedRef = useRef(false);
268
+ const fetchIdRef = useRef(0);
269
+ const prevPropsRef = useRef({ serverData, ssr, fetchError });
270
+ useEffect(() => {
271
+ const prevProps = prevPropsRef.current;
272
+ const propsChanged = prevProps.serverData !== serverData || prevProps.ssr !== ssr || prevProps.fetchError !== fetchError;
273
+ if (propsChanged) {
274
+ prevPropsRef.current = { serverData, ssr, fetchError };
275
+ hasFetchedRef.current = false;
276
+ fetchIdRef.current += 1;
277
+ if (ssr && !fetchError) {
278
+ setData(serverData);
279
+ setIsLoading(false);
280
+ setError(null);
281
+ } else {
282
+ setData(null);
283
+ setIsLoading(true);
284
+ setError(fetchError ? new Error(fetchError) : null);
285
+ }
286
+ }
287
+ }, [serverData, ssr, fetchError]);
288
+ const fetchData = useCallback(async () => {
289
+ const currentFetchId = fetchIdRef.current;
290
+ setIsLoading(true);
291
+ setError(null);
292
+ try {
293
+ const token = await getAccessToken();
294
+ const result = await fetcher(token);
295
+ if (fetchIdRef.current === currentFetchId) {
296
+ setData(result);
297
+ }
298
+ } catch (err) {
299
+ if (fetchIdRef.current === currentFetchId) {
300
+ setError(err instanceof Error ? err : new Error(String(err)));
301
+ }
302
+ } finally {
303
+ if (fetchIdRef.current === currentFetchId) {
304
+ setIsLoading(false);
305
+ }
306
+ }
307
+ }, [fetcher, getAccessToken]);
308
+ useEffect(() => {
309
+ if (hasFetchedRef.current) return;
310
+ if (!needsClientFetch) return;
311
+ if (!ssr && !auth) return;
312
+ hasFetchedRef.current = true;
313
+ fetchData();
314
+ }, [needsClientFetch, ssr, auth, fetchData]);
315
+ return {
316
+ data,
317
+ isLoading,
318
+ error,
319
+ refetch: fetchData
320
+ };
321
+ }
322
+
323
+ // src/callback.tsx
324
+ import { useEffect as useEffect2, useState as useState2, useRef as useRef2 } from "react";
325
+ import { useRouter } from "next/navigation";
326
+ import { signIn as signIn2 } from "next-auth/react";
327
+ import { Auth as Auth2 } from "@imtbl/auth";
328
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
329
+ function getSearchParams() {
330
+ if (typeof window === "undefined") {
331
+ return new URLSearchParams();
332
+ }
333
+ return new URLSearchParams(window.location.search);
334
+ }
335
+ function CallbackPage({
336
+ config,
337
+ redirectTo = "/",
338
+ loadingComponent = null,
339
+ errorComponent,
340
+ onSuccess,
341
+ onError
342
+ }) {
343
+ const router = useRouter();
344
+ const [error, setError] = useState2(null);
345
+ const callbackProcessedRef = useRef2(false);
346
+ useEffect2(() => {
347
+ const searchParams = getSearchParams();
348
+ const handleCallback = async () => {
349
+ try {
350
+ const auth = new Auth2({
351
+ clientId: config.clientId,
352
+ redirectUri: config.redirectUri,
353
+ popupRedirectUri: config.popupRedirectUri,
354
+ logoutRedirectUri: config.logoutRedirectUri,
355
+ audience: config.audience || DEFAULT_AUDIENCE,
356
+ scope: config.scope || DEFAULT_SCOPE,
357
+ authenticationDomain: config.authenticationDomain || DEFAULT_AUTH_DOMAIN,
358
+ passportDomain: config.passportDomain
359
+ });
360
+ const authUser = await auth.loginCallback();
361
+ if (window.opener) {
362
+ if (!authUser) {
363
+ throw new Error("Authentication failed: no user data received from login callback");
364
+ }
365
+ const user = {
366
+ sub: authUser.profile.sub,
367
+ email: authUser.profile.email,
368
+ nickname: authUser.profile.nickname
369
+ };
370
+ if (onSuccess) {
371
+ await onSuccess(user);
372
+ }
373
+ window.close();
374
+ } else if (authUser) {
375
+ const tokenData = {
376
+ accessToken: authUser.accessToken,
377
+ refreshToken: authUser.refreshToken,
378
+ idToken: authUser.idToken,
379
+ accessTokenExpires: getTokenExpiry(authUser.accessToken),
380
+ profile: {
381
+ sub: authUser.profile.sub,
382
+ email: authUser.profile.email,
383
+ nickname: authUser.profile.nickname
384
+ },
385
+ zkEvm: authUser.zkEvm
386
+ };
387
+ const result = await signIn2(IMMUTABLE_PROVIDER_ID, {
388
+ tokens: JSON.stringify(tokenData),
389
+ redirect: false
390
+ });
391
+ if (result?.error) {
392
+ throw new Error(`NextAuth sign-in failed: ${result.error}`);
393
+ }
394
+ if (!result?.ok) {
395
+ throw new Error("NextAuth sign-in failed: unknown error");
396
+ }
397
+ const user = {
398
+ sub: authUser.profile.sub,
399
+ email: authUser.profile.email,
400
+ nickname: authUser.profile.nickname
401
+ };
402
+ if (onSuccess) {
403
+ await onSuccess(user);
404
+ }
405
+ const resolvedRedirectTo = typeof redirectTo === "function" ? redirectTo(user) || "/" : redirectTo;
406
+ router.replace(resolvedRedirectTo);
407
+ } else {
408
+ throw new Error("Authentication failed: no user data received from login callback");
409
+ }
410
+ } catch (err) {
411
+ const errorMessage2 = err instanceof Error ? err.message : "Authentication failed";
412
+ if (onError) {
413
+ onError(errorMessage2);
414
+ }
415
+ setError(errorMessage2);
416
+ }
417
+ };
418
+ const handleOAuthError = () => {
419
+ const errorCode = searchParams.get("error");
420
+ const errorDescription = searchParams.get("error_description");
421
+ const errorMessage2 = errorDescription || errorCode || "Authentication failed";
422
+ if (onError) {
423
+ onError(errorMessage2);
424
+ }
425
+ setError(errorMessage2);
426
+ };
427
+ if (callbackProcessedRef.current) {
428
+ return;
429
+ }
430
+ const hasError = searchParams.get("error");
431
+ const hasCode = searchParams.get("code");
432
+ if (hasError) {
433
+ callbackProcessedRef.current = true;
434
+ handleOAuthError();
435
+ return;
436
+ }
437
+ if (hasCode) {
438
+ callbackProcessedRef.current = true;
439
+ handleCallback();
440
+ return;
441
+ }
442
+ callbackProcessedRef.current = true;
443
+ const errorMessage = "Invalid callback: missing OAuth parameters. Please try logging in again.";
444
+ if (onError) {
445
+ onError(errorMessage);
446
+ }
447
+ setError(errorMessage);
448
+ }, [router, config, redirectTo, onSuccess, onError]);
449
+ if (error) {
450
+ if (errorComponent) {
451
+ return errorComponent(error);
452
+ }
453
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem", textAlign: "center" }, children: [
454
+ /* @__PURE__ */ jsx2("h2", { style: { color: "#dc3545" }, children: "Authentication Error" }),
455
+ /* @__PURE__ */ jsx2("p", { children: error }),
456
+ /* @__PURE__ */ jsx2(
457
+ "button",
458
+ {
459
+ onClick: () => router.push("/"),
460
+ type: "button",
461
+ style: {
462
+ padding: "0.5rem 1rem",
463
+ marginTop: "1rem",
464
+ cursor: "pointer"
465
+ },
466
+ children: "Return to Home"
467
+ }
468
+ )
469
+ ] });
470
+ }
471
+ if (loadingComponent) {
472
+ return loadingComponent;
473
+ }
474
+ return /* @__PURE__ */ jsx2("div", { style: { padding: "2rem", textAlign: "center" }, children: /* @__PURE__ */ jsx2("p", { children: "Completing authentication..." }) });
475
+ }
476
+
477
+ // src/index.ts
478
+ import { MarketingConsentStatus } from "@imtbl/auth";
479
+ export {
480
+ CallbackPage,
481
+ ImmutableAuthProvider,
482
+ MarketingConsentStatus,
483
+ useAccessToken,
484
+ useHydratedData,
485
+ useImmutableAuth
486
+ };
@@ -0,0 +1,66 @@
1
+ import type { Session } from 'next-auth';
2
+ import type { ImmutableAuthProviderProps, UseImmutableAuthReturn } from './types';
3
+ /**
4
+ * Provider component for Immutable authentication with Auth.js v5
5
+ *
6
+ * Wraps your app to provide authentication state via useImmutableAuth hook.
7
+ *
8
+ * @example App Router (recommended)
9
+ * ```tsx
10
+ * // app/providers.tsx
11
+ * "use client";
12
+ * import { ImmutableAuthProvider } from "@imtbl/auth-next-client";
13
+ *
14
+ * const config = {
15
+ * clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
16
+ * redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
17
+ * };
18
+ *
19
+ * export function Providers({ children }: { children: React.ReactNode }) {
20
+ * return (
21
+ * <ImmutableAuthProvider config={config}>
22
+ * {children}
23
+ * </ImmutableAuthProvider>
24
+ * );
25
+ * }
26
+ * ```
27
+ */
28
+ export declare function ImmutableAuthProvider({ children, config, session, basePath, }: ImmutableAuthProviderProps): import("react/jsx-runtime").JSX.Element;
29
+ /**
30
+ * Hook to access Immutable authentication state and methods
31
+ *
32
+ * Must be used within an ImmutableAuthProvider.
33
+ */
34
+ export declare function useImmutableAuth(): UseImmutableAuthReturn;
35
+ /**
36
+ * Hook to get a function that returns a valid access token
37
+ */
38
+ export declare function useAccessToken(): () => Promise<string>;
39
+ /**
40
+ * Result from useHydratedData hook
41
+ */
42
+ export interface UseHydratedDataResult<T> {
43
+ data: T | null;
44
+ isLoading: boolean;
45
+ error: Error | null;
46
+ refetch: () => Promise<void>;
47
+ }
48
+ /**
49
+ * Props for useHydratedData hook - matches AuthPropsWithData from server
50
+ */
51
+ export interface HydratedDataProps<T> {
52
+ session: Session | null;
53
+ ssr: boolean;
54
+ data: T | null;
55
+ fetchError?: string;
56
+ authError?: string;
57
+ }
58
+ /**
59
+ * Hook for hydrating server-fetched data with automatic client-side fallback.
60
+ *
61
+ * This is the recommended pattern for components that receive data from `getAuthenticatedData`:
62
+ * - When `ssr: true` and `data` exists: Uses pre-fetched server data immediately (no loading state)
63
+ * - When `ssr: false`: Refreshes token client-side and fetches data
64
+ * - When `fetchError` exists: Retries fetch client-side
65
+ */
66
+ export declare function useHydratedData<T>(props: HydratedDataProps<T>, fetcher: (accessToken: string) => Promise<T>): UseHydratedDataResult<T>;
@@ -0,0 +1,133 @@
1
+ import type { DefaultSession, Session } from 'next-auth';
2
+ export type { ImmutableAuthConfig, ImmutableTokenData, ZkEvmUser, ImmutableUser, } from '@imtbl/auth-next-server';
3
+ /**
4
+ * zkEVM wallet information
5
+ */
6
+ export interface ZkEvmInfo {
7
+ ethAddress: string;
8
+ userAdminAddress: string;
9
+ }
10
+ /**
11
+ * Auth.js v5 module augmentation to add Immutable-specific fields
12
+ */
13
+ declare module 'next-auth' {
14
+ interface Session extends DefaultSession {
15
+ user: {
16
+ sub: string;
17
+ email?: string;
18
+ nickname?: string;
19
+ } & DefaultSession['user'];
20
+ accessToken: string;
21
+ refreshToken?: string;
22
+ idToken?: string;
23
+ accessTokenExpires: number;
24
+ zkEvm?: ZkEvmInfo;
25
+ error?: string;
26
+ }
27
+ interface User {
28
+ id: string;
29
+ sub: string;
30
+ email?: string | null;
31
+ nickname?: string;
32
+ accessToken: string;
33
+ refreshToken?: string;
34
+ idToken?: string;
35
+ accessTokenExpires: number;
36
+ zkEvm?: ZkEvmInfo;
37
+ }
38
+ }
39
+ /**
40
+ * Props for ImmutableAuthProvider
41
+ */
42
+ export interface ImmutableAuthProviderProps {
43
+ children: React.ReactNode;
44
+ /**
45
+ * Immutable auth configuration
46
+ */
47
+ config: {
48
+ clientId: string;
49
+ redirectUri: string;
50
+ popupRedirectUri?: string;
51
+ logoutRedirectUri?: string;
52
+ audience?: string;
53
+ scope?: string;
54
+ authenticationDomain?: string;
55
+ passportDomain?: string;
56
+ };
57
+ /**
58
+ * Initial session from server (for SSR hydration)
59
+ * Can be Session from auth() or any compatible session object
60
+ */
61
+ session?: Session | DefaultSession | null;
62
+ /**
63
+ * Custom base path for Auth.js API routes
64
+ * Use this when you have multiple auth endpoints (e.g., per environment)
65
+ * @default "/api/auth"
66
+ */
67
+ basePath?: string;
68
+ }
69
+ /**
70
+ * User profile from Immutable (local definition for client)
71
+ */
72
+ export interface ImmutableUserClient {
73
+ sub: string;
74
+ email?: string;
75
+ nickname?: string;
76
+ }
77
+ /**
78
+ * Token data passed from client to Auth.js credentials provider
79
+ */
80
+ export interface ImmutableTokenDataClient {
81
+ accessToken: string;
82
+ refreshToken?: string;
83
+ idToken?: string;
84
+ accessTokenExpires: number;
85
+ profile: {
86
+ sub: string;
87
+ email?: string;
88
+ nickname?: string;
89
+ };
90
+ zkEvm?: ZkEvmInfo;
91
+ }
92
+ /**
93
+ * Return type of useImmutableAuth hook
94
+ */
95
+ export interface UseImmutableAuthReturn {
96
+ /**
97
+ * Current user profile (null if not authenticated)
98
+ */
99
+ user: ImmutableUserClient | null;
100
+ /**
101
+ * Full Auth.js session with tokens
102
+ */
103
+ session: Session | null;
104
+ /**
105
+ * Whether authentication state is loading (initial session fetch)
106
+ */
107
+ isLoading: boolean;
108
+ /**
109
+ * Whether a login flow is in progress (popup open, waiting for OAuth callback)
110
+ */
111
+ isLoggingIn: boolean;
112
+ /**
113
+ * Whether user is authenticated
114
+ */
115
+ isAuthenticated: boolean;
116
+ /**
117
+ * Sign in with Immutable (opens popup)
118
+ * @param options - Optional login options (cached session, silent login, redirect flow, direct login)
119
+ */
120
+ signIn: (options?: import('@imtbl/auth').LoginOptions) => Promise<void>;
121
+ /**
122
+ * Sign out from both Auth.js and Immutable
123
+ */
124
+ signOut: () => Promise<void>;
125
+ /**
126
+ * Get a valid access token (refreshes if needed)
127
+ */
128
+ getAccessToken: () => Promise<string>;
129
+ /**
130
+ * The underlying Auth instance (for advanced use)
131
+ */
132
+ auth: import('@imtbl/auth').Auth | null;
133
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Extract the expiry timestamp from a JWT access token.
3
+ * Returns the expiry as a Unix timestamp in milliseconds.
4
+ *
5
+ * @param accessToken - JWT access token
6
+ * @returns Expiry timestamp in milliseconds, or a default 15-minute expiry if extraction fails
7
+ */
8
+ export declare function getTokenExpiry(accessToken: string | undefined): number;
package/jest.config.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { Config } from 'jest';
2
+
3
+ const config: Config = {
4
+ clearMocks: true,
5
+ coverageProvider: 'v8',
6
+ moduleDirectories: ['node_modules', 'src'],
7
+ testEnvironment: 'jsdom',
8
+ transform: {
9
+ '^.+\\.(t|j)sx?$': '@swc/jest',
10
+ },
11
+ transformIgnorePatterns: [],
12
+ restoreMocks: true,
13
+ roots: ['<rootDir>/src'],
14
+ };
15
+
16
+ export default config;