@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/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This package provides minimal client-side utilities for Next.js applications usi
|
|
|
10
10
|
|
|
11
11
|
- `useLogin` - Hook for login flows with state management (loading, error)
|
|
12
12
|
- `useLogout` - Hook for logout with federated logout support (clears both local and upstream sessions)
|
|
13
|
-
- `useImmutableSession` - Hook that provides session state and
|
|
13
|
+
- `useImmutableSession` - Hook that provides session state, `getAccessToken()` for guaranteed-fresh tokens, and `getUser` for wallet integration
|
|
14
14
|
- `CallbackPage` - OAuth callback handler component
|
|
15
15
|
|
|
16
16
|
For server-side utilities, use [`@imtbl/auth-next-server`](../auth-next-server).
|
|
@@ -27,6 +27,10 @@ npm install @imtbl/auth-next-client @imtbl/auth-next-server next-auth@5
|
|
|
27
27
|
- `next` >= 14.0.0
|
|
28
28
|
- `next-auth` >= 5.0.0-beta.25
|
|
29
29
|
|
|
30
|
+
### Next.js 14 Compatibility
|
|
31
|
+
|
|
32
|
+
This package is compatible with both Next.js 14 and 15. It uses only standard APIs available in both versions (`next/navigation` for `useRouter`, `next-auth/react`). No Next.js 15-only APIs are used.
|
|
33
|
+
|
|
30
34
|
## Quick Start
|
|
31
35
|
|
|
32
36
|
### 1. Set Up Server-Side Auth
|
|
@@ -100,6 +104,42 @@ export default function Callback() {
|
|
|
100
104
|
}
|
|
101
105
|
```
|
|
102
106
|
|
|
107
|
+
### Default Auth (Zero Config)
|
|
108
|
+
|
|
109
|
+
When using `createAuthConfig()` with no args on the server, you can call login/logout with no config—sandbox clientId and redirectUri are used:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// With default auth - no config needed
|
|
113
|
+
function LoginButton() {
|
|
114
|
+
const { isAuthenticated } = useImmutableSession();
|
|
115
|
+
const { loginWithPopup, isLoggingIn, error } = useLogin();
|
|
116
|
+
|
|
117
|
+
if (isAuthenticated) return <p>You are logged in!</p>;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<button onClick={() => loginWithPopup()} disabled={isLoggingIn}>
|
|
121
|
+
{isLoggingIn ? "Signing in..." : "Sign In"}
|
|
122
|
+
</button>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or with custom config (pass full LoginConfig/LogoutConfig when overriding):
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// With custom config - pass complete config
|
|
131
|
+
loginWithPopup({
|
|
132
|
+
clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
|
|
133
|
+
redirectUri: `${window.location.origin}/callback`,
|
|
134
|
+
});
|
|
135
|
+
logout({
|
|
136
|
+
clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
|
|
137
|
+
logoutRedirectUri: process.env.NEXT_PUBLIC_BASE_URL!,
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
See the [wallets-connect-with-nextjs](../../examples/passport/wallets-connect-with-nextjs) example for a full integration with `@imtbl/wallet`.
|
|
142
|
+
|
|
103
143
|
### 5. Add Login Button
|
|
104
144
|
|
|
105
145
|
Use the `useLogin` hook for login flows with built-in state management:
|
|
@@ -395,7 +435,11 @@ With federated logout, the auth server's session is also cleared, so users can s
|
|
|
395
435
|
|
|
396
436
|
### `useImmutableSession()`
|
|
397
437
|
|
|
398
|
-
A convenience hook that wraps `next-auth/react`'s `useSession` with
|
|
438
|
+
A convenience hook that wraps `next-auth/react`'s `useSession` with:
|
|
439
|
+
|
|
440
|
+
- `getAccessToken()` -- async function that returns a **guaranteed-fresh** access token
|
|
441
|
+
- `getUser()` -- function for wallet integration
|
|
442
|
+
- Automatic token refresh -- detects expired tokens and refreshes on demand
|
|
399
443
|
|
|
400
444
|
```tsx
|
|
401
445
|
"use client";
|
|
@@ -404,10 +448,12 @@ import { useImmutableSession } from "@imtbl/auth-next-client";
|
|
|
404
448
|
|
|
405
449
|
function MyComponent() {
|
|
406
450
|
const {
|
|
407
|
-
session, // Session
|
|
451
|
+
session, // Session metadata (user info, zkEvm, error) -- does NOT include accessToken
|
|
408
452
|
status, // 'loading' | 'authenticated' | 'unauthenticated'
|
|
409
453
|
isLoading, // True during initial load
|
|
410
454
|
isAuthenticated, // True when logged in
|
|
455
|
+
isRefreshing, // True during token refresh
|
|
456
|
+
getAccessToken, // Async function: returns a guaranteed-fresh access token
|
|
411
457
|
getUser, // Function for wallet integration
|
|
412
458
|
} = useImmutableSession();
|
|
413
459
|
|
|
@@ -420,13 +466,131 @@ function MyComponent() {
|
|
|
420
466
|
|
|
421
467
|
#### Return Value
|
|
422
468
|
|
|
423
|
-
| Property | Type | Description
|
|
424
|
-
| ----------------- | --------------------------------------------------- |
|
|
425
|
-
| `session` | `ImmutableSession \| null` | Session
|
|
426
|
-
| `status` | `string` | Auth status: `'loading'`, `'authenticated'`, `'unauthenticated'`
|
|
427
|
-
| `isLoading` | `boolean` | Whether initial auth state is loading
|
|
428
|
-
| `isAuthenticated` | `boolean` | Whether user is authenticated
|
|
429
|
-
| `
|
|
469
|
+
| Property | Type | Description |
|
|
470
|
+
| ----------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
471
|
+
| `session` | `ImmutableSession \| null` | Session metadata (user, zkEvm, error). Does **not** include `accessToken` -- see below. |
|
|
472
|
+
| `status` | `string` | Auth status: `'loading'`, `'authenticated'`, `'unauthenticated'` |
|
|
473
|
+
| `isLoading` | `boolean` | Whether initial auth state is loading |
|
|
474
|
+
| `isAuthenticated` | `boolean` | Whether user is authenticated |
|
|
475
|
+
| `isRefreshing` | `boolean` | Whether a token refresh is in progress |
|
|
476
|
+
| `getAccessToken` | `() => Promise<string>` | Get a guaranteed-fresh access token. Throws if not authenticated or refresh fails. |
|
|
477
|
+
| `getUser` | `(forceRefresh?: boolean) => Promise<User \| null>` | Get user function for wallet integration |
|
|
478
|
+
|
|
479
|
+
#### Why no `accessToken` on `session`?
|
|
480
|
+
|
|
481
|
+
The `session` object intentionally does **not** expose `accessToken`. This is a deliberate design choice to prevent consumers from accidentally using a stale/expired token.
|
|
482
|
+
|
|
483
|
+
**Always use `getAccessToken()`** to obtain a token for authenticated requests:
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
// ✅ Correct - always fresh
|
|
487
|
+
const token = await getAccessToken();
|
|
488
|
+
await authenticatedGet("/api/data", token);
|
|
489
|
+
|
|
490
|
+
// ❌ Incorrect - session.accessToken does not exist on the type
|
|
491
|
+
const token = session?.accessToken; // TypeScript error
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`getAccessToken()` guarantees freshness:
|
|
495
|
+
|
|
496
|
+
- **Fast path**: If the current token is valid, returns immediately (no network call).
|
|
497
|
+
- **Slow path**: If the token is expired, triggers a server-side refresh and **blocks** (awaits) until the fresh token is available.
|
|
498
|
+
- **Deduplication**: Multiple concurrent calls share a single refresh request.
|
|
499
|
+
|
|
500
|
+
#### Checking Authentication Status
|
|
501
|
+
|
|
502
|
+
**Always use `isAuthenticated` to determine if a user is logged in.** Do not use the `session` object or `status` field directly for this purpose.
|
|
503
|
+
|
|
504
|
+
**Why not `!!session`?**
|
|
505
|
+
|
|
506
|
+
A `session` object can exist but be **unusable**. For example, the session may be present but the access token is missing, or a token refresh may have failed (indicated by `session.error === "RefreshTokenError"`). Checking `!!session` would incorrectly treat these broken sessions as authenticated.
|
|
507
|
+
|
|
508
|
+
**Why not `status === 'authenticated'`?**
|
|
509
|
+
|
|
510
|
+
The `status` field comes directly from NextAuth's `useSession` and only reflects whether NextAuth considers the session valid at the cookie/JWT level. It does **not** account for whether the access token is actually present or whether a token refresh has failed. A session can have `status === 'authenticated'` while `session.error` is set to `"RefreshTokenError"`, meaning the tokens are no longer usable.
|
|
511
|
+
|
|
512
|
+
**What `isAuthenticated` checks:**
|
|
513
|
+
|
|
514
|
+
The `isAuthenticated` boolean validates all of the following:
|
|
515
|
+
|
|
516
|
+
1. NextAuth reports `'authenticated'` status
|
|
517
|
+
2. The session object exists
|
|
518
|
+
3. A valid access token is present in the session
|
|
519
|
+
4. No session-level error exists (e.g., `RefreshTokenError`)
|
|
520
|
+
|
|
521
|
+
It also handles transient states gracefully — during session refetches (e.g., window focus) or manual refreshes (e.g., after wallet registration via `getUser(true)`), `isAuthenticated` remains `true` if the user was previously authenticated, preventing UI flicker.
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
// ✅ Correct - uses isAuthenticated
|
|
525
|
+
const { isAuthenticated } = useImmutableSession();
|
|
526
|
+
if (!isAuthenticated) return <div>Please log in</div>;
|
|
527
|
+
|
|
528
|
+
// ❌ Incorrect - session can exist with expired/invalid tokens
|
|
529
|
+
const { session } = useImmutableSession();
|
|
530
|
+
if (!session) return <div>Please log in</div>;
|
|
531
|
+
|
|
532
|
+
// ❌ Incorrect - status doesn't account for token errors
|
|
533
|
+
const { status } = useImmutableSession();
|
|
534
|
+
if (status !== "authenticated") return <div>Please log in</div>;
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
#### Using `getAccessToken()` in Practice
|
|
538
|
+
|
|
539
|
+
**SWR fetcher:**
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
import useSWR from "swr";
|
|
543
|
+
import { useImmutableSession } from "@imtbl/auth-next-client";
|
|
544
|
+
|
|
545
|
+
function useProfile() {
|
|
546
|
+
const { getAccessToken, isAuthenticated } = useImmutableSession();
|
|
547
|
+
|
|
548
|
+
return useSWR(
|
|
549
|
+
isAuthenticated ? "/passport-profile/v1/profile" : null,
|
|
550
|
+
async (path) => {
|
|
551
|
+
const token = await getAccessToken(); // blocks until fresh
|
|
552
|
+
return authenticatedGet(path, token);
|
|
553
|
+
},
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Event handler:**
|
|
559
|
+
|
|
560
|
+
```tsx
|
|
561
|
+
import { useImmutableSession } from "@imtbl/auth-next-client";
|
|
562
|
+
|
|
563
|
+
function ClaimRewardButton({ questId }: { questId: string }) {
|
|
564
|
+
const { getAccessToken } = useImmutableSession();
|
|
565
|
+
|
|
566
|
+
const handleClaim = async () => {
|
|
567
|
+
const token = await getAccessToken(); // blocks until fresh
|
|
568
|
+
await authenticatedPost("/v1/quests/claim", token, { questId });
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
return <button onClick={handleClaim}>Claim</button>;
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Periodic polling:**
|
|
576
|
+
|
|
577
|
+
```tsx
|
|
578
|
+
import useSWR from "swr";
|
|
579
|
+
import { useImmutableSession } from "@imtbl/auth-next-client";
|
|
580
|
+
|
|
581
|
+
function ActivityFeed() {
|
|
582
|
+
const { getAccessToken, isAuthenticated } = useImmutableSession();
|
|
583
|
+
|
|
584
|
+
return useSWR(
|
|
585
|
+
isAuthenticated ? "/v1/activities" : null,
|
|
586
|
+
async (path) => {
|
|
587
|
+
const token = await getAccessToken();
|
|
588
|
+
return authenticatedGet(path, token);
|
|
589
|
+
},
|
|
590
|
+
{ refreshInterval: 10000 }, // polls every 10s, always gets a fresh token
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
```
|
|
430
594
|
|
|
431
595
|
#### The `getUser` Function
|
|
432
596
|
|
|
@@ -452,13 +616,13 @@ When `forceRefresh` is `true`:
|
|
|
452
616
|
|
|
453
617
|
### ImmutableSession
|
|
454
618
|
|
|
455
|
-
The session type returned by `useImmutableSession
|
|
619
|
+
The session type returned by `useImmutableSession`. Note that `accessToken` is intentionally **not** included -- use `getAccessToken()` instead to obtain a guaranteed-fresh token.
|
|
456
620
|
|
|
457
621
|
```typescript
|
|
458
622
|
interface ImmutableSession {
|
|
459
|
-
accessToken
|
|
623
|
+
// accessToken is NOT exposed -- use getAccessToken() instead
|
|
460
624
|
refreshToken?: string;
|
|
461
|
-
idToken?: string;
|
|
625
|
+
idToken?: string; // Only present transiently after sign-in or token refresh (not stored in cookie)
|
|
462
626
|
accessTokenExpires: number;
|
|
463
627
|
zkEvm?: {
|
|
464
628
|
ethAddress: string;
|
|
@@ -473,6 +637,8 @@ interface ImmutableSession {
|
|
|
473
637
|
}
|
|
474
638
|
```
|
|
475
639
|
|
|
640
|
+
> **Note:** The `idToken` is **not** stored in the session cookie (to avoid CloudFront 413 errors from oversized headers). It is only present in the session response transiently after sign-in or token refresh. `@imtbl/auth-next-client` automatically persists it in `localStorage` so that `getUser()` always returns a valid `idToken` for wallet operations. All data extracted from the idToken (`email`, `nickname`, `zkEvm`) remains in the cookie as separate fields and is always available in the session.
|
|
641
|
+
|
|
476
642
|
### LoginConfig
|
|
477
643
|
|
|
478
644
|
Configuration for the `useLogin` hook's login functions:
|
|
@@ -531,17 +697,19 @@ interface LogoutConfig {
|
|
|
531
697
|
|
|
532
698
|
The session may contain an `error` field indicating authentication issues:
|
|
533
699
|
|
|
534
|
-
| Error | Description | Handling
|
|
535
|
-
| --------------------- | --------------------- |
|
|
536
|
-
| `"TokenExpired"` | Access token expired |
|
|
537
|
-
| `"RefreshTokenError"` | Refresh token invalid | Prompt user to sign in again
|
|
700
|
+
| Error | Description | Handling |
|
|
701
|
+
| --------------------- | --------------------- | -------------------------------------------- |
|
|
702
|
+
| `"TokenExpired"` | Access token expired | Proactive refresh handles this automatically |
|
|
703
|
+
| `"RefreshTokenError"` | Refresh token invalid | Prompt user to sign in again |
|
|
704
|
+
|
|
705
|
+
`getAccessToken()` throws an error if the token cannot be obtained (e.g., refresh failure). Handle it with try/catch:
|
|
538
706
|
|
|
539
707
|
```tsx
|
|
540
708
|
import { useImmutableSession } from "@imtbl/auth-next-client";
|
|
541
|
-
import {
|
|
709
|
+
import { signOut } from "next-auth/react";
|
|
542
710
|
|
|
543
711
|
function ProtectedContent() {
|
|
544
|
-
const { session, isAuthenticated } = useImmutableSession();
|
|
712
|
+
const { session, isAuthenticated, getAccessToken } = useImmutableSession();
|
|
545
713
|
|
|
546
714
|
if (session?.error === "RefreshTokenError") {
|
|
547
715
|
return (
|
|
@@ -556,11 +724,20 @@ function ProtectedContent() {
|
|
|
556
724
|
return (
|
|
557
725
|
<div>
|
|
558
726
|
<p>Please sign in to continue.</p>
|
|
559
|
-
<button onClick={() => signIn()}>Sign In</button>
|
|
560
727
|
</div>
|
|
561
728
|
);
|
|
562
729
|
}
|
|
563
730
|
|
|
731
|
+
const handleFetch = async () => {
|
|
732
|
+
try {
|
|
733
|
+
const token = await getAccessToken();
|
|
734
|
+
// Use token for authenticated requests
|
|
735
|
+
} catch (error) {
|
|
736
|
+
// Token refresh failed -- session may be expired
|
|
737
|
+
console.error("Failed to get access token:", error);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
564
741
|
return <div>Protected content here</div>;
|
|
565
742
|
}
|
|
566
743
|
```
|
package/dist/node/index.cjs
CHANGED
|
@@ -22,7 +22,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var src_exports = {};
|
|
23
23
|
__export(src_exports, {
|
|
24
24
|
CallbackPage: () => CallbackPage,
|
|
25
|
+
DEFAULT_AUDIENCE: () => DEFAULT_AUDIENCE,
|
|
26
|
+
DEFAULT_AUTH_DOMAIN: () => DEFAULT_AUTH_DOMAIN,
|
|
27
|
+
DEFAULT_LOGOUT_REDIRECT_URI_PATH: () => DEFAULT_LOGOUT_REDIRECT_URI_PATH,
|
|
28
|
+
DEFAULT_REDIRECT_URI_PATH: () => DEFAULT_REDIRECT_URI_PATH,
|
|
29
|
+
DEFAULT_SANDBOX_CLIENT_ID: () => DEFAULT_SANDBOX_CLIENT_ID,
|
|
30
|
+
DEFAULT_SCOPE: () => DEFAULT_SCOPE,
|
|
31
|
+
IMMUTABLE_PROVIDER_ID: () => IMMUTABLE_PROVIDER_ID,
|
|
25
32
|
MarketingConsentStatus: () => import_auth3.MarketingConsentStatus,
|
|
33
|
+
deriveDefaultRedirectUri: () => deriveDefaultRedirectUri,
|
|
26
34
|
useImmutableSession: () => useImmutableSession,
|
|
27
35
|
useLogin: () => useLogin,
|
|
28
36
|
useLogout: () => useLogout
|
|
@@ -36,9 +44,42 @@ var import_react2 = require("next-auth/react");
|
|
|
36
44
|
var import_auth = require("@imtbl/auth");
|
|
37
45
|
|
|
38
46
|
// src/constants.ts
|
|
47
|
+
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
48
|
+
var DEFAULT_AUDIENCE = "platform_api";
|
|
49
|
+
var DEFAULT_SCOPE = "openid profile email offline_access transact";
|
|
39
50
|
var IMMUTABLE_PROVIDER_ID = "immutable";
|
|
40
|
-
var
|
|
41
|
-
var
|
|
51
|
+
var DEFAULT_SANDBOX_CLIENT_ID = "mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo";
|
|
52
|
+
var DEFAULT_REDIRECT_URI_PATH = "/callback";
|
|
53
|
+
var DEFAULT_LOGOUT_REDIRECT_URI_PATH = "/";
|
|
54
|
+
var TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
55
|
+
|
|
56
|
+
// src/idTokenStorage.ts
|
|
57
|
+
var ID_TOKEN_STORAGE_KEY = "imtbl_id_token";
|
|
58
|
+
function storeIdToken(idToken) {
|
|
59
|
+
try {
|
|
60
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
61
|
+
window.localStorage.setItem(ID_TOKEN_STORAGE_KEY, idToken);
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function getStoredIdToken() {
|
|
67
|
+
try {
|
|
68
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
69
|
+
return window.localStorage.getItem(ID_TOKEN_STORAGE_KEY) ?? void 0;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
function clearStoredIdToken() {
|
|
76
|
+
try {
|
|
77
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
78
|
+
window.localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
42
83
|
|
|
43
84
|
// src/callback.tsx
|
|
44
85
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -89,6 +130,9 @@ function CallbackPage({
|
|
|
89
130
|
window.close();
|
|
90
131
|
} else {
|
|
91
132
|
const tokenData = mapTokensToSignInData(tokens);
|
|
133
|
+
if (tokens.idToken) {
|
|
134
|
+
storeIdToken(tokens.idToken);
|
|
135
|
+
}
|
|
92
136
|
const result = await (0, import_react2.signIn)(IMMUTABLE_PROVIDER_ID, {
|
|
93
137
|
tokens: JSON.stringify(tokenData),
|
|
94
138
|
redirect: false
|
|
@@ -181,6 +225,51 @@ function CallbackPage({
|
|
|
181
225
|
var import_react3 = require("react");
|
|
182
226
|
var import_react4 = require("next-auth/react");
|
|
183
227
|
var import_auth2 = require("@imtbl/auth");
|
|
228
|
+
|
|
229
|
+
// src/defaultConfig.ts
|
|
230
|
+
function deriveDefaultRedirectUri() {
|
|
231
|
+
if (typeof window === "undefined") {
|
|
232
|
+
throw new Error(
|
|
233
|
+
"[auth-next-client] deriveDefaultRedirectUri requires window. Login hooks run in the browser when the user triggers login."
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return `${window.location.origin}${DEFAULT_REDIRECT_URI_PATH}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/hooks.tsx
|
|
240
|
+
var pendingRefresh = null;
|
|
241
|
+
function deduplicatedUpdate(update) {
|
|
242
|
+
if (!pendingRefresh) {
|
|
243
|
+
pendingRefresh = update().finally(() => {
|
|
244
|
+
pendingRefresh = null;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return pendingRefresh;
|
|
248
|
+
}
|
|
249
|
+
function getSandboxLoginConfig() {
|
|
250
|
+
const redirectUri = deriveDefaultRedirectUri();
|
|
251
|
+
return {
|
|
252
|
+
clientId: DEFAULT_SANDBOX_CLIENT_ID,
|
|
253
|
+
redirectUri,
|
|
254
|
+
popupRedirectUri: redirectUri,
|
|
255
|
+
scope: DEFAULT_SCOPE,
|
|
256
|
+
audience: DEFAULT_AUDIENCE,
|
|
257
|
+
authenticationDomain: DEFAULT_AUTH_DOMAIN
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function getSandboxLogoutConfig() {
|
|
261
|
+
if (typeof window === "undefined") {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"[auth-next-client] getSandboxLogoutConfig requires window. Logout runs in the browser when the user triggers it."
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const logoutRedirectUri = window.location.origin + DEFAULT_LOGOUT_REDIRECT_URI_PATH;
|
|
267
|
+
return {
|
|
268
|
+
clientId: DEFAULT_SANDBOX_CLIENT_ID,
|
|
269
|
+
logoutRedirectUri,
|
|
270
|
+
authenticationDomain: DEFAULT_AUTH_DOMAIN
|
|
271
|
+
};
|
|
272
|
+
}
|
|
184
273
|
function useImmutableSession() {
|
|
185
274
|
const { data: sessionData, status, update } = (0, import_react4.useSession)();
|
|
186
275
|
const [isRefreshing, setIsRefreshing] = (0, import_react3.useState)(false);
|
|
@@ -197,6 +286,18 @@ function useImmutableSession() {
|
|
|
197
286
|
updateRef.current = update;
|
|
198
287
|
const setIsRefreshingRef = (0, import_react3.useRef)(setIsRefreshing);
|
|
199
288
|
setIsRefreshingRef.current = setIsRefreshing;
|
|
289
|
+
(0, import_react3.useEffect)(() => {
|
|
290
|
+
if (!session?.accessTokenExpires) return;
|
|
291
|
+
const timeUntilExpiry = session.accessTokenExpires - Date.now() - TOKEN_EXPIRY_BUFFER_MS;
|
|
292
|
+
if (timeUntilExpiry <= 0) {
|
|
293
|
+
deduplicatedUpdate(() => updateRef.current());
|
|
294
|
+
}
|
|
295
|
+
}, [session?.accessTokenExpires]);
|
|
296
|
+
(0, import_react3.useEffect)(() => {
|
|
297
|
+
if (session?.idToken) {
|
|
298
|
+
storeIdToken(session.idToken);
|
|
299
|
+
}
|
|
300
|
+
}, [session?.idToken]);
|
|
200
301
|
const getUser = (0, import_react3.useCallback)(async (forceRefresh) => {
|
|
201
302
|
let currentSession;
|
|
202
303
|
if (forceRefresh) {
|
|
@@ -206,6 +307,9 @@ function useImmutableSession() {
|
|
|
206
307
|
currentSession = updatedSession;
|
|
207
308
|
if (currentSession) {
|
|
208
309
|
sessionRef.current = currentSession;
|
|
310
|
+
if (currentSession.idToken) {
|
|
311
|
+
storeIdToken(currentSession.idToken);
|
|
312
|
+
}
|
|
209
313
|
}
|
|
210
314
|
} catch (error) {
|
|
211
315
|
console.error("[auth-next-client] Force refresh failed:", error);
|
|
@@ -213,6 +317,17 @@ function useImmutableSession() {
|
|
|
213
317
|
} finally {
|
|
214
318
|
setIsRefreshingRef.current(false);
|
|
215
319
|
}
|
|
320
|
+
} else if (pendingRefresh) {
|
|
321
|
+
const refreshed = await pendingRefresh;
|
|
322
|
+
if (refreshed) {
|
|
323
|
+
currentSession = refreshed;
|
|
324
|
+
sessionRef.current = currentSession;
|
|
325
|
+
if (currentSession.idToken) {
|
|
326
|
+
storeIdToken(currentSession.idToken);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
currentSession = sessionRef.current;
|
|
330
|
+
}
|
|
216
331
|
} else {
|
|
217
332
|
currentSession = sessionRef.current;
|
|
218
333
|
}
|
|
@@ -226,7 +341,9 @@ function useImmutableSession() {
|
|
|
226
341
|
return {
|
|
227
342
|
accessToken: currentSession.accessToken,
|
|
228
343
|
refreshToken: currentSession.refreshToken,
|
|
229
|
-
idToken
|
|
344
|
+
// Prefer session idToken (fresh after sign-in or refresh, before useEffect
|
|
345
|
+
// stores it), fall back to localStorage for normal reads (cookie has no idToken).
|
|
346
|
+
idToken: currentSession.idToken || getStoredIdToken(),
|
|
230
347
|
profile: {
|
|
231
348
|
sub: currentSession.user?.sub ?? "",
|
|
232
349
|
email: currentSession.user?.email ?? void 0,
|
|
@@ -235,19 +352,40 @@ function useImmutableSession() {
|
|
|
235
352
|
zkEvm: currentSession.zkEvm
|
|
236
353
|
};
|
|
237
354
|
}, []);
|
|
355
|
+
const getAccessToken = (0, import_react3.useCallback)(async () => {
|
|
356
|
+
const currentSession = sessionRef.current;
|
|
357
|
+
if (currentSession?.accessToken && currentSession.accessTokenExpires && Date.now() < currentSession.accessTokenExpires - TOKEN_EXPIRY_BUFFER_MS && !currentSession.error) {
|
|
358
|
+
return currentSession.accessToken;
|
|
359
|
+
}
|
|
360
|
+
const refreshed = await deduplicatedUpdate(
|
|
361
|
+
() => updateRef.current()
|
|
362
|
+
);
|
|
363
|
+
if (!refreshed?.accessToken || refreshed.error) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`[auth-next-client] Failed to get access token: ${refreshed?.error || "no session"}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
sessionRef.current = refreshed;
|
|
369
|
+
return refreshed.accessToken;
|
|
370
|
+
}, []);
|
|
371
|
+
const publicSession = session;
|
|
238
372
|
return {
|
|
239
|
-
session,
|
|
373
|
+
session: publicSession,
|
|
240
374
|
status,
|
|
241
375
|
isLoading,
|
|
242
376
|
isAuthenticated,
|
|
243
377
|
isRefreshing,
|
|
244
|
-
getUser
|
|
378
|
+
getUser,
|
|
379
|
+
getAccessToken
|
|
245
380
|
};
|
|
246
381
|
}
|
|
247
382
|
function useLogin() {
|
|
248
383
|
const [isLoggingIn, setIsLoggingIn] = (0, import_react3.useState)(false);
|
|
249
384
|
const [error, setError] = (0, import_react3.useState)(null);
|
|
250
385
|
const signInWithTokens = (0, import_react3.useCallback)(async (tokens) => {
|
|
386
|
+
if (tokens.idToken) {
|
|
387
|
+
storeIdToken(tokens.idToken);
|
|
388
|
+
}
|
|
251
389
|
const result = await (0, import_react4.signIn)(IMMUTABLE_PROVIDER_ID, {
|
|
252
390
|
tokens: JSON.stringify(tokens),
|
|
253
391
|
redirect: false
|
|
@@ -263,7 +401,8 @@ function useLogin() {
|
|
|
263
401
|
setIsLoggingIn(true);
|
|
264
402
|
setError(null);
|
|
265
403
|
try {
|
|
266
|
-
const
|
|
404
|
+
const fullConfig = config ?? getSandboxLoginConfig();
|
|
405
|
+
const tokens = await (0, import_auth2.loginWithPopup)(fullConfig, options);
|
|
267
406
|
await signInWithTokens(tokens);
|
|
268
407
|
} catch (err) {
|
|
269
408
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
@@ -277,7 +416,8 @@ function useLogin() {
|
|
|
277
416
|
setIsLoggingIn(true);
|
|
278
417
|
setError(null);
|
|
279
418
|
try {
|
|
280
|
-
const
|
|
419
|
+
const fullConfig = config ?? getSandboxLoginConfig();
|
|
420
|
+
const tokens = await (0, import_auth2.loginWithEmbedded)(fullConfig);
|
|
281
421
|
await signInWithTokens(tokens);
|
|
282
422
|
} catch (err) {
|
|
283
423
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
@@ -291,7 +431,8 @@ function useLogin() {
|
|
|
291
431
|
setIsLoggingIn(true);
|
|
292
432
|
setError(null);
|
|
293
433
|
try {
|
|
294
|
-
|
|
434
|
+
const fullConfig = config ?? getSandboxLoginConfig();
|
|
435
|
+
await (0, import_auth2.loginWithRedirect)(fullConfig, options);
|
|
295
436
|
} catch (err) {
|
|
296
437
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
297
438
|
setError(errorMessage);
|
|
@@ -314,8 +455,10 @@ function useLogout() {
|
|
|
314
455
|
setIsLoggingOut(true);
|
|
315
456
|
setError(null);
|
|
316
457
|
try {
|
|
458
|
+
clearStoredIdToken();
|
|
317
459
|
await (0, import_react4.signOut)({ redirect: false });
|
|
318
|
-
|
|
460
|
+
const fullConfig = config ?? getSandboxLogoutConfig();
|
|
461
|
+
(0, import_auth2.logoutWithRedirect)(fullConfig);
|
|
319
462
|
} catch (err) {
|
|
320
463
|
const errorMessage = err instanceof Error ? err.message : "Logout failed";
|
|
321
464
|
setError(errorMessage);
|
|
@@ -335,7 +478,15 @@ var import_auth3 = require("@imtbl/auth");
|
|
|
335
478
|
// Annotate the CommonJS export names for ESM import in node:
|
|
336
479
|
0 && (module.exports = {
|
|
337
480
|
CallbackPage,
|
|
481
|
+
DEFAULT_AUDIENCE,
|
|
482
|
+
DEFAULT_AUTH_DOMAIN,
|
|
483
|
+
DEFAULT_LOGOUT_REDIRECT_URI_PATH,
|
|
484
|
+
DEFAULT_REDIRECT_URI_PATH,
|
|
485
|
+
DEFAULT_SANDBOX_CLIENT_ID,
|
|
486
|
+
DEFAULT_SCOPE,
|
|
487
|
+
IMMUTABLE_PROVIDER_ID,
|
|
338
488
|
MarketingConsentStatus,
|
|
489
|
+
deriveDefaultRedirectUri,
|
|
339
490
|
useImmutableSession,
|
|
340
491
|
useLogin,
|
|
341
492
|
useLogout
|