@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/src/config.ts CHANGED
@@ -3,10 +3,13 @@
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 {
9
10
  DEFAULT_AUTH_DOMAIN,
11
+ DEFAULT_REDIRECT_URI_PATH,
12
+ DEFAULT_SANDBOX_CLIENT_ID,
10
13
  IMMUTABLE_PROVIDER_ID,
11
14
  DEFAULT_SESSION_MAX_AGE_SECONDS,
12
15
  } from './constants';
@@ -15,6 +18,8 @@ import {
15
18
  // may be nested under a 'default' property
16
19
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
20
  const Credentials = ((CredentialsImport as any).default || CredentialsImport) as typeof CredentialsImport;
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ const defaultJwtEncode = ((encodeImport as any).default || encodeImport) as typeof encodeImport;
18
23
 
19
24
  /**
20
25
  * Validate tokens by calling the userinfo endpoint.
@@ -52,26 +57,73 @@ async function validateTokens(
52
57
  }
53
58
 
54
59
  /**
55
- * Create Auth.js v5 configuration for Immutable authentication
60
+ * Resolve redirect URI for zero-config mode.
61
+ * Uses __NEXT_PRIVATE_ORIGIN when available (Next.js internal), otherwise path-only.
62
+ */
63
+ function resolveDefaultRedirectUri(): string {
64
+ // eslint-disable-next-line no-underscore-dangle -- Next.js internal env var
65
+ const origin = process.env.__NEXT_PRIVATE_ORIGIN;
66
+ if (origin) {
67
+ return new URL(DEFAULT_REDIRECT_URI_PATH, origin).href;
68
+ }
69
+ return DEFAULT_REDIRECT_URI_PATH;
70
+ }
71
+
72
+ /**
73
+ * Create Auth.js v5 configuration for Immutable authentication.
74
+ *
75
+ * Policy: provide nothing → full sandbox config; provide config → provide everything.
76
+ * - Zero config: sandbox clientId, auto-derived redirectUri. No conflicts.
77
+ * - With config: clientId and redirectUri required. Pass full config to avoid conflicts.
78
+ *
79
+ * @param config - Optional. When omitted, uses sandbox defaults. When provided, clientId and redirectUri are required.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // Zero config - sandbox, only AUTH_SECRET required in .env
84
+ * import NextAuth from "next-auth";
85
+ * import { createAuthConfig } from "@imtbl/auth-next-server";
86
+ *
87
+ * export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig());
88
+ * ```
56
89
  *
57
90
  * @example
58
91
  * ```typescript
59
- * // lib/auth.ts
92
+ * // With config - provide clientId and redirectUri (and optionally audience, scope, authenticationDomain)
60
93
  * import NextAuth from "next-auth";
61
94
  * import { createAuthConfig } from "@imtbl/auth-next-server";
62
95
  *
63
- * const config = {
96
+ * export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig({
64
97
  * clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
65
98
  * redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
66
- * };
67
- *
68
- * export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig(config));
99
+ * }));
69
100
  * ```
70
101
  */
71
- export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
72
- const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
102
+ export function createAuthConfig(config?: ImmutableAuthConfig): NextAuthConfig {
103
+ const resolvedConfig: ImmutableAuthConfig = config ?? {
104
+ clientId: DEFAULT_SANDBOX_CLIENT_ID,
105
+ redirectUri: resolveDefaultRedirectUri(),
106
+ };
107
+ const authDomain = resolvedConfig.authenticationDomain || DEFAULT_AUTH_DOMAIN;
73
108
 
74
109
  return {
110
+ // Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
111
+ // CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
112
+ // still available in session responses (after sign-in or token refresh) because
113
+ // the session callback runs BEFORE encode. All data extracted FROM idToken
114
+ // (email, nickname, zkEvm) remains in the cookie as separate fields.
115
+ // On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
116
+ jwt: {
117
+ async encode(params) {
118
+ const { token, ...rest } = params;
119
+ if (token) {
120
+ const { idToken, ...cookieToken } = token as Record<string, unknown>;
121
+ return defaultJwtEncode({ ...rest, token: cookieToken });
122
+ }
123
+ return defaultJwtEncode(params);
124
+ },
125
+ },
126
+
75
127
  providers: [
76
128
  Credentials({
77
129
  id: IMMUTABLE_PROVIDER_ID,
@@ -154,35 +206,84 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
154
206
  async jwt({
155
207
  token, user, trigger, session: sessionUpdate,
156
208
  }: 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
- }
209
+ try {
210
+ // Initial sign in - store all token data
211
+ if (user) {
212
+ return {
213
+ ...token,
214
+ sub: user.sub,
215
+ email: user.email,
216
+ nickname: user.nickname,
217
+ accessToken: user.accessToken,
218
+ refreshToken: user.refreshToken,
219
+ idToken: user.idToken,
220
+ accessTokenExpires: user.accessTokenExpires,
221
+ zkEvm: user.zkEvm,
222
+ };
223
+ }
171
224
 
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>;
225
+ // Handle session update (for client-side token sync or forceRefresh)
226
+ // When client-side Auth refreshes tokens via TOKEN_REFRESHED event,
227
+ // it calls updateSession() which triggers this callback with the new tokens.
228
+ // We clear any stale error (e.g., TokenExpired) on successful update.
229
+ if (trigger === 'update' && sessionUpdate) {
230
+ const update = sessionUpdate as Record<string, unknown>;
178
231
 
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) {
232
+ // If forceRefresh is requested, perform server-side token refresh
233
+ // This is used after zkEVM registration to get updated claims from IDP
234
+ if (update.forceRefresh && token.refreshToken) {
235
+ try {
236
+ const refreshed = await refreshAccessToken(
237
+ token.refreshToken as string,
238
+ resolvedConfig.clientId,
239
+ authDomain,
240
+ );
241
+ // Extract zkEvm claims from the refreshed idToken
242
+ const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
243
+ return {
244
+ ...token,
245
+ accessToken: refreshed.accessToken,
246
+ refreshToken: refreshed.refreshToken,
247
+ idToken: refreshed.idToken,
248
+ accessTokenExpires: refreshed.accessTokenExpires,
249
+ zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
250
+ error: undefined,
251
+ };
252
+ } catch (error) {
253
+ // eslint-disable-next-line no-console
254
+ console.error('[auth-next-server] Force refresh failed:', error);
255
+ return {
256
+ ...token,
257
+ error: 'RefreshTokenError',
258
+ };
259
+ }
260
+ }
261
+
262
+ // Standard session update - merge provided values
263
+ return {
264
+ ...token,
265
+ ...(update.accessToken ? { accessToken: update.accessToken } : {}),
266
+ ...(update.refreshToken ? { refreshToken: update.refreshToken } : {}),
267
+ ...(update.idToken ? { idToken: update.idToken } : {}),
268
+ ...(update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {}),
269
+ ...(update.zkEvm ? { zkEvm: update.zkEvm } : {}),
270
+ // Clear any stale error when valid tokens are synced from client-side
271
+ error: undefined,
272
+ };
273
+ }
274
+
275
+ // Return token if not expired
276
+ if (!isTokenExpired(token.accessTokenExpires as number)) {
277
+ return token;
278
+ }
279
+
280
+ // Token expired - attempt server-side refresh
281
+ // This ensures clients always get fresh tokens from session callbacks
282
+ if (token.refreshToken) {
182
283
  try {
183
284
  const refreshed = await refreshAccessToken(
184
285
  token.refreshToken as string,
185
- config.clientId,
286
+ resolvedConfig.clientId,
186
287
  authDomain,
187
288
  );
188
289
  // Extract zkEvm claims from the refreshed idToken
@@ -194,11 +295,11 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
194
295
  idToken: refreshed.idToken,
195
296
  accessTokenExpires: refreshed.accessTokenExpires,
196
297
  zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
197
- error: undefined,
298
+ error: undefined, // Clear any previous error
198
299
  };
199
300
  } catch (error) {
200
301
  // eslint-disable-next-line no-console
201
- console.error('[auth-next-server] Force refresh failed:', error);
302
+ console.error('[auth-next-server] Token refresh failed:', error);
202
303
  return {
203
304
  ...token,
204
305
  error: 'RefreshTokenError',
@@ -206,79 +307,42 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
206
307
  }
207
308
  }
208
309
 
209
- // Standard session update - merge provided values
310
+ // No refresh token available
210
311
  return {
211
312
  ...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,
313
+ error: 'TokenExpired',
219
314
  };
315
+ } catch (error) {
316
+ // eslint-disable-next-line no-console
317
+ console.error('[auth-next-server] JWT callback error:', error);
318
+ throw error;
220
319
  }
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
320
  },
263
321
 
264
322
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
323
  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
- };
324
+ try {
325
+ // Expose token data to the session
326
+ return {
327
+ ...session,
328
+ user: {
329
+ ...session.user,
330
+ sub: token.sub as string,
331
+ email: token.email as string | undefined,
332
+ nickname: token.nickname as string | undefined,
333
+ },
334
+ accessToken: token.accessToken as string,
335
+ refreshToken: token.refreshToken as string | undefined,
336
+ idToken: token.idToken as string | undefined,
337
+ accessTokenExpires: token.accessTokenExpires as number,
338
+ zkEvm: token.zkEvm,
339
+ ...(token.error && { error: token.error as string }),
340
+ };
341
+ } catch (error) {
342
+ // eslint-disable-next-line no-console
343
+ console.error('[auth-next-server] Session callback error:', error);
344
+ throw error;
345
+ }
282
346
  },
283
347
  },
284
348
 
package/src/constants.ts CHANGED
@@ -44,8 +44,29 @@ export const DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1000;
44
44
  */
45
45
  export const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
46
46
 
47
+ /**
48
+ * Buffer time in milliseconds before token expiry to trigger refresh.
49
+ * Used by auth-next-client for client-side refresh timing.
50
+ */
51
+ export const TOKEN_EXPIRY_BUFFER_MS = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
52
+
47
53
  /**
48
54
  * Default session max age in seconds (365 days)
49
55
  * This is how long the NextAuth session cookie will be valid
50
56
  */
51
57
  export const DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
58
+
59
+ /**
60
+ * Sandbox client ID for auth-next zero-config.
61
+ */
62
+ export const DEFAULT_SANDBOX_CLIENT_ID = 'mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo';
63
+
64
+ /**
65
+ * Default redirect URI path for sandbox zero-config.
66
+ */
67
+ export const DEFAULT_REDIRECT_URI_PATH = '/callback';
68
+
69
+ /**
70
+ * Default logout redirect URI path
71
+ */
72
+ export const DEFAULT_LOGOUT_REDIRECT_URI_PATH = '/';
package/src/index.ts CHANGED
@@ -45,6 +45,18 @@ export {
45
45
  type RefreshedTokens,
46
46
  type ZkEvmData,
47
47
  } from './refresh';
48
+ export {
49
+ DEFAULT_AUTH_DOMAIN,
50
+ DEFAULT_AUDIENCE,
51
+ DEFAULT_SCOPE,
52
+ IMMUTABLE_PROVIDER_ID,
53
+ DEFAULT_NEXTAUTH_BASE_PATH,
54
+ DEFAULT_SANDBOX_CLIENT_ID,
55
+ DEFAULT_REDIRECT_URI_PATH,
56
+ DEFAULT_LOGOUT_REDIRECT_URI_PATH,
57
+ DEFAULT_TOKEN_EXPIRY_MS,
58
+ TOKEN_EXPIRY_BUFFER_MS,
59
+ } from './constants';
48
60
 
49
61
  // ============================================================================
50
62
  // Type exports