@imtbl/auth-nextjs 0.0.1-alpha.0 → 2.12.4-alpha.4
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/dist/node/chunk-OPPMGNFZ.js +210 -0
- package/dist/node/client/index.cjs +375 -0
- package/dist/node/client/index.js +359 -0
- package/dist/node/index.cjs +293 -0
- package/dist/node/index.js +53 -0
- package/dist/node/server/index.cjs +300 -0
- package/dist/node/server/index.js +57 -0
- package/dist/types/client/callback.d.ts +37 -0
- package/dist/types/client/index.d.ts +3 -0
- package/dist/types/client/provider.d.ts +70 -0
- package/dist/types/config.d.ts +23 -0
- package/dist/types/constants.d.ts +42 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/refresh.d.ts +16 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/with-page-auth.d.ts +94 -0
- package/dist/types/types.d.ts +191 -0
- package/dist/types/utils/token.d.ts +8 -0
- package/package.json +2 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/client/provider.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
useCallback,
|
|
11
|
+
useMemo
|
|
12
|
+
} from "react";
|
|
13
|
+
import {
|
|
14
|
+
SessionProvider,
|
|
15
|
+
useSession,
|
|
16
|
+
signIn,
|
|
17
|
+
signOut
|
|
18
|
+
} from "next-auth/react";
|
|
19
|
+
import { Auth } from "@imtbl/auth";
|
|
20
|
+
|
|
21
|
+
// src/utils/token.ts
|
|
22
|
+
import { decodeJwtPayload } from "@imtbl/auth";
|
|
23
|
+
|
|
24
|
+
// src/constants.ts
|
|
25
|
+
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
26
|
+
var DEFAULT_AUDIENCE = "platform_api";
|
|
27
|
+
var DEFAULT_SCOPE = "openid profile email offline_access transact";
|
|
28
|
+
var IMMUTABLE_PROVIDER_ID = "immutable";
|
|
29
|
+
var DEFAULT_NEXTAUTH_BASE_PATH = "/api/auth";
|
|
30
|
+
var DEFAULT_TOKEN_EXPIRY_SECONDS = 900;
|
|
31
|
+
var DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
32
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
33
|
+
|
|
34
|
+
// src/utils/token.ts
|
|
35
|
+
function getTokenExpiry(accessToken) {
|
|
36
|
+
if (!accessToken) {
|
|
37
|
+
return Date.now() + DEFAULT_TOKEN_EXPIRY_MS;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const payload = decodeJwtPayload(accessToken);
|
|
41
|
+
if (payload.exp && typeof payload.exp === "number") {
|
|
42
|
+
return payload.exp * 1e3;
|
|
43
|
+
}
|
|
44
|
+
return Date.now() + DEFAULT_TOKEN_EXPIRY_MS;
|
|
45
|
+
} catch {
|
|
46
|
+
return Date.now() + DEFAULT_TOKEN_EXPIRY_MS;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/client/provider.tsx
|
|
51
|
+
import { jsx } from "react/jsx-runtime";
|
|
52
|
+
var ImmutableAuthContext = createContext(null);
|
|
53
|
+
function ImmutableAuthInner({
|
|
54
|
+
children,
|
|
55
|
+
config,
|
|
56
|
+
basePath
|
|
57
|
+
}) {
|
|
58
|
+
const [auth, setAuth] = useState(null);
|
|
59
|
+
const prevConfigRef = useRef(null);
|
|
60
|
+
const [isAuthReady, setIsAuthReady] = useState(false);
|
|
61
|
+
const { data: session, update: updateSession } = useSession();
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (typeof window === "undefined") return;
|
|
64
|
+
const configKey = [
|
|
65
|
+
config.clientId,
|
|
66
|
+
config.redirectUri,
|
|
67
|
+
config.logoutRedirectUri || "",
|
|
68
|
+
config.audience || DEFAULT_AUDIENCE,
|
|
69
|
+
config.scope || DEFAULT_SCOPE,
|
|
70
|
+
config.authenticationDomain || DEFAULT_AUTH_DOMAIN
|
|
71
|
+
].join(":");
|
|
72
|
+
if (prevConfigRef.current === configKey) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
prevConfigRef.current = configKey;
|
|
76
|
+
const newAuth = new Auth({
|
|
77
|
+
clientId: config.clientId,
|
|
78
|
+
redirectUri: config.redirectUri,
|
|
79
|
+
logoutRedirectUri: config.logoutRedirectUri,
|
|
80
|
+
audience: config.audience || DEFAULT_AUDIENCE,
|
|
81
|
+
scope: config.scope || DEFAULT_SCOPE,
|
|
82
|
+
authenticationDomain: config.authenticationDomain || DEFAULT_AUTH_DOMAIN
|
|
83
|
+
});
|
|
84
|
+
setAuth(newAuth);
|
|
85
|
+
setIsAuthReady(true);
|
|
86
|
+
}, [config]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!auth || !isAuthReady) return;
|
|
89
|
+
if (session?.error) return;
|
|
90
|
+
if (!session?.accessToken || !session?.idToken) return;
|
|
91
|
+
const hydrateAuth = async () => {
|
|
92
|
+
try {
|
|
93
|
+
const {
|
|
94
|
+
accessToken,
|
|
95
|
+
idToken,
|
|
96
|
+
refreshToken,
|
|
97
|
+
accessTokenExpires
|
|
98
|
+
} = session;
|
|
99
|
+
if (!accessToken || !idToken) return;
|
|
100
|
+
const existingUser = await auth.getUser();
|
|
101
|
+
if (existingUser) return;
|
|
102
|
+
const expiresIn = accessTokenExpires ? Math.max(0, Math.floor((accessTokenExpires - Date.now()) / 1e3)) : DEFAULT_TOKEN_EXPIRY_SECONDS;
|
|
103
|
+
const tokenResponse = {
|
|
104
|
+
access_token: accessToken,
|
|
105
|
+
refresh_token: refreshToken,
|
|
106
|
+
id_token: idToken,
|
|
107
|
+
token_type: "Bearer",
|
|
108
|
+
expires_in: expiresIn
|
|
109
|
+
};
|
|
110
|
+
await auth.storeTokens(tokenResponse);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn("[auth-nextjs] Failed to hydrate Auth instance:", error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
hydrateAuth();
|
|
116
|
+
}, [auth, isAuthReady, session]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!auth || !isAuthReady) return void 0;
|
|
119
|
+
const handleLoggedIn = async (authUser) => {
|
|
120
|
+
if (session?.accessToken && authUser.accessToken !== session.accessToken) {
|
|
121
|
+
await updateSession({
|
|
122
|
+
accessToken: authUser.accessToken,
|
|
123
|
+
refreshToken: authUser.refreshToken,
|
|
124
|
+
idToken: authUser.idToken,
|
|
125
|
+
accessTokenExpires: getTokenExpiry(authUser.accessToken),
|
|
126
|
+
zkEvm: authUser.zkEvm
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
auth.eventEmitter.on("loggedIn", handleLoggedIn);
|
|
131
|
+
return () => {
|
|
132
|
+
auth.eventEmitter.removeListener("loggedIn", handleLoggedIn);
|
|
133
|
+
};
|
|
134
|
+
}, [auth, isAuthReady, session, updateSession]);
|
|
135
|
+
const contextValue = useMemo(
|
|
136
|
+
() => ({ auth, config, basePath }),
|
|
137
|
+
[auth, config, basePath]
|
|
138
|
+
);
|
|
139
|
+
return /* @__PURE__ */ jsx(ImmutableAuthContext.Provider, { value: contextValue, children });
|
|
140
|
+
}
|
|
141
|
+
function ImmutableAuthProvider({
|
|
142
|
+
children,
|
|
143
|
+
config,
|
|
144
|
+
session,
|
|
145
|
+
basePath = DEFAULT_NEXTAUTH_BASE_PATH
|
|
146
|
+
}) {
|
|
147
|
+
return /* @__PURE__ */ jsx(SessionProvider, { session, basePath, children: /* @__PURE__ */ jsx(ImmutableAuthInner, { config, basePath, children }) });
|
|
148
|
+
}
|
|
149
|
+
function useImmutableAuth() {
|
|
150
|
+
const context = useContext(ImmutableAuthContext);
|
|
151
|
+
const { data: sessionData, status } = useSession();
|
|
152
|
+
if (!context) {
|
|
153
|
+
throw new Error("useImmutableAuth must be used within ImmutableAuthProvider");
|
|
154
|
+
}
|
|
155
|
+
const session = sessionData;
|
|
156
|
+
const { auth } = context;
|
|
157
|
+
const isLoading = status === "loading";
|
|
158
|
+
const isAuthenticated = status === "authenticated" && !!session;
|
|
159
|
+
const user = session?.user ? {
|
|
160
|
+
sub: session.user.sub,
|
|
161
|
+
email: session.user.email,
|
|
162
|
+
nickname: session.user.nickname
|
|
163
|
+
} : null;
|
|
164
|
+
const handleSignIn = useCallback(async () => {
|
|
165
|
+
if (!auth) {
|
|
166
|
+
throw new Error("Auth not initialized");
|
|
167
|
+
}
|
|
168
|
+
const authUser = await auth.login();
|
|
169
|
+
if (!authUser) {
|
|
170
|
+
throw new Error("Login failed");
|
|
171
|
+
}
|
|
172
|
+
const tokenData = {
|
|
173
|
+
accessToken: authUser.accessToken,
|
|
174
|
+
refreshToken: authUser.refreshToken,
|
|
175
|
+
idToken: authUser.idToken,
|
|
176
|
+
accessTokenExpires: getTokenExpiry(authUser.accessToken),
|
|
177
|
+
profile: {
|
|
178
|
+
sub: authUser.profile.sub,
|
|
179
|
+
email: authUser.profile.email,
|
|
180
|
+
nickname: authUser.profile.nickname
|
|
181
|
+
},
|
|
182
|
+
zkEvm: authUser.zkEvm
|
|
183
|
+
};
|
|
184
|
+
const result = await signIn(IMMUTABLE_PROVIDER_ID, {
|
|
185
|
+
tokens: JSON.stringify(tokenData),
|
|
186
|
+
redirect: false
|
|
187
|
+
});
|
|
188
|
+
if (result?.error) {
|
|
189
|
+
throw new Error(`NextAuth sign-in failed: ${result.error}`);
|
|
190
|
+
}
|
|
191
|
+
if (!result?.ok) {
|
|
192
|
+
throw new Error("NextAuth sign-in failed: unknown error");
|
|
193
|
+
}
|
|
194
|
+
}, [auth]);
|
|
195
|
+
const handleSignOut = useCallback(async () => {
|
|
196
|
+
await signOut({ redirect: false });
|
|
197
|
+
if (auth) {
|
|
198
|
+
try {
|
|
199
|
+
await auth.getLogoutUrl();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn("[auth-nextjs] Logout cleanup error:", error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}, [auth]);
|
|
205
|
+
const getAccessToken = useCallback(async () => {
|
|
206
|
+
if (auth) {
|
|
207
|
+
try {
|
|
208
|
+
const token = await auth.getAccessToken();
|
|
209
|
+
if (token) {
|
|
210
|
+
return token;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (session?.error) {
|
|
216
|
+
throw new Error(`Token refresh failed: ${session.error}`);
|
|
217
|
+
}
|
|
218
|
+
if (session?.accessToken) {
|
|
219
|
+
return session.accessToken;
|
|
220
|
+
}
|
|
221
|
+
throw new Error("No access token available");
|
|
222
|
+
}, [auth, session]);
|
|
223
|
+
return {
|
|
224
|
+
user,
|
|
225
|
+
session,
|
|
226
|
+
isLoading,
|
|
227
|
+
isAuthenticated,
|
|
228
|
+
signIn: handleSignIn,
|
|
229
|
+
signOut: handleSignOut,
|
|
230
|
+
getAccessToken,
|
|
231
|
+
auth
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function useAccessToken() {
|
|
235
|
+
const { getAccessToken } = useImmutableAuth();
|
|
236
|
+
return getAccessToken;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/client/callback.tsx
|
|
240
|
+
import { useEffect as useEffect2, useState as useState2, useRef as useRef2 } from "react";
|
|
241
|
+
import { useRouter } from "next/router";
|
|
242
|
+
import { signIn as signIn2 } from "next-auth/react";
|
|
243
|
+
import { Auth as Auth2 } from "@imtbl/auth";
|
|
244
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
245
|
+
function CallbackPage({
|
|
246
|
+
config,
|
|
247
|
+
redirectTo = "/",
|
|
248
|
+
loadingComponent = null,
|
|
249
|
+
errorComponent
|
|
250
|
+
}) {
|
|
251
|
+
const router = useRouter();
|
|
252
|
+
const [error, setError] = useState2(null);
|
|
253
|
+
const callbackProcessedRef = useRef2(false);
|
|
254
|
+
useEffect2(() => {
|
|
255
|
+
const handleCallback = async () => {
|
|
256
|
+
try {
|
|
257
|
+
const auth = new Auth2({
|
|
258
|
+
clientId: config.clientId,
|
|
259
|
+
redirectUri: config.redirectUri,
|
|
260
|
+
logoutRedirectUri: config.logoutRedirectUri,
|
|
261
|
+
audience: config.audience || DEFAULT_AUDIENCE,
|
|
262
|
+
scope: config.scope || DEFAULT_SCOPE,
|
|
263
|
+
authenticationDomain: config.authenticationDomain || DEFAULT_AUTH_DOMAIN
|
|
264
|
+
});
|
|
265
|
+
const authUser = await auth.loginCallback();
|
|
266
|
+
if (window.opener) {
|
|
267
|
+
if (!authUser) {
|
|
268
|
+
throw new Error("Authentication failed: no user data received from login callback");
|
|
269
|
+
}
|
|
270
|
+
window.close();
|
|
271
|
+
} else if (authUser) {
|
|
272
|
+
const tokenData = {
|
|
273
|
+
accessToken: authUser.accessToken,
|
|
274
|
+
refreshToken: authUser.refreshToken,
|
|
275
|
+
idToken: authUser.idToken,
|
|
276
|
+
accessTokenExpires: getTokenExpiry(authUser.accessToken),
|
|
277
|
+
profile: {
|
|
278
|
+
sub: authUser.profile.sub,
|
|
279
|
+
email: authUser.profile.email,
|
|
280
|
+
nickname: authUser.profile.nickname
|
|
281
|
+
},
|
|
282
|
+
zkEvm: authUser.zkEvm
|
|
283
|
+
};
|
|
284
|
+
const result = await signIn2(IMMUTABLE_PROVIDER_ID, {
|
|
285
|
+
tokens: JSON.stringify(tokenData),
|
|
286
|
+
redirect: false
|
|
287
|
+
});
|
|
288
|
+
if (result?.error) {
|
|
289
|
+
throw new Error(`NextAuth sign-in failed: ${result.error}`);
|
|
290
|
+
}
|
|
291
|
+
if (!result?.ok) {
|
|
292
|
+
throw new Error("NextAuth sign-in failed: unknown error");
|
|
293
|
+
}
|
|
294
|
+
router.replace(redirectTo);
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error("Authentication failed: no user data received from login callback");
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
setError(err instanceof Error ? err.message : "Authentication failed");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const handleOAuthError = () => {
|
|
303
|
+
const errorCode = router.query.error;
|
|
304
|
+
const errorDescription = router.query.error_description;
|
|
305
|
+
const errorMessage = errorDescription || errorCode || "Authentication failed";
|
|
306
|
+
setError(errorMessage);
|
|
307
|
+
};
|
|
308
|
+
if (!router.isReady) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (router.query.error) {
|
|
312
|
+
handleOAuthError();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (router.query.code && !callbackProcessedRef.current) {
|
|
316
|
+
callbackProcessedRef.current = true;
|
|
317
|
+
handleCallback();
|
|
318
|
+
}
|
|
319
|
+
}, [
|
|
320
|
+
router.isReady,
|
|
321
|
+
router.query.code,
|
|
322
|
+
router.query.error,
|
|
323
|
+
router.query.error_description,
|
|
324
|
+
router,
|
|
325
|
+
config,
|
|
326
|
+
redirectTo
|
|
327
|
+
]);
|
|
328
|
+
if (error) {
|
|
329
|
+
if (errorComponent) {
|
|
330
|
+
return errorComponent(error);
|
|
331
|
+
}
|
|
332
|
+
return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem", textAlign: "center" }, children: [
|
|
333
|
+
/* @__PURE__ */ jsx2("h2", { style: { color: "#dc3545" }, children: "Authentication Error" }),
|
|
334
|
+
/* @__PURE__ */ jsx2("p", { children: error }),
|
|
335
|
+
/* @__PURE__ */ jsx2(
|
|
336
|
+
"button",
|
|
337
|
+
{
|
|
338
|
+
onClick: () => router.push("/"),
|
|
339
|
+
style: {
|
|
340
|
+
padding: "0.5rem 1rem",
|
|
341
|
+
marginTop: "1rem",
|
|
342
|
+
cursor: "pointer"
|
|
343
|
+
},
|
|
344
|
+
children: "Return to Home"
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
] });
|
|
348
|
+
}
|
|
349
|
+
if (loadingComponent) {
|
|
350
|
+
return loadingComponent;
|
|
351
|
+
}
|
|
352
|
+
return /* @__PURE__ */ jsx2("div", { style: { padding: "2rem", textAlign: "center" }, children: /* @__PURE__ */ jsx2("p", { children: "Completing authentication..." }) });
|
|
353
|
+
}
|
|
354
|
+
export {
|
|
355
|
+
CallbackPage,
|
|
356
|
+
ImmutableAuthProvider,
|
|
357
|
+
useAccessToken,
|
|
358
|
+
useImmutableAuth
|
|
359
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
ImmutableAuth: () => ImmutableAuth,
|
|
34
|
+
isTokenExpired: () => isTokenExpired,
|
|
35
|
+
refreshAccessToken: () => refreshAccessToken
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(src_exports);
|
|
38
|
+
var import_next_auth = __toESM(require("next-auth"), 1);
|
|
39
|
+
|
|
40
|
+
// src/config.ts
|
|
41
|
+
var import_credentials = __toESM(require("next-auth/providers/credentials"), 1);
|
|
42
|
+
|
|
43
|
+
// src/constants.ts
|
|
44
|
+
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
45
|
+
var IMMUTABLE_PROVIDER_ID = "immutable";
|
|
46
|
+
var DEFAULT_TOKEN_EXPIRY_SECONDS = 900;
|
|
47
|
+
var DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
48
|
+
var TOKEN_EXPIRY_BUFFER_SECONDS = 60;
|
|
49
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
50
|
+
|
|
51
|
+
// src/refresh.ts
|
|
52
|
+
async function refreshAccessToken(token, config) {
|
|
53
|
+
const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
54
|
+
if (!token.refreshToken) {
|
|
55
|
+
return {
|
|
56
|
+
...token,
|
|
57
|
+
error: "NoRefreshToken"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`${authDomain}/oauth/token`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
65
|
+
},
|
|
66
|
+
body: new URLSearchParams({
|
|
67
|
+
grant_type: "refresh_token",
|
|
68
|
+
client_id: config.clientId,
|
|
69
|
+
refresh_token: token.refreshToken
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
let errorMessage = `Token refresh failed with status ${response.status}`;
|
|
74
|
+
try {
|
|
75
|
+
const errorData = await response.json();
|
|
76
|
+
if (errorData.error_description || errorData.error) {
|
|
77
|
+
errorMessage = errorData.error_description || errorData.error;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
throw new Error(errorMessage);
|
|
82
|
+
}
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
if (!data.access_token || typeof data.access_token !== "string") {
|
|
85
|
+
throw new Error("Invalid token response: missing access_token");
|
|
86
|
+
}
|
|
87
|
+
const expiresIn = data.expires_in || DEFAULT_TOKEN_EXPIRY_SECONDS;
|
|
88
|
+
const accessTokenExpires = Date.now() + expiresIn * 1e3;
|
|
89
|
+
return {
|
|
90
|
+
...token,
|
|
91
|
+
accessToken: data.access_token,
|
|
92
|
+
refreshToken: data.refresh_token ?? token.refreshToken,
|
|
93
|
+
idToken: data.id_token ?? token.idToken,
|
|
94
|
+
accessTokenExpires,
|
|
95
|
+
error: void 0
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("[auth-nextjs] Failed to refresh token:", error);
|
|
99
|
+
return {
|
|
100
|
+
...token,
|
|
101
|
+
error: "RefreshTokenError"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isTokenExpired(accessTokenExpires, bufferSeconds = TOKEN_EXPIRY_BUFFER_SECONDS) {
|
|
106
|
+
if (typeof accessTokenExpires !== "number" || Number.isNaN(accessTokenExpires)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return Date.now() >= accessTokenExpires - bufferSeconds * 1e3;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/config.ts
|
|
113
|
+
var CredentialsProvider = import_credentials.default.default || import_credentials.default;
|
|
114
|
+
async function validateTokens(accessToken, authDomain) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(`${authDomain}/userinfo`, {
|
|
117
|
+
method: "GET",
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${accessToken}`
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
console.error("[auth-nextjs] Token validation failed:", response.status, response.statusText);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return await response.json();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("[auth-nextjs] Token validation error:", error);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function createAuthOptions(config) {
|
|
133
|
+
const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
134
|
+
return {
|
|
135
|
+
providers: [
|
|
136
|
+
CredentialsProvider({
|
|
137
|
+
id: IMMUTABLE_PROVIDER_ID,
|
|
138
|
+
name: "Immutable",
|
|
139
|
+
credentials: {
|
|
140
|
+
tokens: { label: "Tokens", type: "text" }
|
|
141
|
+
},
|
|
142
|
+
async authorize(credentials) {
|
|
143
|
+
if (!credentials?.tokens) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
let tokenData;
|
|
147
|
+
try {
|
|
148
|
+
tokenData = JSON.parse(credentials.tokens);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("[auth-nextjs] Failed to parse token data:", error);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
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)) {
|
|
154
|
+
console.error("[auth-nextjs] Invalid token data structure - missing required fields");
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const userInfo = await validateTokens(tokenData.accessToken, authDomain);
|
|
158
|
+
if (!userInfo) {
|
|
159
|
+
console.error("[auth-nextjs] Token validation failed - rejecting authentication");
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (userInfo.sub !== tokenData.profile.sub) {
|
|
163
|
+
console.error(
|
|
164
|
+
"[auth-nextjs] User ID mismatch - userinfo sub:",
|
|
165
|
+
userInfo.sub,
|
|
166
|
+
"provided sub:",
|
|
167
|
+
tokenData.profile.sub
|
|
168
|
+
);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
id: userInfo.sub,
|
|
173
|
+
sub: userInfo.sub,
|
|
174
|
+
email: userInfo.email ?? tokenData.profile.email,
|
|
175
|
+
nickname: userInfo.nickname ?? tokenData.profile.nickname,
|
|
176
|
+
accessToken: tokenData.accessToken,
|
|
177
|
+
refreshToken: tokenData.refreshToken,
|
|
178
|
+
idToken: tokenData.idToken,
|
|
179
|
+
accessTokenExpires: tokenData.accessTokenExpires,
|
|
180
|
+
zkEvm: tokenData.zkEvm
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
],
|
|
185
|
+
callbacks: {
|
|
186
|
+
async jwt({
|
|
187
|
+
token,
|
|
188
|
+
user,
|
|
189
|
+
trigger,
|
|
190
|
+
session: sessionUpdate
|
|
191
|
+
}) {
|
|
192
|
+
if (user) {
|
|
193
|
+
return {
|
|
194
|
+
...token,
|
|
195
|
+
sub: user.sub,
|
|
196
|
+
email: user.email,
|
|
197
|
+
nickname: user.nickname,
|
|
198
|
+
accessToken: user.accessToken,
|
|
199
|
+
refreshToken: user.refreshToken,
|
|
200
|
+
idToken: user.idToken,
|
|
201
|
+
accessTokenExpires: user.accessTokenExpires,
|
|
202
|
+
zkEvm: user.zkEvm
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (trigger === "update" && sessionUpdate) {
|
|
206
|
+
return {
|
|
207
|
+
...token,
|
|
208
|
+
...sessionUpdate.accessToken && { accessToken: sessionUpdate.accessToken },
|
|
209
|
+
...sessionUpdate.refreshToken && { refreshToken: sessionUpdate.refreshToken },
|
|
210
|
+
...sessionUpdate.idToken && { idToken: sessionUpdate.idToken },
|
|
211
|
+
...sessionUpdate.accessTokenExpires && { accessTokenExpires: sessionUpdate.accessTokenExpires },
|
|
212
|
+
...sessionUpdate.zkEvm && { zkEvm: sessionUpdate.zkEvm }
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (!isTokenExpired(token.accessTokenExpires)) {
|
|
216
|
+
return token;
|
|
217
|
+
}
|
|
218
|
+
return refreshAccessToken(token, config);
|
|
219
|
+
},
|
|
220
|
+
async session({ session, token }) {
|
|
221
|
+
return {
|
|
222
|
+
...session,
|
|
223
|
+
user: {
|
|
224
|
+
sub: token.sub,
|
|
225
|
+
email: token.email,
|
|
226
|
+
nickname: token.nickname
|
|
227
|
+
},
|
|
228
|
+
accessToken: token.accessToken,
|
|
229
|
+
refreshToken: token.refreshToken,
|
|
230
|
+
idToken: token.idToken,
|
|
231
|
+
accessTokenExpires: token.accessTokenExpires,
|
|
232
|
+
zkEvm: token.zkEvm,
|
|
233
|
+
...token.error && { error: token.error }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
session: {
|
|
238
|
+
strategy: "jwt",
|
|
239
|
+
// Session max age in seconds (30 days default)
|
|
240
|
+
maxAge: DEFAULT_SESSION_MAX_AGE_SECONDS
|
|
241
|
+
},
|
|
242
|
+
// Use NEXTAUTH_SECRET from environment
|
|
243
|
+
secret: process.env.NEXTAUTH_SECRET
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/index.ts
|
|
248
|
+
var NextAuth = import_next_auth.default.default || import_next_auth.default;
|
|
249
|
+
function ImmutableAuth(config, overrides) {
|
|
250
|
+
const authOptions = createAuthOptions(config);
|
|
251
|
+
if (!overrides) {
|
|
252
|
+
return NextAuth(authOptions);
|
|
253
|
+
}
|
|
254
|
+
const composedCallbacks = {
|
|
255
|
+
...authOptions.callbacks
|
|
256
|
+
};
|
|
257
|
+
if (overrides.callbacks) {
|
|
258
|
+
if (overrides.callbacks.jwt) {
|
|
259
|
+
const internalJwt = authOptions.callbacks?.jwt;
|
|
260
|
+
const userJwt = overrides.callbacks.jwt;
|
|
261
|
+
composedCallbacks.jwt = async (params) => {
|
|
262
|
+
const token = internalJwt ? await internalJwt(params) : params.token;
|
|
263
|
+
return userJwt({ ...params, token });
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (overrides.callbacks.session) {
|
|
267
|
+
const internalSession = authOptions.callbacks?.session;
|
|
268
|
+
const userSession = overrides.callbacks.session;
|
|
269
|
+
composedCallbacks.session = async (params) => {
|
|
270
|
+
const session = internalSession ? await internalSession(params) : params.session;
|
|
271
|
+
return userSession({ ...params, session });
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (overrides.callbacks.signIn) {
|
|
275
|
+
composedCallbacks.signIn = overrides.callbacks.signIn;
|
|
276
|
+
}
|
|
277
|
+
if (overrides.callbacks.redirect) {
|
|
278
|
+
composedCallbacks.redirect = overrides.callbacks.redirect;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const mergedOptions = {
|
|
282
|
+
...authOptions,
|
|
283
|
+
...overrides,
|
|
284
|
+
callbacks: composedCallbacks
|
|
285
|
+
};
|
|
286
|
+
return NextAuth(mergedOptions);
|
|
287
|
+
}
|
|
288
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
289
|
+
0 && (module.exports = {
|
|
290
|
+
ImmutableAuth,
|
|
291
|
+
isTokenExpired,
|
|
292
|
+
refreshAccessToken
|
|
293
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAuthOptions,
|
|
3
|
+
isTokenExpired,
|
|
4
|
+
refreshAccessToken
|
|
5
|
+
} from "./chunk-OPPMGNFZ.js";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import NextAuthDefault from "next-auth";
|
|
9
|
+
var NextAuth = NextAuthDefault.default || NextAuthDefault;
|
|
10
|
+
function ImmutableAuth(config, overrides) {
|
|
11
|
+
const authOptions = createAuthOptions(config);
|
|
12
|
+
if (!overrides) {
|
|
13
|
+
return NextAuth(authOptions);
|
|
14
|
+
}
|
|
15
|
+
const composedCallbacks = {
|
|
16
|
+
...authOptions.callbacks
|
|
17
|
+
};
|
|
18
|
+
if (overrides.callbacks) {
|
|
19
|
+
if (overrides.callbacks.jwt) {
|
|
20
|
+
const internalJwt = authOptions.callbacks?.jwt;
|
|
21
|
+
const userJwt = overrides.callbacks.jwt;
|
|
22
|
+
composedCallbacks.jwt = async (params) => {
|
|
23
|
+
const token = internalJwt ? await internalJwt(params) : params.token;
|
|
24
|
+
return userJwt({ ...params, token });
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (overrides.callbacks.session) {
|
|
28
|
+
const internalSession = authOptions.callbacks?.session;
|
|
29
|
+
const userSession = overrides.callbacks.session;
|
|
30
|
+
composedCallbacks.session = async (params) => {
|
|
31
|
+
const session = internalSession ? await internalSession(params) : params.session;
|
|
32
|
+
return userSession({ ...params, session });
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (overrides.callbacks.signIn) {
|
|
36
|
+
composedCallbacks.signIn = overrides.callbacks.signIn;
|
|
37
|
+
}
|
|
38
|
+
if (overrides.callbacks.redirect) {
|
|
39
|
+
composedCallbacks.redirect = overrides.callbacks.redirect;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const mergedOptions = {
|
|
43
|
+
...authOptions,
|
|
44
|
+
...overrides,
|
|
45
|
+
callbacks: composedCallbacks
|
|
46
|
+
};
|
|
47
|
+
return NextAuth(mergedOptions);
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
ImmutableAuth,
|
|
51
|
+
isTokenExpired,
|
|
52
|
+
refreshAccessToken
|
|
53
|
+
};
|