@imtbl/auth-next-server 2.12.5-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +17 -0
- package/LICENSE.md +176 -0
- package/dist/node/config.d.ts +21 -0
- package/dist/node/constants.d.ts +42 -0
- package/dist/node/index.cjs +436 -0
- package/dist/node/index.d.ts +301 -0
- package/dist/node/index.js +390 -0
- package/dist/node/refresh.d.ts +9 -0
- package/dist/node/types.d.ts +111 -0
- package/dist/node/utils/pathMatch.d.ts +10 -0
- package/jest.config.ts +16 -0
- package/package.json +60 -0
- package/src/config.ts +243 -0
- package/src/constants.ts +51 -0
- package/src/index.ts +662 -0
- package/src/refresh.ts +21 -0
- package/src/types.ts +124 -0
- package/src/utils/pathMatch.ts +16 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +16 -0
- package/tsconfig.types.json +8 -0
- package/tsup.config.ts +29 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @imtbl/auth-next-server
|
|
3
|
+
*
|
|
4
|
+
* Server-side utilities for Immutable Auth.js v5 integration with Next.js.
|
|
5
|
+
* This package has NO dependency on @imtbl/auth and is safe to use in
|
|
6
|
+
* Next.js middleware and Edge Runtime environments.
|
|
7
|
+
*
|
|
8
|
+
* For client-side components (provider, hooks, callback), use @imtbl/auth-next-client.
|
|
9
|
+
*/
|
|
10
|
+
import NextAuthImport from 'next-auth';
|
|
11
|
+
import type { NextAuthConfig, Session } from 'next-auth';
|
|
12
|
+
import { type NextRequest, NextResponse } from 'next/server';
|
|
13
|
+
import type { ImmutableAuthConfig } from './types';
|
|
14
|
+
declare const NextAuth: typeof NextAuthImport;
|
|
15
|
+
/**
|
|
16
|
+
* Auth.js v5 config options that can be overridden.
|
|
17
|
+
* Excludes 'providers' as that's managed internally.
|
|
18
|
+
*/
|
|
19
|
+
export type ImmutableAuthOverrides = Omit<NextAuthConfig, 'providers'>;
|
|
20
|
+
/**
|
|
21
|
+
* Return type of createImmutableAuth - the NextAuth instance with handlers
|
|
22
|
+
*/
|
|
23
|
+
export type ImmutableAuthResult = ReturnType<typeof NextAuth>;
|
|
24
|
+
/**
|
|
25
|
+
* Create an Auth.js v5 instance with Immutable authentication
|
|
26
|
+
*
|
|
27
|
+
* @param config - Immutable auth configuration
|
|
28
|
+
* @param overrides - Optional Auth.js options to override defaults
|
|
29
|
+
* @returns NextAuth instance with { handlers, auth, signIn, signOut }
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Callback composition: The `jwt` and `session` callbacks are composed rather than
|
|
33
|
+
* replaced. Internal callbacks run first (handling token storage and refresh), then
|
|
34
|
+
* your custom callbacks receive the result. Other callbacks (`signIn`, `redirect`)
|
|
35
|
+
* are replaced entirely if provided.
|
|
36
|
+
*
|
|
37
|
+
* @example Basic usage (App Router)
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // lib/auth.ts
|
|
40
|
+
* import { createImmutableAuth } from "@imtbl/auth-next-server";
|
|
41
|
+
*
|
|
42
|
+
* export const { handlers, auth, signIn, signOut } = createImmutableAuth({
|
|
43
|
+
* clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
|
|
44
|
+
* redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // app/api/auth/[...nextauth]/route.ts
|
|
48
|
+
* import { handlers } from "@/lib/auth";
|
|
49
|
+
* export const { GET, POST } = handlers;
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function createImmutableAuth(config: ImmutableAuthConfig, overrides?: ImmutableAuthOverrides): ImmutableAuthResult;
|
|
53
|
+
export { createAuthConfig, createAuthOptions } from './config';
|
|
54
|
+
export type { ImmutableAuthConfig, ImmutableTokenData, UserInfoResponse, ZkEvmUser, ImmutableUser, } from './types';
|
|
55
|
+
/**
|
|
56
|
+
* Result from getValidSession indicating auth state
|
|
57
|
+
*/
|
|
58
|
+
export type ValidSessionResult = {
|
|
59
|
+
status: 'authenticated';
|
|
60
|
+
session: Session;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'token_expired';
|
|
63
|
+
session: Session;
|
|
64
|
+
} | {
|
|
65
|
+
status: 'unauthenticated';
|
|
66
|
+
session: null;
|
|
67
|
+
} | {
|
|
68
|
+
status: 'error';
|
|
69
|
+
session: Session;
|
|
70
|
+
error: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Auth props to pass to components - enables automatic SSR/CSR switching.
|
|
74
|
+
* When token is valid, session contains accessToken for immediate use.
|
|
75
|
+
* When token is expired, ssr is false and component should fetch client-side.
|
|
76
|
+
*/
|
|
77
|
+
export interface AuthProps {
|
|
78
|
+
/** Session with valid tokens, or null if token expired/unauthenticated */
|
|
79
|
+
session: Session | null;
|
|
80
|
+
/** If true, SSR data fetching occurred with valid token */
|
|
81
|
+
ssr: boolean;
|
|
82
|
+
/** Auth error that requires user action (not TokenExpired) */
|
|
83
|
+
authError?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Auth props with pre-fetched data for SSR hydration.
|
|
87
|
+
* Extends AuthProps with optional data that was fetched server-side.
|
|
88
|
+
*/
|
|
89
|
+
export interface AuthPropsWithData<T> extends AuthProps {
|
|
90
|
+
/** Pre-fetched data from server (null if SSR was skipped or fetch failed) */
|
|
91
|
+
data: T | null;
|
|
92
|
+
/** Error message if server-side fetch failed */
|
|
93
|
+
fetchError?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Auth props without the authError field.
|
|
97
|
+
* Used when auth error handling is automatic via onAuthError callback.
|
|
98
|
+
*/
|
|
99
|
+
export interface ProtectedAuthProps {
|
|
100
|
+
/** Session with valid tokens, or null if token expired/unauthenticated */
|
|
101
|
+
session: Session | null;
|
|
102
|
+
/** If true, SSR data fetching occurred with valid token */
|
|
103
|
+
ssr: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Protected auth props with pre-fetched data.
|
|
107
|
+
* Used when auth error handling is automatic via onAuthError callback.
|
|
108
|
+
*/
|
|
109
|
+
export interface ProtectedAuthPropsWithData<T> extends ProtectedAuthProps {
|
|
110
|
+
/** Pre-fetched data from server (null if SSR was skipped or fetch failed) */
|
|
111
|
+
data: T | null;
|
|
112
|
+
/** Error message if server-side fetch failed */
|
|
113
|
+
fetchError?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Type for the auth function returned by createImmutableAuth
|
|
117
|
+
*/
|
|
118
|
+
export type AuthFunction = () => Promise<Session | null>;
|
|
119
|
+
/**
|
|
120
|
+
* Get auth props for passing to Client Components (without data fetching).
|
|
121
|
+
* Use this when you want to handle data fetching separately or client-side only.
|
|
122
|
+
*
|
|
123
|
+
* For SSR data fetching, use `getAuthenticatedData` instead.
|
|
124
|
+
*
|
|
125
|
+
* @param auth - The auth function from createImmutableAuth
|
|
126
|
+
* @returns AuthProps with session and ssr flag
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const authProps = await getAuthProps(auth);
|
|
131
|
+
* if (authProps.authError) redirect("/login");
|
|
132
|
+
* return <MyComponent {...authProps} />;
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export declare function getAuthProps(auth: AuthFunction): Promise<AuthProps>;
|
|
136
|
+
/**
|
|
137
|
+
* Fetch authenticated data on the server with automatic SSR/CSR switching.
|
|
138
|
+
*
|
|
139
|
+
* This is the recommended pattern for Server Components that need authenticated data:
|
|
140
|
+
* - When token is valid: Fetches data server-side, returns with `ssr: true`
|
|
141
|
+
* - When token is expired: Skips fetch, returns `ssr: false` for client-side handling
|
|
142
|
+
*
|
|
143
|
+
* @param auth - The auth function from createImmutableAuth
|
|
144
|
+
* @param fetcher - Async function that receives access token and returns data
|
|
145
|
+
* @returns AuthPropsWithData containing session, ssr flag, and pre-fetched data
|
|
146
|
+
*/
|
|
147
|
+
export declare function getAuthenticatedData<T>(auth: AuthFunction, fetcher: (accessToken: string) => Promise<T>): Promise<AuthPropsWithData<T>>;
|
|
148
|
+
/**
|
|
149
|
+
* Get session with detailed status for Server Components.
|
|
150
|
+
* Use this when you need fine-grained control over different auth states.
|
|
151
|
+
*
|
|
152
|
+
* @param auth - The auth function from createImmutableAuth
|
|
153
|
+
* @returns Object with status and session
|
|
154
|
+
*/
|
|
155
|
+
export declare function getValidSession(auth: AuthFunction): Promise<ValidSessionResult>;
|
|
156
|
+
/**
|
|
157
|
+
* Auth error handler signature.
|
|
158
|
+
* The handler should either redirect (using Next.js redirect()) or throw an error.
|
|
159
|
+
* It must never return normally - hence the `never` return type.
|
|
160
|
+
*
|
|
161
|
+
* @param error - The auth error (e.g., "RefreshTokenError")
|
|
162
|
+
*/
|
|
163
|
+
export type AuthErrorHandler = (error: string) => never;
|
|
164
|
+
/**
|
|
165
|
+
* Create a protected data fetcher with automatic auth error handling.
|
|
166
|
+
*
|
|
167
|
+
* This eliminates the need to check `authError` on every page. Define the error
|
|
168
|
+
* handling once, and all pages using this fetcher will automatically redirect
|
|
169
|
+
* on auth errors.
|
|
170
|
+
*
|
|
171
|
+
* @param auth - The auth function from createImmutableAuth
|
|
172
|
+
* @param onAuthError - Handler called when there's an auth error (should redirect or throw)
|
|
173
|
+
* @returns A function to fetch protected data without needing authError checks
|
|
174
|
+
*/
|
|
175
|
+
export declare function createProtectedDataFetcher(auth: AuthFunction, onAuthError: AuthErrorHandler): <T>(fetcher: (accessToken: string) => Promise<T>) => Promise<ProtectedAuthPropsWithData<T>>;
|
|
176
|
+
/**
|
|
177
|
+
* Create auth props getter with automatic auth error handling.
|
|
178
|
+
*
|
|
179
|
+
* Similar to createProtectedDataFetcher but for cases where you don't need
|
|
180
|
+
* server-side data fetching.
|
|
181
|
+
*
|
|
182
|
+
* @param auth - The auth function from createImmutableAuth
|
|
183
|
+
* @param onAuthError - Handler called when there's an auth error (should redirect or throw)
|
|
184
|
+
* @returns A function to get auth props without needing authError checks
|
|
185
|
+
*/
|
|
186
|
+
export declare function createProtectedAuthProps(auth: AuthFunction, onAuthError: AuthErrorHandler): () => Promise<ProtectedAuthProps>;
|
|
187
|
+
/**
|
|
188
|
+
* Result of createProtectedFetchers
|
|
189
|
+
*/
|
|
190
|
+
export interface ProtectedFetchers {
|
|
191
|
+
/**
|
|
192
|
+
* Get auth props with automatic auth error handling.
|
|
193
|
+
* No data fetching - use when you only need session/auth state.
|
|
194
|
+
*/
|
|
195
|
+
getAuthProps: () => Promise<ProtectedAuthProps>;
|
|
196
|
+
/**
|
|
197
|
+
* Fetch authenticated data with automatic auth error handling.
|
|
198
|
+
* Use for SSR data fetching with automatic fallback.
|
|
199
|
+
*/
|
|
200
|
+
getData: <T>(fetcher: (accessToken: string) => Promise<T>) => Promise<ProtectedAuthPropsWithData<T>>;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create protected fetchers with centralized auth error handling.
|
|
204
|
+
*
|
|
205
|
+
* This is the recommended way to set up auth error handling once and use it
|
|
206
|
+
* across all protected pages. Define your error handler once, then use the
|
|
207
|
+
* returned functions without needing to check authError on each page.
|
|
208
|
+
*
|
|
209
|
+
* @param auth - The auth function from createImmutableAuth
|
|
210
|
+
* @param onAuthError - Handler called when there's an auth error (should redirect or throw)
|
|
211
|
+
* @returns Object with getAuthProps and getData functions
|
|
212
|
+
*/
|
|
213
|
+
export declare function createProtectedFetchers(auth: AuthFunction, onAuthError: AuthErrorHandler): ProtectedFetchers;
|
|
214
|
+
/**
|
|
215
|
+
* Options for withServerAuth
|
|
216
|
+
*/
|
|
217
|
+
export interface WithServerAuthOptions<TFallback> {
|
|
218
|
+
/**
|
|
219
|
+
* Content to render when token is expired.
|
|
220
|
+
* This should typically be a Client Component that will refresh tokens and fetch data.
|
|
221
|
+
* If not provided, the serverRender function will still be called with the expired session.
|
|
222
|
+
*/
|
|
223
|
+
onTokenExpired?: TFallback | (() => TFallback);
|
|
224
|
+
/**
|
|
225
|
+
* Content to render when user is not authenticated at all.
|
|
226
|
+
* If not provided, throws an error.
|
|
227
|
+
*/
|
|
228
|
+
onUnauthenticated?: TFallback | (() => TFallback);
|
|
229
|
+
/**
|
|
230
|
+
* Content to render when there's an auth error (e.g., refresh token invalid).
|
|
231
|
+
* If not provided, throws an error.
|
|
232
|
+
*/
|
|
233
|
+
onError?: TFallback | ((error: string) => TFallback);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Helper for Server Components that need authenticated data.
|
|
237
|
+
* Automatically handles token expiration by rendering a client fallback.
|
|
238
|
+
*
|
|
239
|
+
* @param auth - The auth function from createImmutableAuth
|
|
240
|
+
* @param serverRender - Async function that receives valid session and returns JSX
|
|
241
|
+
* @param options - Fallback options for different auth states
|
|
242
|
+
* @returns The rendered content based on auth state
|
|
243
|
+
*/
|
|
244
|
+
export declare function withServerAuth<TResult, TFallback = TResult>(auth: AuthFunction, serverRender: (session: Session) => Promise<TResult>, options?: WithServerAuthOptions<TFallback>): Promise<TResult | TFallback>;
|
|
245
|
+
/**
|
|
246
|
+
* Options for createAuthMiddleware
|
|
247
|
+
*/
|
|
248
|
+
export interface AuthMiddlewareOptions {
|
|
249
|
+
/**
|
|
250
|
+
* URL to redirect to when not authenticated
|
|
251
|
+
* @default "/login"
|
|
252
|
+
*/
|
|
253
|
+
loginUrl?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Paths that should be protected (regex patterns)
|
|
256
|
+
* If not provided, middleware should be configured via Next.js matcher
|
|
257
|
+
*/
|
|
258
|
+
protectedPaths?: (string | RegExp)[];
|
|
259
|
+
/**
|
|
260
|
+
* Paths that should be excluded from protection (regex patterns)
|
|
261
|
+
* Takes precedence over protectedPaths
|
|
262
|
+
*/
|
|
263
|
+
publicPaths?: (string | RegExp)[];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Create a Next.js middleware for protecting routes with Immutable authentication.
|
|
267
|
+
*
|
|
268
|
+
* This is the App Router replacement for `withPageAuthRequired`.
|
|
269
|
+
*
|
|
270
|
+
* @param auth - The auth function from createImmutableAuth
|
|
271
|
+
* @param options - Middleware options
|
|
272
|
+
* @returns A Next.js middleware function
|
|
273
|
+
*
|
|
274
|
+
* @example Basic usage with Next.js middleware:
|
|
275
|
+
* ```typescript
|
|
276
|
+
* // middleware.ts
|
|
277
|
+
* import { createAuthMiddleware } from "@imtbl/auth-next-server";
|
|
278
|
+
* import { auth } from "@/lib/auth";
|
|
279
|
+
*
|
|
280
|
+
* export default createAuthMiddleware(auth, {
|
|
281
|
+
* loginUrl: "/login",
|
|
282
|
+
* });
|
|
283
|
+
*
|
|
284
|
+
* export const config = {
|
|
285
|
+
* matcher: ["/dashboard/:path*", "/profile/:path*"],
|
|
286
|
+
* };
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
export declare function createAuthMiddleware(auth: AuthFunction, options?: AuthMiddlewareOptions): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
290
|
+
/**
|
|
291
|
+
* Higher-order function to protect a Server Action or Route Handler.
|
|
292
|
+
*
|
|
293
|
+
* The returned function forwards all arguments from Next.js to your handler,
|
|
294
|
+
* allowing access to the request, context, form data, or any other arguments.
|
|
295
|
+
*
|
|
296
|
+
* @param auth - The auth function from createImmutableAuth
|
|
297
|
+
* @param handler - The handler function to protect. Receives session as first arg,
|
|
298
|
+
* followed by any arguments passed by Next.js (request, context, etc.)
|
|
299
|
+
* @returns A protected handler that checks authentication before executing
|
|
300
|
+
*/
|
|
301
|
+
export declare function withAuth<TArgs extends unknown[], TReturn>(auth: AuthFunction, handler: (session: Session, ...args: TArgs) => Promise<TReturn>): (...args: TArgs) => Promise<TReturn>;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import NextAuthImport from "next-auth";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import CredentialsImport from "next-auth/providers/credentials";
|
|
7
|
+
|
|
8
|
+
// src/constants.ts
|
|
9
|
+
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
10
|
+
var IMMUTABLE_PROVIDER_ID = "immutable";
|
|
11
|
+
var DEFAULT_TOKEN_EXPIRY_SECONDS = 900;
|
|
12
|
+
var DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
13
|
+
var TOKEN_EXPIRY_BUFFER_SECONDS = 60;
|
|
14
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
15
|
+
|
|
16
|
+
// src/refresh.ts
|
|
17
|
+
function isTokenExpired(accessTokenExpires, bufferSeconds = TOKEN_EXPIRY_BUFFER_SECONDS) {
|
|
18
|
+
if (typeof accessTokenExpires !== "number" || Number.isNaN(accessTokenExpires)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return Date.now() >= accessTokenExpires - bufferSeconds * 1e3;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/config.ts
|
|
25
|
+
var Credentials = CredentialsImport.default || CredentialsImport;
|
|
26
|
+
async function validateTokens(accessToken, authDomain) {
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(`${authDomain}/userinfo`, {
|
|
29
|
+
method: "GET",
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${accessToken}`
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
console.error("[auth-next-server] Token validation failed:", response.status, response.statusText);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return await response.json();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("[auth-next-server] Token validation error:", error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function createAuthConfig(config) {
|
|
45
|
+
const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
46
|
+
return {
|
|
47
|
+
providers: [
|
|
48
|
+
Credentials({
|
|
49
|
+
id: IMMUTABLE_PROVIDER_ID,
|
|
50
|
+
name: "Immutable",
|
|
51
|
+
credentials: {
|
|
52
|
+
tokens: { label: "Tokens", type: "text" }
|
|
53
|
+
},
|
|
54
|
+
async authorize(credentials) {
|
|
55
|
+
if (!credentials?.tokens || typeof credentials.tokens !== "string") {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let tokenData;
|
|
59
|
+
try {
|
|
60
|
+
tokenData = JSON.parse(credentials.tokens);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("[auth-next-server] Failed to parse token data:", error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!tokenData.accessToken || typeof tokenData.accessToken !== "string" || !tokenData.profile || typeof tokenData.profile !== "object" || !tokenData.profile.sub || typeof tokenData.profile.sub !== "string" || typeof tokenData.accessTokenExpires !== "number" || Number.isNaN(tokenData.accessTokenExpires)) {
|
|
66
|
+
console.error("[auth-next-server] Invalid token data structure - missing required fields");
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const userInfo = await validateTokens(tokenData.accessToken, authDomain);
|
|
70
|
+
if (!userInfo) {
|
|
71
|
+
console.error("[auth-next-server] Token validation failed - rejecting authentication");
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (userInfo.sub !== tokenData.profile.sub) {
|
|
75
|
+
console.error(
|
|
76
|
+
"[auth-next-server] User ID mismatch - userinfo sub:",
|
|
77
|
+
userInfo.sub,
|
|
78
|
+
"provided sub:",
|
|
79
|
+
tokenData.profile.sub
|
|
80
|
+
);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
id: userInfo.sub,
|
|
85
|
+
sub: userInfo.sub,
|
|
86
|
+
email: userInfo.email ?? tokenData.profile.email,
|
|
87
|
+
nickname: userInfo.nickname ?? tokenData.profile.nickname,
|
|
88
|
+
accessToken: tokenData.accessToken,
|
|
89
|
+
refreshToken: tokenData.refreshToken,
|
|
90
|
+
idToken: tokenData.idToken,
|
|
91
|
+
accessTokenExpires: tokenData.accessTokenExpires,
|
|
92
|
+
zkEvm: tokenData.zkEvm
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
],
|
|
97
|
+
callbacks: {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
async jwt({
|
|
100
|
+
token,
|
|
101
|
+
user,
|
|
102
|
+
trigger,
|
|
103
|
+
session: sessionUpdate
|
|
104
|
+
}) {
|
|
105
|
+
if (user) {
|
|
106
|
+
return {
|
|
107
|
+
...token,
|
|
108
|
+
sub: user.sub,
|
|
109
|
+
email: user.email,
|
|
110
|
+
nickname: user.nickname,
|
|
111
|
+
accessToken: user.accessToken,
|
|
112
|
+
refreshToken: user.refreshToken,
|
|
113
|
+
idToken: user.idToken,
|
|
114
|
+
accessTokenExpires: user.accessTokenExpires,
|
|
115
|
+
zkEvm: user.zkEvm
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (trigger === "update" && sessionUpdate) {
|
|
119
|
+
const update = sessionUpdate;
|
|
120
|
+
return {
|
|
121
|
+
...token,
|
|
122
|
+
...update.accessToken ? { accessToken: update.accessToken } : {},
|
|
123
|
+
...update.refreshToken ? { refreshToken: update.refreshToken } : {},
|
|
124
|
+
...update.idToken ? { idToken: update.idToken } : {},
|
|
125
|
+
...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
|
|
126
|
+
...update.zkEvm ? { zkEvm: update.zkEvm } : {},
|
|
127
|
+
// Clear any stale error when valid tokens are synced from client-side
|
|
128
|
+
error: void 0
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (!isTokenExpired(token.accessTokenExpires)) {
|
|
132
|
+
return token;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...token,
|
|
136
|
+
error: "TokenExpired"
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
async session({ session, token }) {
|
|
141
|
+
return {
|
|
142
|
+
...session,
|
|
143
|
+
user: {
|
|
144
|
+
...session.user,
|
|
145
|
+
sub: token.sub,
|
|
146
|
+
email: token.email,
|
|
147
|
+
nickname: token.nickname
|
|
148
|
+
},
|
|
149
|
+
accessToken: token.accessToken,
|
|
150
|
+
refreshToken: token.refreshToken,
|
|
151
|
+
idToken: token.idToken,
|
|
152
|
+
accessTokenExpires: token.accessTokenExpires,
|
|
153
|
+
zkEvm: token.zkEvm,
|
|
154
|
+
...token.error && { error: token.error }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
session: {
|
|
159
|
+
strategy: "jwt",
|
|
160
|
+
// Session max age in seconds (365 days default)
|
|
161
|
+
maxAge: DEFAULT_SESSION_MAX_AGE_SECONDS
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
var createAuthOptions = createAuthConfig;
|
|
166
|
+
|
|
167
|
+
// src/utils/pathMatch.ts
|
|
168
|
+
function matchPathPrefix(pathname, pattern) {
|
|
169
|
+
if (pathname === pattern) return true;
|
|
170
|
+
const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
|
|
171
|
+
return pathname.startsWith(prefix);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/index.ts
|
|
175
|
+
var NextAuth = NextAuthImport.default || NextAuthImport;
|
|
176
|
+
function createImmutableAuth(config, overrides) {
|
|
177
|
+
const baseConfig = createAuthConfig(config);
|
|
178
|
+
if (!overrides) {
|
|
179
|
+
return NextAuth(baseConfig);
|
|
180
|
+
}
|
|
181
|
+
const { callbacks: overrideCallbacks, ...otherOverrides } = overrides;
|
|
182
|
+
const composedCallbacks = { ...baseConfig.callbacks };
|
|
183
|
+
if (overrideCallbacks) {
|
|
184
|
+
if (overrideCallbacks.jwt) {
|
|
185
|
+
const baseJwt = baseConfig.callbacks?.jwt;
|
|
186
|
+
const userJwt = overrideCallbacks.jwt;
|
|
187
|
+
composedCallbacks.jwt = async (params) => {
|
|
188
|
+
const result = baseJwt ? await baseJwt(params) : params.token;
|
|
189
|
+
return userJwt({ ...params, token: result });
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (overrideCallbacks.session) {
|
|
193
|
+
const baseSession = baseConfig.callbacks?.session;
|
|
194
|
+
const userSession = overrideCallbacks.session;
|
|
195
|
+
composedCallbacks.session = async (params) => {
|
|
196
|
+
const result = baseSession ? await baseSession(params) : params.session;
|
|
197
|
+
return userSession({ ...params, session: result });
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (overrideCallbacks.signIn) {
|
|
201
|
+
composedCallbacks.signIn = overrideCallbacks.signIn;
|
|
202
|
+
}
|
|
203
|
+
if (overrideCallbacks.redirect) {
|
|
204
|
+
composedCallbacks.redirect = overrideCallbacks.redirect;
|
|
205
|
+
}
|
|
206
|
+
if (overrideCallbacks.authorized) {
|
|
207
|
+
composedCallbacks.authorized = overrideCallbacks.authorized;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const mergedConfig = {
|
|
211
|
+
...baseConfig,
|
|
212
|
+
...otherOverrides,
|
|
213
|
+
callbacks: composedCallbacks
|
|
214
|
+
};
|
|
215
|
+
return NextAuth(mergedConfig);
|
|
216
|
+
}
|
|
217
|
+
async function getAuthProps(auth) {
|
|
218
|
+
const session = await auth();
|
|
219
|
+
if (!session) {
|
|
220
|
+
return { session: null, ssr: false };
|
|
221
|
+
}
|
|
222
|
+
if (session.error === "TokenExpired") {
|
|
223
|
+
return { session: null, ssr: false };
|
|
224
|
+
}
|
|
225
|
+
if (session.error) {
|
|
226
|
+
return { session: null, ssr: false, authError: session.error };
|
|
227
|
+
}
|
|
228
|
+
return { session, ssr: true };
|
|
229
|
+
}
|
|
230
|
+
async function getAuthenticatedData(auth, fetcher) {
|
|
231
|
+
const session = await auth();
|
|
232
|
+
if (!session) {
|
|
233
|
+
return { session: null, ssr: false, data: null };
|
|
234
|
+
}
|
|
235
|
+
if (session.error === "TokenExpired") {
|
|
236
|
+
return { session: null, ssr: false, data: null };
|
|
237
|
+
}
|
|
238
|
+
if (session.error) {
|
|
239
|
+
return {
|
|
240
|
+
session: null,
|
|
241
|
+
ssr: false,
|
|
242
|
+
data: null,
|
|
243
|
+
authError: session.error
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const data = await fetcher(session.accessToken);
|
|
248
|
+
return { session, ssr: true, data };
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
251
|
+
return {
|
|
252
|
+
session,
|
|
253
|
+
ssr: true,
|
|
254
|
+
data: null,
|
|
255
|
+
fetchError: errorMessage
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function getValidSession(auth) {
|
|
260
|
+
const session = await auth();
|
|
261
|
+
if (!session) {
|
|
262
|
+
return { status: "unauthenticated", session: null };
|
|
263
|
+
}
|
|
264
|
+
if (!session.error) {
|
|
265
|
+
return { status: "authenticated", session };
|
|
266
|
+
}
|
|
267
|
+
if (session.error === "TokenExpired") {
|
|
268
|
+
return { status: "token_expired", session };
|
|
269
|
+
}
|
|
270
|
+
return { status: "error", session, error: session.error };
|
|
271
|
+
}
|
|
272
|
+
function createProtectedDataFetcher(auth, onAuthError) {
|
|
273
|
+
return async function getProtectedData(fetcher) {
|
|
274
|
+
const result = await getAuthenticatedData(auth, fetcher);
|
|
275
|
+
if (result.authError) {
|
|
276
|
+
onAuthError(result.authError);
|
|
277
|
+
}
|
|
278
|
+
const { authError: handledAuthError, ...props } = result;
|
|
279
|
+
return props;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function createProtectedAuthProps(auth, onAuthError) {
|
|
283
|
+
return async function getProtectedAuth() {
|
|
284
|
+
const result = await getAuthProps(auth);
|
|
285
|
+
if (result.authError) {
|
|
286
|
+
onAuthError(result.authError);
|
|
287
|
+
}
|
|
288
|
+
const { authError: handledAuthError, ...props } = result;
|
|
289
|
+
return props;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function createProtectedFetchers(auth, onAuthError) {
|
|
293
|
+
return {
|
|
294
|
+
getAuthProps: createProtectedAuthProps(auth, onAuthError),
|
|
295
|
+
getData: createProtectedDataFetcher(auth, onAuthError)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function withServerAuth(auth, serverRender, options = {}) {
|
|
299
|
+
const result = await getValidSession(auth);
|
|
300
|
+
switch (result.status) {
|
|
301
|
+
case "authenticated":
|
|
302
|
+
return serverRender(result.session);
|
|
303
|
+
case "token_expired":
|
|
304
|
+
if (options.onTokenExpired !== void 0) {
|
|
305
|
+
return typeof options.onTokenExpired === "function" ? options.onTokenExpired() : options.onTokenExpired;
|
|
306
|
+
}
|
|
307
|
+
return serverRender(result.session);
|
|
308
|
+
case "unauthenticated":
|
|
309
|
+
if (options.onUnauthenticated !== void 0) {
|
|
310
|
+
return typeof options.onUnauthenticated === "function" ? options.onUnauthenticated() : options.onUnauthenticated;
|
|
311
|
+
}
|
|
312
|
+
throw new Error("Unauthorized: No active session");
|
|
313
|
+
case "error":
|
|
314
|
+
if (options.onError !== void 0) {
|
|
315
|
+
return typeof options.onError === "function" ? options.onError(result.error) : options.onError;
|
|
316
|
+
}
|
|
317
|
+
throw new Error(`Unauthorized: ${result.error}`);
|
|
318
|
+
default:
|
|
319
|
+
throw new Error("Unknown auth state");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function createAuthMiddleware(auth, options = {}) {
|
|
323
|
+
const { loginUrl = "/login", protectedPaths, publicPaths } = options;
|
|
324
|
+
return async function middleware(request) {
|
|
325
|
+
const { pathname } = request.nextUrl;
|
|
326
|
+
if (publicPaths) {
|
|
327
|
+
const isPublic = publicPaths.some((pattern) => {
|
|
328
|
+
if (typeof pattern === "string") {
|
|
329
|
+
return matchPathPrefix(pathname, pattern);
|
|
330
|
+
}
|
|
331
|
+
return pattern.test(pathname);
|
|
332
|
+
});
|
|
333
|
+
if (isPublic) {
|
|
334
|
+
return NextResponse.next();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (protectedPaths) {
|
|
338
|
+
const isProtected = protectedPaths.some((pattern) => {
|
|
339
|
+
if (typeof pattern === "string") {
|
|
340
|
+
return matchPathPrefix(pathname, pattern);
|
|
341
|
+
}
|
|
342
|
+
return pattern.test(pathname);
|
|
343
|
+
});
|
|
344
|
+
if (!isProtected) {
|
|
345
|
+
return NextResponse.next();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const session = await auth();
|
|
349
|
+
if (!session) {
|
|
350
|
+
const url = new URL(loginUrl, request.url);
|
|
351
|
+
const returnTo = request.nextUrl.search ? `${pathname}${request.nextUrl.search}` : pathname;
|
|
352
|
+
url.searchParams.set("returnTo", returnTo);
|
|
353
|
+
return NextResponse.redirect(url);
|
|
354
|
+
}
|
|
355
|
+
if (session.error && session.error !== "TokenExpired") {
|
|
356
|
+
const url = new URL(loginUrl, request.url);
|
|
357
|
+
const returnTo = request.nextUrl.search ? `${pathname}${request.nextUrl.search}` : pathname;
|
|
358
|
+
url.searchParams.set("returnTo", returnTo);
|
|
359
|
+
url.searchParams.set("error", session.error);
|
|
360
|
+
return NextResponse.redirect(url);
|
|
361
|
+
}
|
|
362
|
+
return NextResponse.next();
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function withAuth(auth, handler) {
|
|
366
|
+
return async (...args) => {
|
|
367
|
+
const session = await auth();
|
|
368
|
+
if (!session) {
|
|
369
|
+
throw new Error("Unauthorized: No active session");
|
|
370
|
+
}
|
|
371
|
+
if (session.error && session.error !== "TokenExpired") {
|
|
372
|
+
throw new Error(`Unauthorized: ${session.error}`);
|
|
373
|
+
}
|
|
374
|
+
return handler(session, ...args);
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
export {
|
|
378
|
+
createAuthConfig,
|
|
379
|
+
createAuthMiddleware,
|
|
380
|
+
createAuthOptions,
|
|
381
|
+
createImmutableAuth,
|
|
382
|
+
createProtectedAuthProps,
|
|
383
|
+
createProtectedDataFetcher,
|
|
384
|
+
createProtectedFetchers,
|
|
385
|
+
getAuthProps,
|
|
386
|
+
getAuthenticatedData,
|
|
387
|
+
getValidSession,
|
|
388
|
+
withAuth,
|
|
389
|
+
withServerAuth
|
|
390
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if the access token is expired or about to expire
|
|
3
|
+
* Returns true if token expires within the buffer time (default 60 seconds)
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* If accessTokenExpires is not a valid number (undefined, null, NaN),
|
|
7
|
+
* returns true to trigger a refresh as a safety measure.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isTokenExpired(accessTokenExpires: number, bufferSeconds?: number): boolean;
|