@imtbl/auth-next-server 2.12.7-alpha.0 → 2.12.7-alpha.10

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
@@ -40,10 +41,12 @@ Create a file to configure Immutable authentication:
40
41
  import NextAuth from "next-auth";
41
42
  import { createAuthConfig } from "@imtbl/auth-next-server";
42
43
 
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
- }));
44
+ export const { handlers, auth, signIn, signOut } = NextAuth(
45
+ createAuthConfig({
46
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
47
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
48
+ }),
49
+ );
47
50
  ```
48
51
 
49
52
  ### 2. Set Up API Route
@@ -76,27 +79,29 @@ Creates an Auth.js v5 configuration object for Immutable authentication. You pas
76
79
  import NextAuth from "next-auth";
77
80
  import { createAuthConfig } from "@imtbl/auth-next-server";
78
81
 
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
- }));
82
+ const { handlers, auth, signIn, signOut } = NextAuth(
83
+ createAuthConfig({
84
+ // Required
85
+ clientId: "your-client-id",
86
+ redirectUri: "https://your-app.com/callback",
87
+
88
+ // Optional
89
+ audience: "platform_api", // Default: "platform_api"
90
+ scope: "openid profile email offline_access transact", // Default scope
91
+ authenticationDomain: "https://auth.immutable.com", // Default domain
92
+ }),
93
+ );
89
94
  ```
90
95
 
91
96
  #### Configuration Options
92
97
 
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"`) |
98
+ | Option | Type | Required | Description |
99
+ | ---------------------- | -------- | -------- | ------------------------------------------------------------------------ |
100
+ | `clientId` | `string` | Yes | Your Immutable application client ID |
101
+ | `redirectUri` | `string` | Yes | OAuth redirect URI configured in Immutable Hub |
102
+ | `audience` | `string` | No | OAuth audience (default: `"platform_api"`) |
103
+ | `scope` | `string` | No | OAuth scopes (default: `"openid profile email offline_access transact"`) |
104
+ | `authenticationDomain` | `string` | No | Auth domain (default: `"https://auth.immutable.com"`) |
100
105
 
101
106
  #### Extending the Configuration
102
107
 
@@ -117,19 +122,20 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
117
122
  secret: process.env.AUTH_SECRET,
118
123
  trustHost: true,
119
124
  basePath: "/api/auth/custom",
120
-
125
+
121
126
  // Extend callbacks (be sure to call the base callbacks first)
122
127
  callbacks: {
123
128
  ...baseConfig.callbacks,
124
129
  async jwt(params) {
125
130
  // Call base jwt callback first
126
- const token = await baseConfig.callbacks?.jwt?.(params) ?? params.token;
131
+ const token = (await baseConfig.callbacks?.jwt?.(params)) ?? params.token;
127
132
  // Add your custom logic
128
133
  return token;
129
134
  },
130
135
  async session(params) {
131
136
  // Call base session callback first
132
- const session = await baseConfig.callbacks?.session?.(params) ?? params.session;
137
+ const session =
138
+ (await baseConfig.callbacks?.session?.(params)) ?? params.session;
133
139
  // Add your custom logic
134
140
  return session;
135
141
  },
@@ -141,18 +147,19 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
141
147
 
142
148
  This package provides several utilities for handling authentication in Server Components. Choose the right one based on your needs:
143
149
 
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) |
150
+ | Utility | Use Case | Data Fetching | Error Handling |
151
+ | ------------------------- | ------------------------------------------------- | ------------- | ----------------- |
152
+ | `getAuthProps` | Pass auth state to client, fetch data client-side | No | Manual |
153
+ | `getAuthenticatedData` | SSR data fetching with client fallback | Yes | Manual |
154
+ | `createProtectedFetchers` | Multiple pages with same error handling | Optional | Centralized |
155
+ | `getValidSession` | Custom logic for each auth state | No | Manual (detailed) |
150
156
 
151
157
  ### `getAuthProps(auth)`
152
158
 
153
159
  **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
160
 
155
161
  **When to use:**
162
+
156
163
  - Pages where data is fetched client-side (e.g., infinite scroll, real-time updates)
157
164
  - Pages that show a loading skeleton while fetching
158
165
  - When you want full control over loading states in the client
@@ -167,11 +174,11 @@ import { DashboardClient } from "./DashboardClient";
167
174
 
168
175
  export default async function DashboardPage() {
169
176
  const authProps = await getAuthProps(auth);
170
-
177
+
171
178
  if (authProps.authError) {
172
179
  redirect("/login");
173
180
  }
174
-
181
+
175
182
  // DashboardClient will fetch its own data using useImmutableSession().getUser()
176
183
  return <DashboardClient {...authProps} />;
177
184
  }
@@ -182,11 +189,13 @@ export default async function DashboardPage() {
182
189
  **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
190
 
184
191
  **When to use:**
192
+
185
193
  - Pages that benefit from SSR (SEO, faster first paint)
186
194
  - Profile pages, settings pages, or any page showing user-specific data
187
195
  - When you want the best of both worlds: SSR when possible, CSR as fallback
188
196
 
189
197
  **How it works:**
198
+
190
199
  1. If token is valid → fetches data server-side, returns `ssr: true`
191
200
  2. If token is expired → skips fetch, returns `ssr: false`, client refreshes and fetches
192
201
  3. Pair with `useHydratedData` hook on the client for seamless handling
@@ -208,11 +217,11 @@ async function fetchUserProfile(accessToken: string) {
208
217
 
209
218
  export default async function ProfilePage() {
210
219
  const result = await getAuthenticatedData(auth, fetchUserProfile);
211
-
220
+
212
221
  if (result.authError) {
213
222
  redirect("/login");
214
223
  }
215
-
224
+
216
225
  // ProfileClient uses useHydratedData() to handle both SSR data and client fallback
217
226
  return <ProfileClient {...result} />;
218
227
  }
@@ -223,11 +232,13 @@ export default async function ProfilePage() {
223
232
  **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
233
 
225
234
  **When to use:**
235
+
226
236
  - Apps with many protected pages sharing the same error handling logic
227
237
  - When you want DRY (Don't Repeat Yourself) error handling
228
238
  - Teams that want consistent auth error behavior across the app
229
239
 
230
240
  **How it works:**
241
+
231
242
  - Define error handling once in a shared file
232
243
  - Use the returned `getAuthProps` and `getData` functions in your pages
233
244
  - Auth errors automatically trigger your handler (no manual checking needed)
@@ -245,7 +256,7 @@ export const { getAuthProps, getData } = createProtectedFetchers(
245
256
  (error) => {
246
257
  // This runs automatically when there's an auth error (e.g., RefreshTokenError)
247
258
  redirect(`/login?error=${error}`);
248
- }
259
+ },
249
260
  );
250
261
  ```
251
262
 
@@ -259,7 +270,7 @@ export default async function DashboardPage() {
259
270
  const result = await getData(async (token) => {
260
271
  return fetchDashboardData(token);
261
272
  });
262
-
273
+
263
274
  return <DashboardClient {...result} />;
264
275
  }
265
276
  ```
@@ -281,6 +292,7 @@ export default async function SettingsPage() {
281
292
  **Use case:** You need fine-grained control over different authentication states and want to handle each case with custom logic.
282
293
 
283
294
  **When to use:**
295
+
284
296
  - Complex pages that render completely different UI based on auth state
285
297
  - When you need to distinguish between "token expired" vs "not authenticated"
286
298
  - Analytics or logging that needs to track specific auth states
@@ -294,22 +306,22 @@ import { getValidSession } from "@imtbl/auth-next-server";
294
306
 
295
307
  export default async function AccountPage() {
296
308
  const result = await getValidSession(auth);
297
-
309
+
298
310
  switch (result.status) {
299
311
  case "authenticated":
300
312
  // Full access - render the complete account page with SSR data
301
313
  const userData = await fetchUserData(result.session.accessToken);
302
314
  return <FullAccountPage user={userData} />;
303
-
315
+
304
316
  case "token_expired":
305
317
  // Token expired but user has session - show skeleton, let client refresh
306
318
  // This avoids a flash of "please login" for users who are actually logged in
307
319
  return <AccountPageSkeleton session={result.session} />;
308
-
320
+
309
321
  case "unauthenticated":
310
322
  // No session at all - show login prompt or redirect
311
323
  return <LoginPrompt message="Sign in to view your account" />;
312
-
324
+
313
325
  case "error":
314
326
  // Auth system error (e.g., refresh token revoked) - needs re-login
315
327
  return <AuthErrorPage error={result.error} />;
@@ -324,11 +336,13 @@ export default async function AccountPage() {
324
336
  **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
337
 
326
338
  **When to use:**
339
+
327
340
  - You have groups of pages that all require authentication (e.g., `/dashboard/*`, `/settings/*`)
328
341
  - You want to redirect unauthenticated users before any page code runs
329
342
  - You need consistent protection across many routes without adding checks to each page
330
343
 
331
344
  **When NOT to use:**
345
+
332
346
  - Pages that show different content for authenticated vs unauthenticated users (use page-level checks instead)
333
347
  - Public pages with optional authenticated features
334
348
 
@@ -352,17 +366,18 @@ export const config = {
352
366
 
353
367
  #### Middleware Options
354
368
 
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) |
369
+ | Option | Type | Description |
370
+ | ---------------- | ---------------------- | ----------------------------------------------------------- |
371
+ | `loginUrl` | `string` | URL to redirect unauthenticated users (default: `"/login"`) |
372
+ | `protectedPaths` | `(string \| RegExp)[]` | Paths that require authentication |
373
+ | `publicPaths` | `(string \| RegExp)[]` | Paths that skip authentication (takes precedence) |
360
374
 
361
375
  ### `withAuth(auth, handler)`
362
376
 
363
377
  **Use case:** Protect individual API Route Handlers or Server Actions. Ensures the handler only runs for authenticated users.
364
378
 
365
379
  **When to use:**
380
+
366
381
  - API routes that should only be accessible to authenticated users
367
382
  - Server Actions (form submissions, mutations) that require authentication
368
383
  - When you need the session/user info inside the handler
@@ -394,11 +409,11 @@ import { auth } from "@/lib/auth";
394
409
  import { withAuth } from "@imtbl/auth-next-server";
395
410
 
396
411
  export const transferAsset = withAuth(
397
- auth,
412
+ auth,
398
413
  async (session, formData: FormData) => {
399
414
  const assetId = formData.get("assetId") as string;
400
415
  const toAddress = formData.get("toAddress") as string;
401
-
416
+
402
417
  // Use session.user.sub to identify the sender
403
418
  // Use session.accessToken to call Immutable APIs
404
419
  const result = await executeTransfer({
@@ -407,9 +422,9 @@ export const transferAsset = withAuth(
407
422
  assetId,
408
423
  accessToken: session.accessToken,
409
424
  });
410
-
425
+
411
426
  return result;
412
- }
427
+ },
413
428
  );
414
429
  ```
415
430
 
@@ -420,22 +435,24 @@ The package augments the Auth.js `Session` type with Immutable-specific fields:
420
435
  ```typescript
421
436
  interface Session {
422
437
  user: {
423
- sub: string; // Immutable user ID
438
+ sub: string; // Immutable user ID
424
439
  email?: string;
425
440
  nickname?: string;
426
441
  };
427
442
  accessToken: string;
428
443
  refreshToken?: string;
429
- idToken?: string;
444
+ idToken?: string; // Only present transiently after sign-in or token refresh (not stored in cookie)
430
445
  accessTokenExpires: number;
431
446
  zkEvm?: {
432
447
  ethAddress: string;
433
448
  userAdminAddress: string;
434
449
  };
435
- error?: string; // "TokenExpired" or "RefreshTokenError"
450
+ error?: string; // "TokenExpired" or "RefreshTokenError"
436
451
  }
437
452
  ```
438
453
 
454
+ > **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.
455
+
439
456
  ## Token Refresh
440
457
 
441
458
  ### Automatic Refresh on Token Expiry
@@ -453,10 +470,10 @@ import { useImmutableSession } from "@imtbl/auth-next-client";
453
470
 
454
471
  function MyComponent() {
455
472
  const { getUser } = useImmutableSession();
456
-
473
+
457
474
  const handleRegistration = async () => {
458
475
  // After zkEVM registration completes...
459
-
476
+
460
477
  // Force refresh to get updated zkEvm claims from IDP
461
478
  const freshUser = await getUser(true);
462
479
  console.log("Updated zkEvm:", freshUser?.zkEvm);
@@ -465,6 +482,7 @@ function MyComponent() {
465
482
  ```
466
483
 
467
484
  When `forceRefresh` is triggered:
485
+
468
486
  1. Client calls `update({ forceRefresh: true })` via NextAuth
469
487
  2. The `jwt` callback detects `trigger === 'update'` with `forceRefresh: true`
470
488
  3. Server performs a token refresh using the refresh token
@@ -476,10 +494,10 @@ When `forceRefresh` is triggered:
476
494
  The package also exports utilities for manual token handling:
477
495
 
478
496
  ```typescript
479
- import {
480
- isTokenExpired, // Check if access token is expired
481
- refreshAccessToken, // Manually refresh tokens
482
- extractZkEvmFromIdToken // Extract zkEvm claims from ID token
497
+ import {
498
+ isTokenExpired, // Check if access token is expired
499
+ refreshAccessToken, // Manually refresh tokens
500
+ extractZkEvmFromIdToken, // Extract zkEvm claims from ID token
483
501
  } from "@imtbl/auth-next-server";
484
502
  ```
485
503
 
@@ -487,10 +505,10 @@ import {
487
505
 
488
506
  The session may contain an `error` field indicating authentication issues:
489
507
 
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 |
508
+ | Error | Description | Recommended Action |
509
+ | --------------------- | ------------------------------------------------ | ------------------------------------------------ |
510
+ | `"TokenExpired"` | Access token expired, refresh token may be valid | Let client refresh via `@imtbl/auth-next-client` |
511
+ | `"RefreshTokenError"` | Refresh token invalid/expired | Redirect to login |
494
512
 
495
513
  ## TypeScript
496
514
 
@@ -60,6 +60,7 @@ var import_next_auth = __toESM(require("next-auth"), 1);
60
60
 
61
61
  // src/config.ts
62
62
  var import_credentials = __toESM(require("next-auth/providers/credentials"), 1);
63
+ var import_jwt = require("next-auth/jwt");
63
64
 
64
65
  // src/constants.ts
65
66
  var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
@@ -148,6 +149,7 @@ async function refreshAccessToken(refreshToken, clientId, authDomain = DEFAULT_A
148
149
 
149
150
  // src/config.ts
150
151
  var Credentials = import_credentials.default.default || import_credentials.default;
152
+ var defaultJwtEncode = import_jwt.encode.default || import_jwt.encode;
151
153
  async function validateTokens(accessToken, authDomain) {
152
154
  try {
153
155
  const response = await fetch(`${authDomain}/userinfo`, {
@@ -169,6 +171,22 @@ async function validateTokens(accessToken, authDomain) {
169
171
  function createAuthConfig(config) {
170
172
  const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
171
173
  return {
174
+ // Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
175
+ // CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
176
+ // still available in session responses (after sign-in or token refresh) because
177
+ // the session callback runs BEFORE encode. All data extracted FROM idToken
178
+ // (email, nickname, zkEvm) remains in the cookie as separate fields.
179
+ // On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
180
+ jwt: {
181
+ async encode(params) {
182
+ const { token, ...rest } = params;
183
+ if (token) {
184
+ const { idToken, ...cookieToken } = token;
185
+ return defaultJwtEncode({ ...rest, token: cookieToken });
186
+ }
187
+ return defaultJwtEncode(params);
188
+ }
189
+ },
172
190
  providers: [
173
191
  Credentials({
174
192
  id: IMMUTABLE_PROVIDER_ID,
@@ -227,22 +245,63 @@ function createAuthConfig(config) {
227
245
  trigger,
228
246
  session: sessionUpdate
229
247
  }) {
230
- if (user) {
231
- return {
232
- ...token,
233
- sub: user.sub,
234
- email: user.email,
235
- nickname: user.nickname,
236
- accessToken: user.accessToken,
237
- refreshToken: user.refreshToken,
238
- idToken: user.idToken,
239
- accessTokenExpires: user.accessTokenExpires,
240
- zkEvm: user.zkEvm
241
- };
242
- }
243
- if (trigger === "update" && sessionUpdate) {
244
- const update = sessionUpdate;
245
- if (update.forceRefresh && token.refreshToken) {
248
+ try {
249
+ if (user) {
250
+ return {
251
+ ...token,
252
+ sub: user.sub,
253
+ email: user.email,
254
+ nickname: user.nickname,
255
+ accessToken: user.accessToken,
256
+ refreshToken: user.refreshToken,
257
+ idToken: user.idToken,
258
+ accessTokenExpires: user.accessTokenExpires,
259
+ zkEvm: user.zkEvm
260
+ };
261
+ }
262
+ if (trigger === "update" && sessionUpdate) {
263
+ const update = sessionUpdate;
264
+ if (update.forceRefresh && token.refreshToken) {
265
+ try {
266
+ const refreshed = await refreshAccessToken(
267
+ token.refreshToken,
268
+ config.clientId,
269
+ authDomain
270
+ );
271
+ const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
272
+ return {
273
+ ...token,
274
+ accessToken: refreshed.accessToken,
275
+ refreshToken: refreshed.refreshToken,
276
+ idToken: refreshed.idToken,
277
+ accessTokenExpires: refreshed.accessTokenExpires,
278
+ zkEvm: zkEvm ?? token.zkEvm,
279
+ // Keep existing zkEvm if not in new token
280
+ error: void 0
281
+ };
282
+ } catch (error) {
283
+ console.error("[auth-next-server] Force refresh failed:", error);
284
+ return {
285
+ ...token,
286
+ error: "RefreshTokenError"
287
+ };
288
+ }
289
+ }
290
+ return {
291
+ ...token,
292
+ ...update.accessToken ? { accessToken: update.accessToken } : {},
293
+ ...update.refreshToken ? { refreshToken: update.refreshToken } : {},
294
+ ...update.idToken ? { idToken: update.idToken } : {},
295
+ ...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
296
+ ...update.zkEvm ? { zkEvm: update.zkEvm } : {},
297
+ // Clear any stale error when valid tokens are synced from client-side
298
+ error: void 0
299
+ };
300
+ }
301
+ if (!isTokenExpired(token.accessTokenExpires)) {
302
+ return token;
303
+ }
304
+ if (token.refreshToken) {
246
305
  try {
247
306
  const refreshed = await refreshAccessToken(
248
307
  token.refreshToken,
@@ -259,9 +318,10 @@ function createAuthConfig(config) {
259
318
  zkEvm: zkEvm ?? token.zkEvm,
260
319
  // Keep existing zkEvm if not in new token
261
320
  error: void 0
321
+ // Clear any previous error
262
322
  };
263
323
  } catch (error) {
264
- console.error("[auth-next-server] Force refresh failed:", error);
324
+ console.error("[auth-next-server] Token refresh failed:", error);
265
325
  return {
266
326
  ...token,
267
327
  error: "RefreshTokenError"
@@ -270,67 +330,35 @@ function createAuthConfig(config) {
270
330
  }
271
331
  return {
272
332
  ...token,
273
- ...update.accessToken ? { accessToken: update.accessToken } : {},
274
- ...update.refreshToken ? { refreshToken: update.refreshToken } : {},
275
- ...update.idToken ? { idToken: update.idToken } : {},
276
- ...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
277
- ...update.zkEvm ? { zkEvm: update.zkEvm } : {},
278
- // Clear any stale error when valid tokens are synced from client-side
279
- error: void 0
333
+ error: "TokenExpired"
280
334
  };
335
+ } catch (error) {
336
+ console.error("[auth-next-server] JWT callback error:", error);
337
+ throw error;
281
338
  }
282
- if (!isTokenExpired(token.accessTokenExpires)) {
283
- return token;
284
- }
285
- if (token.refreshToken) {
286
- try {
287
- const refreshed = await refreshAccessToken(
288
- token.refreshToken,
289
- config.clientId,
290
- authDomain
291
- );
292
- const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
293
- return {
294
- ...token,
295
- accessToken: refreshed.accessToken,
296
- refreshToken: refreshed.refreshToken,
297
- idToken: refreshed.idToken,
298
- accessTokenExpires: refreshed.accessTokenExpires,
299
- zkEvm: zkEvm ?? token.zkEvm,
300
- // Keep existing zkEvm if not in new token
301
- error: void 0
302
- // Clear any previous error
303
- };
304
- } catch (error) {
305
- console.error("[auth-next-server] Token refresh failed:", error);
306
- return {
307
- ...token,
308
- error: "RefreshTokenError"
309
- };
310
- }
311
- }
312
- return {
313
- ...token,
314
- error: "TokenExpired"
315
- };
316
339
  },
317
340
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
318
341
  async session({ session, token }) {
319
- return {
320
- ...session,
321
- user: {
322
- ...session.user,
323
- sub: token.sub,
324
- email: token.email,
325
- nickname: token.nickname
326
- },
327
- accessToken: token.accessToken,
328
- refreshToken: token.refreshToken,
329
- idToken: token.idToken,
330
- accessTokenExpires: token.accessTokenExpires,
331
- zkEvm: token.zkEvm,
332
- ...token.error && { error: token.error }
333
- };
342
+ try {
343
+ return {
344
+ ...session,
345
+ user: {
346
+ ...session.user,
347
+ sub: token.sub,
348
+ email: token.email,
349
+ nickname: token.nickname
350
+ },
351
+ accessToken: token.accessToken,
352
+ refreshToken: token.refreshToken,
353
+ idToken: token.idToken,
354
+ accessTokenExpires: token.accessTokenExpires,
355
+ zkEvm: token.zkEvm,
356
+ ...token.error && { error: token.error }
357
+ };
358
+ } catch (error) {
359
+ console.error("[auth-next-server] Session callback error:", error);
360
+ throw error;
361
+ }
334
362
  }
335
363
  },
336
364
  session: {
@@ -13,6 +13,7 @@ import { default as default2 } from "next-auth";
13
13
 
14
14
  // src/config.ts
15
15
  import CredentialsImport from "next-auth/providers/credentials";
16
+ import { encode as encodeImport } from "next-auth/jwt";
16
17
 
17
18
  // src/constants.ts
18
19
  var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
@@ -101,6 +102,7 @@ async function refreshAccessToken(refreshToken, clientId, authDomain = DEFAULT_A
101
102
 
102
103
  // src/config.ts
103
104
  var Credentials = CredentialsImport.default || CredentialsImport;
105
+ var defaultJwtEncode = encodeImport.default || encodeImport;
104
106
  async function validateTokens(accessToken, authDomain) {
105
107
  try {
106
108
  const response = await fetch(`${authDomain}/userinfo`, {
@@ -122,6 +124,22 @@ async function validateTokens(accessToken, authDomain) {
122
124
  function createAuthConfig(config) {
123
125
  const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
124
126
  return {
127
+ // Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
128
+ // CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
129
+ // still available in session responses (after sign-in or token refresh) because
130
+ // the session callback runs BEFORE encode. All data extracted FROM idToken
131
+ // (email, nickname, zkEvm) remains in the cookie as separate fields.
132
+ // On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
133
+ jwt: {
134
+ async encode(params) {
135
+ const { token, ...rest } = params;
136
+ if (token) {
137
+ const { idToken, ...cookieToken } = token;
138
+ return defaultJwtEncode({ ...rest, token: cookieToken });
139
+ }
140
+ return defaultJwtEncode(params);
141
+ }
142
+ },
125
143
  providers: [
126
144
  Credentials({
127
145
  id: IMMUTABLE_PROVIDER_ID,
@@ -180,22 +198,63 @@ function createAuthConfig(config) {
180
198
  trigger,
181
199
  session: sessionUpdate
182
200
  }) {
183
- if (user) {
184
- return {
185
- ...token,
186
- sub: user.sub,
187
- email: user.email,
188
- nickname: user.nickname,
189
- accessToken: user.accessToken,
190
- refreshToken: user.refreshToken,
191
- idToken: user.idToken,
192
- accessTokenExpires: user.accessTokenExpires,
193
- zkEvm: user.zkEvm
194
- };
195
- }
196
- if (trigger === "update" && sessionUpdate) {
197
- const update = sessionUpdate;
198
- if (update.forceRefresh && token.refreshToken) {
201
+ try {
202
+ if (user) {
203
+ return {
204
+ ...token,
205
+ sub: user.sub,
206
+ email: user.email,
207
+ nickname: user.nickname,
208
+ accessToken: user.accessToken,
209
+ refreshToken: user.refreshToken,
210
+ idToken: user.idToken,
211
+ accessTokenExpires: user.accessTokenExpires,
212
+ zkEvm: user.zkEvm
213
+ };
214
+ }
215
+ if (trigger === "update" && sessionUpdate) {
216
+ const update = sessionUpdate;
217
+ if (update.forceRefresh && token.refreshToken) {
218
+ try {
219
+ const refreshed = await refreshAccessToken(
220
+ token.refreshToken,
221
+ config.clientId,
222
+ authDomain
223
+ );
224
+ const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
225
+ return {
226
+ ...token,
227
+ accessToken: refreshed.accessToken,
228
+ refreshToken: refreshed.refreshToken,
229
+ idToken: refreshed.idToken,
230
+ accessTokenExpires: refreshed.accessTokenExpires,
231
+ zkEvm: zkEvm ?? token.zkEvm,
232
+ // Keep existing zkEvm if not in new token
233
+ error: void 0
234
+ };
235
+ } catch (error) {
236
+ console.error("[auth-next-server] Force refresh failed:", error);
237
+ return {
238
+ ...token,
239
+ error: "RefreshTokenError"
240
+ };
241
+ }
242
+ }
243
+ return {
244
+ ...token,
245
+ ...update.accessToken ? { accessToken: update.accessToken } : {},
246
+ ...update.refreshToken ? { refreshToken: update.refreshToken } : {},
247
+ ...update.idToken ? { idToken: update.idToken } : {},
248
+ ...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
249
+ ...update.zkEvm ? { zkEvm: update.zkEvm } : {},
250
+ // Clear any stale error when valid tokens are synced from client-side
251
+ error: void 0
252
+ };
253
+ }
254
+ if (!isTokenExpired(token.accessTokenExpires)) {
255
+ return token;
256
+ }
257
+ if (token.refreshToken) {
199
258
  try {
200
259
  const refreshed = await refreshAccessToken(
201
260
  token.refreshToken,
@@ -212,9 +271,10 @@ function createAuthConfig(config) {
212
271
  zkEvm: zkEvm ?? token.zkEvm,
213
272
  // Keep existing zkEvm if not in new token
214
273
  error: void 0
274
+ // Clear any previous error
215
275
  };
216
276
  } catch (error) {
217
- console.error("[auth-next-server] Force refresh failed:", error);
277
+ console.error("[auth-next-server] Token refresh failed:", error);
218
278
  return {
219
279
  ...token,
220
280
  error: "RefreshTokenError"
@@ -223,67 +283,35 @@ function createAuthConfig(config) {
223
283
  }
224
284
  return {
225
285
  ...token,
226
- ...update.accessToken ? { accessToken: update.accessToken } : {},
227
- ...update.refreshToken ? { refreshToken: update.refreshToken } : {},
228
- ...update.idToken ? { idToken: update.idToken } : {},
229
- ...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
230
- ...update.zkEvm ? { zkEvm: update.zkEvm } : {},
231
- // Clear any stale error when valid tokens are synced from client-side
232
- error: void 0
286
+ error: "TokenExpired"
233
287
  };
288
+ } catch (error) {
289
+ console.error("[auth-next-server] JWT callback error:", error);
290
+ throw error;
234
291
  }
235
- if (!isTokenExpired(token.accessTokenExpires)) {
236
- return token;
237
- }
238
- if (token.refreshToken) {
239
- try {
240
- const refreshed = await refreshAccessToken(
241
- token.refreshToken,
242
- config.clientId,
243
- authDomain
244
- );
245
- const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
246
- return {
247
- ...token,
248
- accessToken: refreshed.accessToken,
249
- refreshToken: refreshed.refreshToken,
250
- idToken: refreshed.idToken,
251
- accessTokenExpires: refreshed.accessTokenExpires,
252
- zkEvm: zkEvm ?? token.zkEvm,
253
- // Keep existing zkEvm if not in new token
254
- error: void 0
255
- // Clear any previous error
256
- };
257
- } catch (error) {
258
- console.error("[auth-next-server] Token refresh failed:", error);
259
- return {
260
- ...token,
261
- error: "RefreshTokenError"
262
- };
263
- }
264
- }
265
- return {
266
- ...token,
267
- error: "TokenExpired"
268
- };
269
292
  },
270
293
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
294
  async session({ session, token }) {
272
- return {
273
- ...session,
274
- user: {
275
- ...session.user,
276
- sub: token.sub,
277
- email: token.email,
278
- nickname: token.nickname
279
- },
280
- accessToken: token.accessToken,
281
- refreshToken: token.refreshToken,
282
- idToken: token.idToken,
283
- accessTokenExpires: token.accessTokenExpires,
284
- zkEvm: token.zkEvm,
285
- ...token.error && { error: token.error }
286
- };
295
+ try {
296
+ return {
297
+ ...session,
298
+ user: {
299
+ ...session.user,
300
+ sub: token.sub,
301
+ email: token.email,
302
+ nickname: token.nickname
303
+ },
304
+ accessToken: token.accessToken,
305
+ refreshToken: token.refreshToken,
306
+ idToken: token.idToken,
307
+ accessTokenExpires: token.accessTokenExpires,
308
+ zkEvm: token.zkEvm,
309
+ ...token.error && { error: token.error }
310
+ };
311
+ } catch (error) {
312
+ console.error("[auth-next-server] Session callback error:", error);
313
+ throw error;
314
+ }
287
315
  }
288
316
  },
289
317
  session: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imtbl/auth-next-server",
3
- "version": "2.12.7-alpha.0",
3
+ "version": "2.12.7-alpha.10",
4
4
  "description": "Immutable Auth.js v5 integration for Next.js - Server-side utilities",
5
5
  "author": "Immutable",
6
6
  "license": "Apache-2.0",
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // @ts-ignore - Type exists in next-auth v5 but TS resolver may use stale types
4
4
  import type { NextAuthConfig } from 'next-auth';
5
5
  import CredentialsImport from 'next-auth/providers/credentials';
6
+ import { encode as encodeImport } from 'next-auth/jwt';
6
7
  import type { ImmutableAuthConfig, ImmutableTokenData, UserInfoResponse } from './types';
7
8
  import { isTokenExpired, refreshAccessToken, extractZkEvmFromIdToken } from './refresh';
8
9
  import {
@@ -15,6 +16,8 @@ import {
15
16
  // may be nested under a 'default' property
16
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
18
  const Credentials = ((CredentialsImport as any).default || CredentialsImport) as typeof CredentialsImport;
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const defaultJwtEncode = ((encodeImport as any).default || encodeImport) as typeof encodeImport;
18
21
 
19
22
  /**
20
23
  * Validate tokens by calling the userinfo endpoint.
@@ -72,6 +75,23 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
72
75
  const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
73
76
 
74
77
  return {
78
+ // Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
79
+ // CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
80
+ // still available in session responses (after sign-in or token refresh) because
81
+ // the session callback runs BEFORE encode. All data extracted FROM idToken
82
+ // (email, nickname, zkEvm) remains in the cookie as separate fields.
83
+ // On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
84
+ jwt: {
85
+ async encode(params) {
86
+ const { token, ...rest } = params;
87
+ if (token) {
88
+ const { idToken, ...cookieToken } = token as Record<string, unknown>;
89
+ return defaultJwtEncode({ ...rest, token: cookieToken });
90
+ }
91
+ return defaultJwtEncode(params);
92
+ },
93
+ },
94
+
75
95
  providers: [
76
96
  Credentials({
77
97
  id: IMMUTABLE_PROVIDER_ID,
@@ -154,31 +174,80 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
154
174
  async jwt({
155
175
  token, user, trigger, session: sessionUpdate,
156
176
  }: any) {
157
- // Initial sign in - store all token data
158
- if (user) {
159
- return {
160
- ...token,
161
- sub: user.sub,
162
- email: user.email,
163
- nickname: user.nickname,
164
- accessToken: user.accessToken,
165
- refreshToken: user.refreshToken,
166
- idToken: user.idToken,
167
- accessTokenExpires: user.accessTokenExpires,
168
- zkEvm: user.zkEvm,
169
- };
170
- }
177
+ try {
178
+ // Initial sign in - store all token data
179
+ if (user) {
180
+ return {
181
+ ...token,
182
+ sub: user.sub,
183
+ email: user.email,
184
+ nickname: user.nickname,
185
+ accessToken: user.accessToken,
186
+ refreshToken: user.refreshToken,
187
+ idToken: user.idToken,
188
+ accessTokenExpires: user.accessTokenExpires,
189
+ zkEvm: user.zkEvm,
190
+ };
191
+ }
192
+
193
+ // Handle session update (for client-side token sync or forceRefresh)
194
+ // When client-side Auth refreshes tokens via TOKEN_REFRESHED event,
195
+ // it calls updateSession() which triggers this callback with the new tokens.
196
+ // We clear any stale error (e.g., TokenExpired) on successful update.
197
+ if (trigger === 'update' && sessionUpdate) {
198
+ const update = sessionUpdate as Record<string, unknown>;
199
+
200
+ // If forceRefresh is requested, perform server-side token refresh
201
+ // This is used after zkEVM registration to get updated claims from IDP
202
+ if (update.forceRefresh && token.refreshToken) {
203
+ try {
204
+ const refreshed = await refreshAccessToken(
205
+ token.refreshToken as string,
206
+ config.clientId,
207
+ authDomain,
208
+ );
209
+ // Extract zkEvm claims from the refreshed idToken
210
+ const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
211
+ return {
212
+ ...token,
213
+ accessToken: refreshed.accessToken,
214
+ refreshToken: refreshed.refreshToken,
215
+ idToken: refreshed.idToken,
216
+ accessTokenExpires: refreshed.accessTokenExpires,
217
+ zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
218
+ error: undefined,
219
+ };
220
+ } catch (error) {
221
+ // eslint-disable-next-line no-console
222
+ console.error('[auth-next-server] Force refresh failed:', error);
223
+ return {
224
+ ...token,
225
+ error: 'RefreshTokenError',
226
+ };
227
+ }
228
+ }
229
+
230
+ // Standard session update - merge provided values
231
+ return {
232
+ ...token,
233
+ ...(update.accessToken ? { accessToken: update.accessToken } : {}),
234
+ ...(update.refreshToken ? { refreshToken: update.refreshToken } : {}),
235
+ ...(update.idToken ? { idToken: update.idToken } : {}),
236
+ ...(update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {}),
237
+ ...(update.zkEvm ? { zkEvm: update.zkEvm } : {}),
238
+ // Clear any stale error when valid tokens are synced from client-side
239
+ error: undefined,
240
+ };
241
+ }
171
242
 
172
- // Handle session update (for client-side token sync or forceRefresh)
173
- // When client-side Auth refreshes tokens via TOKEN_REFRESHED event,
174
- // it calls updateSession() which triggers this callback with the new tokens.
175
- // We clear any stale error (e.g., TokenExpired) on successful update.
176
- if (trigger === 'update' && sessionUpdate) {
177
- const update = sessionUpdate as Record<string, unknown>;
243
+ // Return token if not expired
244
+ if (!isTokenExpired(token.accessTokenExpires as number)) {
245
+ return token;
246
+ }
178
247
 
179
- // If forceRefresh is requested, perform server-side token refresh
180
- // This is used after zkEVM registration to get updated claims from IDP
181
- if (update.forceRefresh && token.refreshToken) {
248
+ // Token expired - attempt server-side refresh
249
+ // This ensures clients always get fresh tokens from session callbacks
250
+ if (token.refreshToken) {
182
251
  try {
183
252
  const refreshed = await refreshAccessToken(
184
253
  token.refreshToken as string,
@@ -194,11 +263,11 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
194
263
  idToken: refreshed.idToken,
195
264
  accessTokenExpires: refreshed.accessTokenExpires,
196
265
  zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
197
- error: undefined,
266
+ error: undefined, // Clear any previous error
198
267
  };
199
268
  } catch (error) {
200
- // eslint-disable-next-line no-console
201
- console.error('[auth-next-server] Force refresh failed:', error);
269
+ // eslint-disable-next-line no-console
270
+ console.error('[auth-next-server] Token refresh failed:', error);
202
271
  return {
203
272
  ...token,
204
273
  error: 'RefreshTokenError',
@@ -206,79 +275,42 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
206
275
  }
207
276
  }
208
277
 
209
- // Standard session update - merge provided values
278
+ // No refresh token available
210
279
  return {
211
280
  ...token,
212
- ...(update.accessToken ? { accessToken: update.accessToken } : {}),
213
- ...(update.refreshToken ? { refreshToken: update.refreshToken } : {}),
214
- ...(update.idToken ? { idToken: update.idToken } : {}),
215
- ...(update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {}),
216
- ...(update.zkEvm ? { zkEvm: update.zkEvm } : {}),
217
- // Clear any stale error when valid tokens are synced from client-side
218
- error: undefined,
281
+ error: 'TokenExpired',
219
282
  };
283
+ } catch (error) {
284
+ // eslint-disable-next-line no-console
285
+ console.error('[auth-next-server] JWT callback error:', error);
286
+ throw error;
220
287
  }
221
-
222
- // Return token if not expired
223
- if (!isTokenExpired(token.accessTokenExpires as number)) {
224
- return token;
225
- }
226
-
227
- // Token expired - attempt server-side refresh
228
- // This ensures clients always get fresh tokens from session callbacks
229
- if (token.refreshToken) {
230
- try {
231
- const refreshed = await refreshAccessToken(
232
- token.refreshToken as string,
233
- config.clientId,
234
- authDomain,
235
- );
236
- // Extract zkEvm claims from the refreshed idToken
237
- const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
238
- return {
239
- ...token,
240
- accessToken: refreshed.accessToken,
241
- refreshToken: refreshed.refreshToken,
242
- idToken: refreshed.idToken,
243
- accessTokenExpires: refreshed.accessTokenExpires,
244
- zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
245
- error: undefined, // Clear any previous error
246
- };
247
- } catch (error) {
248
- // eslint-disable-next-line no-console
249
- console.error('[auth-next-server] Token refresh failed:', error);
250
- return {
251
- ...token,
252
- error: 'RefreshTokenError',
253
- };
254
- }
255
- }
256
-
257
- // No refresh token available
258
- return {
259
- ...token,
260
- error: 'TokenExpired',
261
- };
262
288
  },
263
289
 
264
290
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
291
  async session({ session, token }: any) {
266
- // Expose token data to the session
267
- return {
268
- ...session,
269
- user: {
270
- ...session.user,
271
- sub: token.sub as string,
272
- email: token.email as string | undefined,
273
- nickname: token.nickname as string | undefined,
274
- },
275
- accessToken: token.accessToken as string,
276
- refreshToken: token.refreshToken as string | undefined,
277
- idToken: token.idToken as string | undefined,
278
- accessTokenExpires: token.accessTokenExpires as number,
279
- zkEvm: token.zkEvm,
280
- ...(token.error && { error: token.error as string }),
281
- };
292
+ try {
293
+ // Expose token data to the session
294
+ return {
295
+ ...session,
296
+ user: {
297
+ ...session.user,
298
+ sub: token.sub as string,
299
+ email: token.email as string | undefined,
300
+ nickname: token.nickname as string | undefined,
301
+ },
302
+ accessToken: token.accessToken as string,
303
+ refreshToken: token.refreshToken as string | undefined,
304
+ idToken: token.idToken as string | undefined,
305
+ accessTokenExpires: token.accessTokenExpires as number,
306
+ zkEvm: token.zkEvm,
307
+ ...(token.error && { error: token.error as string }),
308
+ };
309
+ } catch (error) {
310
+ // eslint-disable-next-line no-console
311
+ console.error('[auth-next-server] Session callback error:', error);
312
+ throw error;
313
+ }
282
314
  },
283
315
  },
284
316