@imtbl/auth-next-server 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 CHANGED
@@ -7,6 +7,7 @@ Server-side utilities for Immutable authentication with Auth.js v5 (NextAuth) in
7
7
  This package provides server-side authentication utilities for Next.js applications using the App Router. It integrates with Auth.js v5 to handle OAuth authentication with Immutable's identity provider.
8
8
 
9
9
  **Key features:**
10
+
10
11
  - Auth.js v5 configuration for Immutable authentication
11
12
  - Route protection via middleware
12
13
  - Server utilities for authenticated data fetching
@@ -29,6 +30,16 @@ yarn add @imtbl/auth-next-server next-auth@5
29
30
  - `next` >= 14.0.0
30
31
  - `next-auth` >= 5.0.0-beta.25
31
32
 
33
+ ### Next.js 14 Compatibility
34
+
35
+ This package is compatible with both Next.js 14 and 15. It uses only standard APIs available in both versions:
36
+
37
+ - `next/server`: `NextRequest`, `NextResponse` (middleware)
38
+ - `next/navigation`: `redirect` (Server Components)
39
+ - NextAuth v5 with App Router
40
+
41
+ No Next.js 15-only APIs are used (e.g. async `headers()`/`cookies()`, `unstable_after`).
42
+
32
43
  ## Quick Start
33
44
 
34
45
  ### 1. Create Auth Configuration
@@ -40,10 +51,12 @@ Create a file to configure Immutable authentication:
40
51
  import NextAuth from "next-auth";
41
52
  import { createAuthConfig } from "@imtbl/auth-next-server";
42
53
 
43
- export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig({
44
- clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
45
- redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
46
- }));
54
+ export const { handlers, auth, signIn, signOut } = NextAuth(
55
+ createAuthConfig({
56
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
57
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
58
+ }),
59
+ );
47
60
  ```
48
61
 
49
62
  ### 2. Set Up API Route
@@ -66,37 +79,75 @@ NEXT_PUBLIC_BASE_URL=http://localhost:3000
66
79
  AUTH_SECRET=your-secret-key-min-32-characters
67
80
  ```
68
81
 
82
+ ## Default Auth (Zero Config)
83
+
84
+ Policy: **provide nothing → full sandbox; provide config → provide everything.**
85
+
86
+ With no configuration, `createAuthConfig()` uses sandbox defaults:
87
+ - `clientId`: sandbox (public Immutable client ID)
88
+ - `redirectUri`: from `window.location.origin + '/callback'` (path only on server)
89
+
90
+ When providing config, pass `clientId` and `redirectUri` (and optionally `audience`, `scope`, `authenticationDomain`) to avoid conflicts.
91
+
92
+ ```typescript
93
+ // lib/auth.ts
94
+ import NextAuth from "next-auth";
95
+ import { createAuthConfig } from "@imtbl/auth-next-server";
96
+
97
+ // Zero config - only AUTH_SECRET required in .env
98
+ export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig());
99
+ ```
100
+
101
+ With partial overrides:
102
+
103
+ ```typescript
104
+ // With config - provide clientId and redirectUri
105
+ export const { handlers, auth, signIn, signOut } = NextAuth(
106
+ createAuthConfig({
107
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
108
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
109
+ }),
110
+ );
111
+ ```
112
+
113
+ > **Note:** Default auth uses public Immutable client IDs. For production apps, use your own client ID from Immutable Hub.
114
+
69
115
  ## Configuration
70
116
 
71
- ### `createAuthConfig(config)`
117
+ ### `createAuthConfig(config?)`
72
118
 
73
- Creates an Auth.js v5 configuration object for Immutable authentication. You pass this to `NextAuth()` to create your auth instance.
119
+ Creates an Auth.js v5 configuration object for Immutable authentication. Config is optional—when omitted, sensible defaults are used. Pass this to `NextAuth()` to create your auth instance.
74
120
 
75
121
  ```typescript
76
122
  import NextAuth from "next-auth";
77
123
  import { createAuthConfig } from "@imtbl/auth-next-server";
78
124
 
79
- const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig({
80
- // Required
81
- clientId: "your-client-id",
82
- redirectUri: "https://your-app.com/callback",
83
-
84
- // Optional
85
- audience: "platform_api", // Default: "platform_api"
86
- scope: "openid profile email offline_access transact", // Default scope
87
- authenticationDomain: "https://auth.immutable.com", // Default domain
88
- }));
125
+ // Zero config
126
+ const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig());
127
+
128
+ // Or with custom config
129
+ const { handlers, auth, signIn, signOut } = NextAuth(
130
+ createAuthConfig({
131
+ clientId: "your-client-id",
132
+ redirectUri: "https://your-app.com/callback",
133
+
134
+ // Optional
135
+ audience: "platform_api", // Default: "platform_api"
136
+ scope: "openid profile email offline_access transact", // Default scope
137
+ authenticationDomain: "https://auth.immutable.com", // Default domain
138
+ }),
139
+ );
89
140
  ```
90
141
 
91
142
  #### Configuration Options
92
143
 
93
- | Option | Type | Required | Description |
94
- |--------|------|----------|-------------|
95
- | `clientId` | `string` | Yes | Your Immutable application client ID |
96
- | `redirectUri` | `string` | Yes | OAuth redirect URI configured in Immutable Hub |
97
- | `audience` | `string` | No | OAuth audience (default: `"platform_api"`) |
98
- | `scope` | `string` | No | OAuth scopes (default: `"openid profile email offline_access transact"`) |
99
- | `authenticationDomain` | `string` | No | Auth domain (default: `"https://auth.immutable.com"`) |
144
+ | Option | Type | Required | Description |
145
+ | ---------------------- | -------- | -------- | ------------------------------------------------------------------------ |
146
+ | `clientId` | `string` | Yes* | Your Immutable application client ID (*required when config is provided) |
147
+ | `redirectUri` | `string` | Yes* | OAuth redirect URI (*required when config is provided) |
148
+ | `audience` | `string` | No | OAuth audience (default: `"platform_api"`) |
149
+ | `scope` | `string` | No | OAuth scopes (default: `"openid profile email offline_access transact"`) |
150
+ | `authenticationDomain` | `string` | No | Auth domain (default: `"https://auth.immutable.com"`) |
100
151
 
101
152
  #### Extending the Configuration
102
153
 
@@ -117,19 +168,20 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
117
168
  secret: process.env.AUTH_SECRET,
118
169
  trustHost: true,
119
170
  basePath: "/api/auth/custom",
120
-
171
+
121
172
  // Extend callbacks (be sure to call the base callbacks first)
122
173
  callbacks: {
123
174
  ...baseConfig.callbacks,
124
175
  async jwt(params) {
125
176
  // Call base jwt callback first
126
- const token = await baseConfig.callbacks?.jwt?.(params) ?? params.token;
177
+ const token = (await baseConfig.callbacks?.jwt?.(params)) ?? params.token;
127
178
  // Add your custom logic
128
179
  return token;
129
180
  },
130
181
  async session(params) {
131
182
  // Call base session callback first
132
- const session = await baseConfig.callbacks?.session?.(params) ?? params.session;
183
+ const session =
184
+ (await baseConfig.callbacks?.session?.(params)) ?? params.session;
133
185
  // Add your custom logic
134
186
  return session;
135
187
  },
@@ -141,18 +193,19 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
141
193
 
142
194
  This package provides several utilities for handling authentication in Server Components. Choose the right one based on your needs:
143
195
 
144
- | Utility | Use Case | Data Fetching | Error Handling |
145
- |---------|----------|---------------|----------------|
146
- | `getAuthProps` | Pass auth state to client, fetch data client-side | No | Manual |
147
- | `getAuthenticatedData` | SSR data fetching with client fallback | Yes | Manual |
148
- | `createProtectedFetchers` | Multiple pages with same error handling | Optional | Centralized |
149
- | `getValidSession` | Custom logic for each auth state | No | Manual (detailed) |
196
+ | Utility | Use Case | Data Fetching | Error Handling |
197
+ | ------------------------- | ------------------------------------------------- | ------------- | ----------------- |
198
+ | `getAuthProps` | Pass auth state to client, fetch data client-side | No | Manual |
199
+ | `getAuthenticatedData` | SSR data fetching with client fallback | Yes | Manual |
200
+ | `createProtectedFetchers` | Multiple pages with same error handling | Optional | Centralized |
201
+ | `getValidSession` | Custom logic for each auth state | No | Manual (detailed) |
150
202
 
151
203
  ### `getAuthProps(auth)`
152
204
 
153
205
  **Use case:** You want to pass authentication state to a Client Component but handle data fetching entirely on the client side. This is the simplest approach when your page doesn't need SSR data fetching.
154
206
 
155
207
  **When to use:**
208
+
156
209
  - Pages where data is fetched client-side (e.g., infinite scroll, real-time updates)
157
210
  - Pages that show a loading skeleton while fetching
158
211
  - When you want full control over loading states in the client
@@ -167,11 +220,11 @@ import { DashboardClient } from "./DashboardClient";
167
220
 
168
221
  export default async function DashboardPage() {
169
222
  const authProps = await getAuthProps(auth);
170
-
223
+
171
224
  if (authProps.authError) {
172
225
  redirect("/login");
173
226
  }
174
-
227
+
175
228
  // DashboardClient will fetch its own data using useImmutableSession().getUser()
176
229
  return <DashboardClient {...authProps} />;
177
230
  }
@@ -182,11 +235,13 @@ export default async function DashboardPage() {
182
235
  **Use case:** You want to fetch data server-side for faster initial page loads (SSR), but gracefully fall back to client-side fetching when the token is expired.
183
236
 
184
237
  **When to use:**
238
+
185
239
  - Pages that benefit from SSR (SEO, faster first paint)
186
240
  - Profile pages, settings pages, or any page showing user-specific data
187
241
  - When you want the best of both worlds: SSR when possible, CSR as fallback
188
242
 
189
243
  **How it works:**
244
+
190
245
  1. If token is valid → fetches data server-side, returns `ssr: true`
191
246
  2. If token is expired → skips fetch, returns `ssr: false`, client refreshes and fetches
192
247
  3. Pair with `useHydratedData` hook on the client for seamless handling
@@ -208,11 +263,11 @@ async function fetchUserProfile(accessToken: string) {
208
263
 
209
264
  export default async function ProfilePage() {
210
265
  const result = await getAuthenticatedData(auth, fetchUserProfile);
211
-
266
+
212
267
  if (result.authError) {
213
268
  redirect("/login");
214
269
  }
215
-
270
+
216
271
  // ProfileClient uses useHydratedData() to handle both SSR data and client fallback
217
272
  return <ProfileClient {...result} />;
218
273
  }
@@ -223,11 +278,13 @@ export default async function ProfilePage() {
223
278
  **Use case:** You have multiple protected pages and want to define auth error handling once, rather than repeating `if (authError) redirect(...)` on every page.
224
279
 
225
280
  **When to use:**
281
+
226
282
  - Apps with many protected pages sharing the same error handling logic
227
283
  - When you want DRY (Don't Repeat Yourself) error handling
228
284
  - Teams that want consistent auth error behavior across the app
229
285
 
230
286
  **How it works:**
287
+
231
288
  - Define error handling once in a shared file
232
289
  - Use the returned `getAuthProps` and `getData` functions in your pages
233
290
  - Auth errors automatically trigger your handler (no manual checking needed)
@@ -245,7 +302,7 @@ export const { getAuthProps, getData } = createProtectedFetchers(
245
302
  (error) => {
246
303
  // This runs automatically when there's an auth error (e.g., RefreshTokenError)
247
304
  redirect(`/login?error=${error}`);
248
- }
305
+ },
249
306
  );
250
307
  ```
251
308
 
@@ -259,7 +316,7 @@ export default async function DashboardPage() {
259
316
  const result = await getData(async (token) => {
260
317
  return fetchDashboardData(token);
261
318
  });
262
-
319
+
263
320
  return <DashboardClient {...result} />;
264
321
  }
265
322
  ```
@@ -281,6 +338,7 @@ export default async function SettingsPage() {
281
338
  **Use case:** You need fine-grained control over different authentication states and want to handle each case with custom logic.
282
339
 
283
340
  **When to use:**
341
+
284
342
  - Complex pages that render completely different UI based on auth state
285
343
  - When you need to distinguish between "token expired" vs "not authenticated"
286
344
  - Analytics or logging that needs to track specific auth states
@@ -294,22 +352,22 @@ import { getValidSession } from "@imtbl/auth-next-server";
294
352
 
295
353
  export default async function AccountPage() {
296
354
  const result = await getValidSession(auth);
297
-
355
+
298
356
  switch (result.status) {
299
357
  case "authenticated":
300
358
  // Full access - render the complete account page with SSR data
301
359
  const userData = await fetchUserData(result.session.accessToken);
302
360
  return <FullAccountPage user={userData} />;
303
-
361
+
304
362
  case "token_expired":
305
363
  // Token expired but user has session - show skeleton, let client refresh
306
364
  // This avoids a flash of "please login" for users who are actually logged in
307
365
  return <AccountPageSkeleton session={result.session} />;
308
-
366
+
309
367
  case "unauthenticated":
310
368
  // No session at all - show login prompt or redirect
311
369
  return <LoginPrompt message="Sign in to view your account" />;
312
-
370
+
313
371
  case "error":
314
372
  // Auth system error (e.g., refresh token revoked) - needs re-login
315
373
  return <AuthErrorPage error={result.error} />;
@@ -324,11 +382,13 @@ export default async function AccountPage() {
324
382
  **Use case:** Protect entire sections of your app at the routing level, before pages even render. This is the most efficient way to block unauthenticated access.
325
383
 
326
384
  **When to use:**
385
+
327
386
  - You have groups of pages that all require authentication (e.g., `/dashboard/*`, `/settings/*`)
328
387
  - You want to redirect unauthenticated users before any page code runs
329
388
  - You need consistent protection across many routes without adding checks to each page
330
389
 
331
390
  **When NOT to use:**
391
+
332
392
  - Pages that show different content for authenticated vs unauthenticated users (use page-level checks instead)
333
393
  - Public pages with optional authenticated features
334
394
 
@@ -352,17 +412,18 @@ export const config = {
352
412
 
353
413
  #### Middleware Options
354
414
 
355
- | Option | Type | Description |
356
- |--------|------|-------------|
357
- | `loginUrl` | `string` | URL to redirect unauthenticated users (default: `"/login"`) |
358
- | `protectedPaths` | `(string \| RegExp)[]` | Paths that require authentication |
359
- | `publicPaths` | `(string \| RegExp)[]` | Paths that skip authentication (takes precedence) |
415
+ | Option | Type | Description |
416
+ | ---------------- | ---------------------- | ----------------------------------------------------------- |
417
+ | `loginUrl` | `string` | URL to redirect unauthenticated users (default: `"/login"`) |
418
+ | `protectedPaths` | `(string \| RegExp)[]` | Paths that require authentication |
419
+ | `publicPaths` | `(string \| RegExp)[]` | Paths that skip authentication (takes precedence) |
360
420
 
361
421
  ### `withAuth(auth, handler)`
362
422
 
363
423
  **Use case:** Protect individual API Route Handlers or Server Actions. Ensures the handler only runs for authenticated users.
364
424
 
365
425
  **When to use:**
426
+
366
427
  - API routes that should only be accessible to authenticated users
367
428
  - Server Actions (form submissions, mutations) that require authentication
368
429
  - When you need the session/user info inside the handler
@@ -394,11 +455,11 @@ import { auth } from "@/lib/auth";
394
455
  import { withAuth } from "@imtbl/auth-next-server";
395
456
 
396
457
  export const transferAsset = withAuth(
397
- auth,
458
+ auth,
398
459
  async (session, formData: FormData) => {
399
460
  const assetId = formData.get("assetId") as string;
400
461
  const toAddress = formData.get("toAddress") as string;
401
-
462
+
402
463
  // Use session.user.sub to identify the sender
403
464
  // Use session.accessToken to call Immutable APIs
404
465
  const result = await executeTransfer({
@@ -407,9 +468,9 @@ export const transferAsset = withAuth(
407
468
  assetId,
408
469
  accessToken: session.accessToken,
409
470
  });
410
-
471
+
411
472
  return result;
412
- }
473
+ },
413
474
  );
414
475
  ```
415
476
 
@@ -420,22 +481,24 @@ The package augments the Auth.js `Session` type with Immutable-specific fields:
420
481
  ```typescript
421
482
  interface Session {
422
483
  user: {
423
- sub: string; // Immutable user ID
484
+ sub: string; // Immutable user ID
424
485
  email?: string;
425
486
  nickname?: string;
426
487
  };
427
488
  accessToken: string;
428
489
  refreshToken?: string;
429
- idToken?: string;
490
+ idToken?: string; // Only present transiently after sign-in or token refresh (not stored in cookie)
430
491
  accessTokenExpires: number;
431
492
  zkEvm?: {
432
493
  ethAddress: string;
433
494
  userAdminAddress: string;
434
495
  };
435
- error?: string; // "TokenExpired" or "RefreshTokenError"
496
+ error?: string; // "TokenExpired" or "RefreshTokenError"
436
497
  }
437
498
  ```
438
499
 
500
+ > **Note:** The `idToken` is **not** stored in the session cookie. It is stripped by a custom `jwt.encode` to keep cookie size under CDN header limits. The `idToken` is only present in the session response transiently after sign-in or token refresh. On the client, `@imtbl/auth-next-client` automatically persists it in `localStorage` so that wallet operations (via `getUser()`) can always access it. All data extracted from the idToken (`email`, `nickname`, `zkEvm`) remains in the cookie as separate fields.
501
+
439
502
  ## Token Refresh
440
503
 
441
504
  ### Automatic Refresh on Token Expiry
@@ -453,10 +516,10 @@ import { useImmutableSession } from "@imtbl/auth-next-client";
453
516
 
454
517
  function MyComponent() {
455
518
  const { getUser } = useImmutableSession();
456
-
519
+
457
520
  const handleRegistration = async () => {
458
521
  // After zkEVM registration completes...
459
-
522
+
460
523
  // Force refresh to get updated zkEvm claims from IDP
461
524
  const freshUser = await getUser(true);
462
525
  console.log("Updated zkEvm:", freshUser?.zkEvm);
@@ -465,6 +528,7 @@ function MyComponent() {
465
528
  ```
466
529
 
467
530
  When `forceRefresh` is triggered:
531
+
468
532
  1. Client calls `update({ forceRefresh: true })` via NextAuth
469
533
  2. The `jwt` callback detects `trigger === 'update'` with `forceRefresh: true`
470
534
  3. Server performs a token refresh using the refresh token
@@ -476,10 +540,10 @@ When `forceRefresh` is triggered:
476
540
  The package also exports utilities for manual token handling:
477
541
 
478
542
  ```typescript
479
- import {
480
- isTokenExpired, // Check if access token is expired
481
- refreshAccessToken, // Manually refresh tokens
482
- extractZkEvmFromIdToken // Extract zkEvm claims from ID token
543
+ import {
544
+ isTokenExpired, // Check if access token is expired
545
+ refreshAccessToken, // Manually refresh tokens
546
+ extractZkEvmFromIdToken, // Extract zkEvm claims from ID token
483
547
  } from "@imtbl/auth-next-server";
484
548
  ```
485
549
 
@@ -487,10 +551,10 @@ import {
487
551
 
488
552
  The session may contain an `error` field indicating authentication issues:
489
553
 
490
- | Error | Description | Recommended Action |
491
- |-------|-------------|-------------------|
492
- | `"TokenExpired"` | Access token expired, refresh token may be valid | Let client refresh via `@imtbl/auth-next-client` |
493
- | `"RefreshTokenError"` | Refresh token invalid/expired | Redirect to login |
554
+ | Error | Description | Recommended Action |
555
+ | --------------------- | ------------------------------------------------ | ------------------------------------------------ |
556
+ | `"TokenExpired"` | Access token expired, refresh token may be valid | Let client refresh via `@imtbl/auth-next-client` |
557
+ | `"RefreshTokenError"` | Refresh token invalid/expired | Redirect to login |
494
558
 
495
559
  ## TypeScript
496
560