@neondatabase/auth 0.1.0-beta.8 → 0.2.0-beta.1
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 +108 -18
- package/dist/{adapter-core-Bw9mn_AS.d.mts → adapter-core-CnrOXh1T.d.mts} +246 -280
- package/dist/{adapter-core-C_NEMs0b.mjs → adapter-core-CtmnMMJ7.mjs} +392 -67
- package/dist/better-auth-react-adapter-DNi5PC5D.d.mts +2170 -0
- package/dist/{better-auth-react-adapter-BbM3jLLv.mjs → better-auth-react-adapter-Dv-o6A6O.mjs} +10 -8
- package/dist/{chunk-5DLVHPZS-Bxj7snpZ-DoVNlsyk.mjs → chunk-VCZJYX65-CLnrj1o7-D6ZQkcc_.mjs} +13 -3
- package/dist/constants-Cupc_bln.mjs +28 -0
- package/dist/index.d.mts +4 -98
- package/dist/index.mjs +2 -1
- package/dist/neon-auth-BEGCfAe6.d.mts +107 -0
- package/dist/{neon-auth-DdlToh7_.mjs → neon-auth-Cs2cWh1B.mjs} +7 -4
- package/dist/next/index.d.mts +61 -170
- package/dist/next/index.mjs +4 -311
- package/dist/next/server/index.d.mts +538 -0
- package/dist/next/server/index.mjs +1373 -0
- package/dist/react/adapters/index.d.mts +4 -4
- package/dist/react/adapters/index.mjs +2 -1
- package/dist/react/index.d.mts +5 -5
- package/dist/react/index.mjs +4 -3
- package/dist/react/ui/index.d.mts +1 -1
- package/dist/react/ui/index.mjs +2 -2
- package/dist/react/ui/server.mjs +1 -1
- package/dist/{supabase-adapter-CAqbpOC7.mjs → supabase-adapter-BlcGPyOf.mjs} +28 -45
- package/dist/supabase-adapter-DUqw2fw8.d.mts +2258 -0
- package/dist/types/index.d.mts +2 -7
- package/dist/ui/.safelist.html +3 -0
- package/dist/ui/css.css +2 -2
- package/dist/ui/tailwind.css +4 -3
- package/dist/ui/theme-inline.css +44 -0
- package/dist/ui/theme.css +221 -118
- package/dist/{ui-aMoA-9nq.mjs → ui-COLWzDsu.mjs} +6024 -3004
- package/dist/vanilla/adapters/index.d.mts +3 -3
- package/dist/vanilla/adapters/index.mjs +2 -1
- package/dist/vanilla/index.d.mts +3 -3
- package/dist/vanilla/index.mjs +2 -1
- package/llms.txt +330 -0
- package/package.json +17 -10
- package/dist/better-auth-react-adapter-JoscqoDc.d.mts +0 -722
- package/dist/better-auth-types-CE4hLv9E.d.mts +0 -9
- package/dist/supabase-adapter-Clxlqg1x.d.mts +0 -127
- /package/dist/{adapters-D0mxG3F-.mjs → adapters-B7YKkjaL.mjs} +0 -0
- /package/dist/{adapters-Df6Dd3KK.mjs → adapters-CivF9wql.mjs} +0 -0
- /package/dist/{index-ClXLQ1fw.d.mts → index-CPnFzULh.d.mts} +0 -0
- /package/dist/{index-BXlAjlSt.d.mts → index-CzsGMS7C.d.mts} +0 -0
- /package/dist/{index-DCQ5Y2ED.d.mts → index-OEBbnNdr.d.mts} +0 -0
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
import { o as NEON_AUTH_SESSION_VERIFIER_PARAM_NAME } from "../../constants-Cupc_bln.mjs";
|
|
2
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
3
|
+
import { parseCookies, parseSetCookieHeader } from "better-auth/cookies";
|
|
4
|
+
import { cookies, headers } from "next/headers";
|
|
5
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
6
|
+
|
|
7
|
+
//#region src/server/request-context.ts
|
|
8
|
+
/**
|
|
9
|
+
* Header name used to identify server-side proxy requests.
|
|
10
|
+
* The value will be the framework name (e.g., 'nextjs', 'remix').
|
|
11
|
+
*/
|
|
12
|
+
const NEON_AUTH_SERVER_PROXY_HEADER = "x-neon-auth-proxy";
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/server/endpoints.ts
|
|
16
|
+
const API_ENDPOINTS = {
|
|
17
|
+
getSession: {
|
|
18
|
+
path: "get-session",
|
|
19
|
+
method: "GET"
|
|
20
|
+
},
|
|
21
|
+
getAccessToken: {
|
|
22
|
+
path: "get-access-token",
|
|
23
|
+
method: "GET"
|
|
24
|
+
},
|
|
25
|
+
listSessions: {
|
|
26
|
+
path: "list-sessions",
|
|
27
|
+
method: "GET"
|
|
28
|
+
},
|
|
29
|
+
revokeSession: {
|
|
30
|
+
path: "revoke-session",
|
|
31
|
+
method: "POST"
|
|
32
|
+
},
|
|
33
|
+
revokeSessions: {
|
|
34
|
+
path: "revoke-sessions",
|
|
35
|
+
method: "POST"
|
|
36
|
+
},
|
|
37
|
+
revokeOtherSessions: {
|
|
38
|
+
path: "revoke-all-sessions",
|
|
39
|
+
method: "POST"
|
|
40
|
+
},
|
|
41
|
+
refreshToken: {
|
|
42
|
+
path: "refresh-token",
|
|
43
|
+
method: "POST"
|
|
44
|
+
},
|
|
45
|
+
signIn: {
|
|
46
|
+
email: {
|
|
47
|
+
path: "sign-in/email",
|
|
48
|
+
method: "POST"
|
|
49
|
+
},
|
|
50
|
+
social: {
|
|
51
|
+
path: "sign-in/social",
|
|
52
|
+
method: "POST"
|
|
53
|
+
},
|
|
54
|
+
emailOtp: {
|
|
55
|
+
path: "sign-in/email-otp",
|
|
56
|
+
method: "POST"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
signUp: { email: {
|
|
60
|
+
path: "sign-up/email",
|
|
61
|
+
method: "POST"
|
|
62
|
+
} },
|
|
63
|
+
signOut: {
|
|
64
|
+
path: "sign-out",
|
|
65
|
+
method: "POST"
|
|
66
|
+
},
|
|
67
|
+
listAccounts: {
|
|
68
|
+
path: "list-accounts",
|
|
69
|
+
method: "GET"
|
|
70
|
+
},
|
|
71
|
+
accountInfo: {
|
|
72
|
+
path: "account-info",
|
|
73
|
+
method: "GET"
|
|
74
|
+
},
|
|
75
|
+
updateUser: {
|
|
76
|
+
path: "update-user",
|
|
77
|
+
method: "POST"
|
|
78
|
+
},
|
|
79
|
+
deleteUser: {
|
|
80
|
+
path: "delete-user",
|
|
81
|
+
method: "POST"
|
|
82
|
+
},
|
|
83
|
+
changePassword: {
|
|
84
|
+
path: "change-password",
|
|
85
|
+
method: "POST"
|
|
86
|
+
},
|
|
87
|
+
sendVerificationEmail: {
|
|
88
|
+
path: "send-verification-email",
|
|
89
|
+
method: "POST"
|
|
90
|
+
},
|
|
91
|
+
verifyEmail: {
|
|
92
|
+
path: "verify-email",
|
|
93
|
+
method: "POST"
|
|
94
|
+
},
|
|
95
|
+
resetPassword: {
|
|
96
|
+
path: "reset-password",
|
|
97
|
+
method: "POST"
|
|
98
|
+
},
|
|
99
|
+
requestPasswordReset: {
|
|
100
|
+
path: "request-password-reset",
|
|
101
|
+
method: "POST"
|
|
102
|
+
},
|
|
103
|
+
token: {
|
|
104
|
+
path: "token",
|
|
105
|
+
method: "GET"
|
|
106
|
+
},
|
|
107
|
+
jwks: {
|
|
108
|
+
path: "jwt",
|
|
109
|
+
method: "GET"
|
|
110
|
+
},
|
|
111
|
+
getAnonymousToken: {
|
|
112
|
+
path: "token/anonymous",
|
|
113
|
+
method: "GET"
|
|
114
|
+
},
|
|
115
|
+
admin: {
|
|
116
|
+
createUser: {
|
|
117
|
+
path: "admin/create-user",
|
|
118
|
+
method: "POST"
|
|
119
|
+
},
|
|
120
|
+
listUsers: {
|
|
121
|
+
path: "admin/list-users",
|
|
122
|
+
method: "GET"
|
|
123
|
+
},
|
|
124
|
+
setRole: {
|
|
125
|
+
path: "admin/set-role",
|
|
126
|
+
method: "POST"
|
|
127
|
+
},
|
|
128
|
+
setUserPassword: {
|
|
129
|
+
path: "admin/set-user-password",
|
|
130
|
+
method: "POST"
|
|
131
|
+
},
|
|
132
|
+
updateUser: {
|
|
133
|
+
path: "admin/update-user",
|
|
134
|
+
method: "POST"
|
|
135
|
+
},
|
|
136
|
+
banUser: {
|
|
137
|
+
path: "admin/ban-user",
|
|
138
|
+
method: "POST"
|
|
139
|
+
},
|
|
140
|
+
unbanUser: {
|
|
141
|
+
path: "admin/unban-user",
|
|
142
|
+
method: "POST"
|
|
143
|
+
},
|
|
144
|
+
listUserSessions: {
|
|
145
|
+
path: "admin/list-user-sessions",
|
|
146
|
+
method: "GET"
|
|
147
|
+
},
|
|
148
|
+
revokeUserSession: {
|
|
149
|
+
path: "admin/revoke-user-session",
|
|
150
|
+
method: "POST"
|
|
151
|
+
},
|
|
152
|
+
revokeUserSessions: {
|
|
153
|
+
path: "admin/revoke-user-sessions",
|
|
154
|
+
method: "POST"
|
|
155
|
+
},
|
|
156
|
+
impersonateUser: {
|
|
157
|
+
path: "admin/impersonate-user",
|
|
158
|
+
method: "POST"
|
|
159
|
+
},
|
|
160
|
+
stopImpersonating: {
|
|
161
|
+
path: "admin/stop-impersonating",
|
|
162
|
+
method: "POST"
|
|
163
|
+
},
|
|
164
|
+
removeUser: {
|
|
165
|
+
path: "admin/remove-user",
|
|
166
|
+
method: "POST"
|
|
167
|
+
},
|
|
168
|
+
hasPermission: {
|
|
169
|
+
path: "admin/has-permission",
|
|
170
|
+
method: "POST"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
organization: {
|
|
174
|
+
create: {
|
|
175
|
+
path: "organization/create",
|
|
176
|
+
method: "POST"
|
|
177
|
+
},
|
|
178
|
+
update: {
|
|
179
|
+
path: "organization/update",
|
|
180
|
+
method: "POST"
|
|
181
|
+
},
|
|
182
|
+
delete: {
|
|
183
|
+
path: "organization/delete",
|
|
184
|
+
method: "POST"
|
|
185
|
+
},
|
|
186
|
+
list: {
|
|
187
|
+
path: "organization/list",
|
|
188
|
+
method: "GET"
|
|
189
|
+
},
|
|
190
|
+
getFullOrganization: {
|
|
191
|
+
path: "organization/get-full-organization",
|
|
192
|
+
method: "GET"
|
|
193
|
+
},
|
|
194
|
+
setActive: {
|
|
195
|
+
path: "organization/set-active",
|
|
196
|
+
method: "POST"
|
|
197
|
+
},
|
|
198
|
+
checkSlug: {
|
|
199
|
+
path: "organization/check-slug",
|
|
200
|
+
method: "GET"
|
|
201
|
+
},
|
|
202
|
+
listMembers: {
|
|
203
|
+
path: "organization/list-members",
|
|
204
|
+
method: "GET"
|
|
205
|
+
},
|
|
206
|
+
removeMember: {
|
|
207
|
+
path: "organization/remove-member",
|
|
208
|
+
method: "POST"
|
|
209
|
+
},
|
|
210
|
+
updateMemberRole: {
|
|
211
|
+
path: "organization/update-member-role",
|
|
212
|
+
method: "POST"
|
|
213
|
+
},
|
|
214
|
+
leave: {
|
|
215
|
+
path: "organization/leave",
|
|
216
|
+
method: "POST"
|
|
217
|
+
},
|
|
218
|
+
getActiveMember: {
|
|
219
|
+
path: "organization/get-active-member",
|
|
220
|
+
method: "GET"
|
|
221
|
+
},
|
|
222
|
+
getActiveMemberRole: {
|
|
223
|
+
path: "organization/get-active-member-role",
|
|
224
|
+
method: "GET"
|
|
225
|
+
},
|
|
226
|
+
inviteMember: {
|
|
227
|
+
path: "organization/invite-member",
|
|
228
|
+
method: "POST"
|
|
229
|
+
},
|
|
230
|
+
acceptInvitation: {
|
|
231
|
+
path: "organization/accept-invitation",
|
|
232
|
+
method: "POST"
|
|
233
|
+
},
|
|
234
|
+
rejectInvitation: {
|
|
235
|
+
path: "organization/reject-invitation",
|
|
236
|
+
method: "POST"
|
|
237
|
+
},
|
|
238
|
+
cancelInvitation: {
|
|
239
|
+
path: "organization/cancel-invitation",
|
|
240
|
+
method: "POST"
|
|
241
|
+
},
|
|
242
|
+
getInvitation: {
|
|
243
|
+
path: "organization/get-invitation",
|
|
244
|
+
method: "GET"
|
|
245
|
+
},
|
|
246
|
+
listInvitations: {
|
|
247
|
+
path: "organization/list-invitations",
|
|
248
|
+
method: "GET"
|
|
249
|
+
},
|
|
250
|
+
listUserInvitations: {
|
|
251
|
+
path: "organization/list-user-invitations",
|
|
252
|
+
method: "GET"
|
|
253
|
+
},
|
|
254
|
+
hasPermission: {
|
|
255
|
+
path: "organization/has-permission",
|
|
256
|
+
method: "POST"
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
emailOtp: {
|
|
260
|
+
sendVerificationOtp: {
|
|
261
|
+
path: "email-otp/send-verification-otp",
|
|
262
|
+
method: "POST"
|
|
263
|
+
},
|
|
264
|
+
verifyEmail: {
|
|
265
|
+
path: "email-otp/verify-email",
|
|
266
|
+
method: "POST"
|
|
267
|
+
},
|
|
268
|
+
checkVerificationOtp: {
|
|
269
|
+
path: "email-otp/check-verification-otp",
|
|
270
|
+
method: "POST"
|
|
271
|
+
},
|
|
272
|
+
resetPassword: {
|
|
273
|
+
path: "email-otp/passcode",
|
|
274
|
+
method: "POST"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/server/constants.ts
|
|
281
|
+
/** Prefix for all Neon Auth cookies */
|
|
282
|
+
const NEON_AUTH_COOKIE_PREFIX = "__Secure-neon-auth";
|
|
283
|
+
/** Cookie name for cached session data (signed JWT) - used for server-side session caching */
|
|
284
|
+
const NEON_AUTH_SESSION_DATA_COOKIE_NAME = `${NEON_AUTH_COOKIE_PREFIX}.local.session_data`;
|
|
285
|
+
/** Cookie name for OAuth session challenge - used for OAuth flow security */
|
|
286
|
+
const NEON_AUTH_SESSION_CHALLENGE_COOKIE_NAME = `${NEON_AUTH_COOKIE_PREFIX}.session_challange`;
|
|
287
|
+
/** Cookie name for session token - the primary authentication cookie */
|
|
288
|
+
const NEON_AUTH_SESSION_COOKIE_NAME = `${NEON_AUTH_COOKIE_PREFIX}.session_token`;
|
|
289
|
+
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/server/utils/cookies.ts
|
|
292
|
+
/**
|
|
293
|
+
* Extract the Neon Auth cookies from the request headers.
|
|
294
|
+
* Only returns cookies that start with the NEON_AUTH_COOKIE_PREFIX.
|
|
295
|
+
*
|
|
296
|
+
* @param headers - The request headers or cookie header string.
|
|
297
|
+
* @returns The cookie string with all Neon Auth cookies (e.g., "name=value; name2=value2").
|
|
298
|
+
*/
|
|
299
|
+
const extractNeonAuthCookies = (headers$1) => {
|
|
300
|
+
const cookieHeader = typeof headers$1 === "string" ? headers$1 : headers$1.get("cookie");
|
|
301
|
+
if (!cookieHeader) return "";
|
|
302
|
+
const parsedCookies = parseCookies(cookieHeader);
|
|
303
|
+
const result = [];
|
|
304
|
+
for (const [name, value] of parsedCookies.entries()) if (name.startsWith(NEON_AUTH_COOKIE_PREFIX)) result.push(`${name}=${value}`);
|
|
305
|
+
return result.join("; ");
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Parses the `set-cookie` header from Neon Auth response into a list of cookies.
|
|
309
|
+
*
|
|
310
|
+
* @param setCookieHeader - The `set-cookie` header from Neon Auth response.
|
|
311
|
+
* @returns The list of parsed cookies with their options.
|
|
312
|
+
*/
|
|
313
|
+
const parseSetCookies = (setCookieHeader) => {
|
|
314
|
+
const parsedCookies = parseSetCookieHeader(setCookieHeader);
|
|
315
|
+
const cookies$1 = [];
|
|
316
|
+
for (const entry of parsedCookies.entries()) {
|
|
317
|
+
const [name, parsedCookie] = entry;
|
|
318
|
+
cookies$1.push({
|
|
319
|
+
name,
|
|
320
|
+
value: decodeURIComponent(parsedCookie.value),
|
|
321
|
+
path: parsedCookie.path,
|
|
322
|
+
domain: parsedCookie.domain,
|
|
323
|
+
maxAge: parsedCookie["max-age"] ?? parsedCookie.maxAge,
|
|
324
|
+
httpOnly: parsedCookie.httponly ?? true,
|
|
325
|
+
secure: parsedCookie.secure ?? true,
|
|
326
|
+
sameSite: parsedCookie.samesite ?? "lax",
|
|
327
|
+
partitioned: parsedCookie.partitioned
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return cookies$1;
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Serializes a parsed cookie object back into a Set-Cookie header string
|
|
334
|
+
*
|
|
335
|
+
* @param cookie - The parsed cookie object
|
|
336
|
+
* @returns The Set-Cookie header string
|
|
337
|
+
*/
|
|
338
|
+
const serializeSetCookie = (cookie) => {
|
|
339
|
+
let result = `${cookie.name}=${encodeURIComponent(cookie.value)}`;
|
|
340
|
+
if (cookie.path) result += `; Path=${cookie.path}`;
|
|
341
|
+
if (cookie.domain) result += `; Domain=${cookie.domain}`;
|
|
342
|
+
if (cookie.maxAge !== void 0) result += `; Max-Age=${cookie.maxAge}`;
|
|
343
|
+
if (cookie.expires) result += `; Expires=${cookie.expires.toUTCString()}`;
|
|
344
|
+
if (cookie.httpOnly) result += "; HttpOnly";
|
|
345
|
+
if (cookie.secure) result += "; Secure";
|
|
346
|
+
if (cookie.sameSite) {
|
|
347
|
+
const sameSite = cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1);
|
|
348
|
+
result += `; SameSite=${sameSite}`;
|
|
349
|
+
}
|
|
350
|
+
if (cookie.partitioned) result += "; Partitioned";
|
|
351
|
+
return result;
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Extract a single cookie value by name from a cookie header string
|
|
355
|
+
*
|
|
356
|
+
* @param cookieString - The cookie header string (e.g., "name=value; name2=value2")
|
|
357
|
+
* @param name - The cookie name to extract
|
|
358
|
+
* @returns The cookie value or null if not found
|
|
359
|
+
*/
|
|
360
|
+
const parseCookieValue = (cookieString, name) => {
|
|
361
|
+
if (!cookieString) return null;
|
|
362
|
+
return parseCookies(cookieString).get(name) ?? null;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/server/session/operations.ts
|
|
367
|
+
const DEFAULT_SESSION_CACHE_TTL_SECONDS = 300;
|
|
368
|
+
const JWS_ALGO = "HS256";
|
|
369
|
+
/**
|
|
370
|
+
* Parse and validate date value, throwing descriptive error on failure
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
373
|
+
function parseDate(dateValue, fieldName) {
|
|
374
|
+
const date = new Date(dateValue);
|
|
375
|
+
if (Number.isNaN(date.getTime())) throw new TypeError(`Invalid date value for ${fieldName}: ${JSON.stringify(dateValue)}`);
|
|
376
|
+
return date;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Convert session data from /get-session into a signed cookie
|
|
380
|
+
* @param sessionData - Session and user data from Auth server
|
|
381
|
+
* @param secret - Secret for signing the cookie
|
|
382
|
+
* @param ttlSeconds - Time-to-live in seconds (default: 300 = 5 minutes)
|
|
383
|
+
* @returns Signed session data cookie
|
|
384
|
+
*/
|
|
385
|
+
async function signSessionDataCookie(sessionData, secret, ttlSeconds = DEFAULT_SESSION_CACHE_TTL_SECONDS) {
|
|
386
|
+
const ttlMs = ttlSeconds * 1e3;
|
|
387
|
+
const expiresAt = Math.min(sessionData.session.expiresAt.getTime(), Date.now() + ttlMs);
|
|
388
|
+
return {
|
|
389
|
+
value: await signPayload(sessionData, expiresAt, secret),
|
|
390
|
+
expiresAt: new Date(expiresAt)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function signPayload(sessionData, expiresAt, secret) {
|
|
394
|
+
const encodedSecret = new TextEncoder().encode(secret);
|
|
395
|
+
const expSeconds = Math.floor(expiresAt / 1e3);
|
|
396
|
+
return new SignJWT(sessionData).setProtectedHeader({
|
|
397
|
+
alg: JWS_ALGO,
|
|
398
|
+
typ: "JWT"
|
|
399
|
+
}).setIssuedAt().setExpirationTime(expSeconds).setSubject(sessionData.user?.id ?? "anonymous").sign(encodedSecret);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Parse session data from JSON, converting date strings to Date objects
|
|
403
|
+
*
|
|
404
|
+
* Note: Better Auth API returns ISO 8601 date strings. JSON.parse() does not
|
|
405
|
+
* automatically convert these to Date objects, so manual conversion is required.
|
|
406
|
+
*
|
|
407
|
+
* @internal Exported for internal use by auth handler
|
|
408
|
+
*/
|
|
409
|
+
function parseSessionData(json) {
|
|
410
|
+
if (!json || typeof json !== "object") return {
|
|
411
|
+
session: null,
|
|
412
|
+
user: null
|
|
413
|
+
};
|
|
414
|
+
const data = json;
|
|
415
|
+
if (!data.session || !data.user) return {
|
|
416
|
+
session: null,
|
|
417
|
+
user: null
|
|
418
|
+
};
|
|
419
|
+
try {
|
|
420
|
+
return {
|
|
421
|
+
session: {
|
|
422
|
+
...data.session,
|
|
423
|
+
expiresAt: parseDate(data.session.expiresAt, "session.expiresAt"),
|
|
424
|
+
createdAt: parseDate(data.session.createdAt, "session.createdAt"),
|
|
425
|
+
updatedAt: parseDate(data.session.updatedAt, "session.updatedAt")
|
|
426
|
+
},
|
|
427
|
+
user: {
|
|
428
|
+
...data.user,
|
|
429
|
+
createdAt: parseDate(data.user.createdAt, "user.createdAt"),
|
|
430
|
+
updatedAt: parseDate(data.user.updatedAt, "user.updatedAt")
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error("[parseSessionData] Failed to parse session dates:", {
|
|
435
|
+
error: error instanceof Error ? error.message : String(error),
|
|
436
|
+
hasSession: !!data.session,
|
|
437
|
+
hasUser: !!data.user
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
session: null,
|
|
441
|
+
user: null
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Extract and validate session data from cookie header
|
|
447
|
+
* Falls back to null on any error (caller should fetch from API)
|
|
448
|
+
*
|
|
449
|
+
* @param request - Request object with cookie header
|
|
450
|
+
* @param cookieName - Name of session data cookie
|
|
451
|
+
* @param cookieSecret - cookie secret for validation
|
|
452
|
+
* @returns SessionData or null on validation failure
|
|
453
|
+
*/
|
|
454
|
+
async function getSessionDataFromCookie(request, cookieName, cookieSecret) {
|
|
455
|
+
try {
|
|
456
|
+
const cookieHeader = request.headers.get("cookie");
|
|
457
|
+
if (!cookieHeader) return null;
|
|
458
|
+
const sessionDataCookie = parseCookies(cookieHeader).get(cookieName);
|
|
459
|
+
if (!sessionDataCookie) return null;
|
|
460
|
+
const result = await validateSessionData(sessionDataCookie, cookieSecret);
|
|
461
|
+
if (result.valid && result.payload) return result.payload;
|
|
462
|
+
console.warn("[getSessionDataFromCookie] Invalid session cookie:", {
|
|
463
|
+
error: result.error,
|
|
464
|
+
cookieName
|
|
465
|
+
});
|
|
466
|
+
return null;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error("[getSessionDataFromCookie] Unexpected validation error:", {
|
|
469
|
+
error: error instanceof Error ? error.message : String(error),
|
|
470
|
+
cookieName,
|
|
471
|
+
...process.env.NODE_ENV !== "production" && { stack: error instanceof Error ? error.stack : void 0 }
|
|
472
|
+
});
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/server/errors.ts
|
|
479
|
+
const ERRORS = {
|
|
480
|
+
MISSING_AUTH_BASE_URL: "Missing required config: baseUrl. You must provide the auth URL of your Neon Auth instance in the config object.",
|
|
481
|
+
MISSING_COOKIE_SECRET: "Missing required config: cookies.secret. You must provide the cookie secret in the config object.",
|
|
482
|
+
COOKIE_SECRET_TOO_SHORT: "cookies.secret must be at least 32 characters long for security. Generate a secure secret with: openssl rand -base64 32",
|
|
483
|
+
INVALID_SESSION_DATA_TTL: "cookies.sessionDataTtl must be a positive number (in seconds)"
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/server/session/validator.ts
|
|
488
|
+
/**
|
|
489
|
+
* Validate session data signature and expiry using jose
|
|
490
|
+
* @param sessionDataString - Session data string to validate
|
|
491
|
+
* @param cookieSecret - cookie secret for validation
|
|
492
|
+
* @returns Validation result with payload if valid
|
|
493
|
+
*/
|
|
494
|
+
async function validateSessionData(sessionDataString, cookieSecret) {
|
|
495
|
+
try {
|
|
496
|
+
const { payload } = await jwtVerify(sessionDataString, new TextEncoder().encode(cookieSecret), { algorithms: ["HS256"] });
|
|
497
|
+
return {
|
|
498
|
+
valid: true,
|
|
499
|
+
payload: parseSessionData(payload)
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return {
|
|
503
|
+
valid: false,
|
|
504
|
+
error: error instanceof Error ? error.message : "Invalid session data"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/server/proxy/request.ts
|
|
511
|
+
const PROXY_HEADERS = [
|
|
512
|
+
"user-agent",
|
|
513
|
+
"authorization",
|
|
514
|
+
"referer",
|
|
515
|
+
"content-type"
|
|
516
|
+
];
|
|
517
|
+
/**
|
|
518
|
+
* Proxy header constant - indicates request went through Neon Auth middleware/handler
|
|
519
|
+
* This is framework-agnostic and can be used by any server framework
|
|
520
|
+
*/
|
|
521
|
+
const NEON_AUTH_HEADER_MIDDLEWARE_NAME = "x-neon-auth-middleware";
|
|
522
|
+
/**
|
|
523
|
+
* Handles proxying authentication requests to the upstream Neon Auth server
|
|
524
|
+
*
|
|
525
|
+
* @param baseUrl - Base URL of the Neon Auth server
|
|
526
|
+
* @param request - Standard Web API Request object
|
|
527
|
+
* @param path - API path to proxy to (e.g., 'get-session', 'sign-in')
|
|
528
|
+
* @returns Response from upstream server or error response
|
|
529
|
+
*/
|
|
530
|
+
const handleAuthRequest = async (baseUrl, request, path) => {
|
|
531
|
+
const headers$1 = prepareRequestHeaders(request);
|
|
532
|
+
const body = await parseRequestBody(request);
|
|
533
|
+
try {
|
|
534
|
+
const upstreamURL = getUpstreamURL(baseUrl, path, { originalUrl: new URL(request.url) });
|
|
535
|
+
return await fetch(upstreamURL.toString(), {
|
|
536
|
+
method: request.method,
|
|
537
|
+
headers: headers$1,
|
|
538
|
+
body
|
|
539
|
+
});
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (error instanceof Error && error.name === "TypeError" && error.message.includes("fetch")) return Response.json({
|
|
542
|
+
error: "Unable to connect to authentication server",
|
|
543
|
+
code: "NETWORK_ERROR"
|
|
544
|
+
}, {
|
|
545
|
+
status: 502,
|
|
546
|
+
headers: { "Content-Type": "application/json" }
|
|
547
|
+
});
|
|
548
|
+
const message = error instanceof Error ? error.message : "Internal Server Error";
|
|
549
|
+
console.error(`[AuthError] ${message}`, error);
|
|
550
|
+
return Response.json({
|
|
551
|
+
error: message,
|
|
552
|
+
code: "INTERNAL_ERROR"
|
|
553
|
+
}, {
|
|
554
|
+
status: 500,
|
|
555
|
+
headers: { "Content-Type": "application/json" }
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
/**
|
|
560
|
+
* Constructs the upstream URL for proxying to Neon Auth server
|
|
561
|
+
*
|
|
562
|
+
* @param baseUrl - Base URL of the Neon Auth server
|
|
563
|
+
* @param path - API path (e.g., 'get-session')
|
|
564
|
+
* @param options - Options including original URL for preserving query params
|
|
565
|
+
* @returns Constructed upstream URL
|
|
566
|
+
*/
|
|
567
|
+
const getUpstreamURL = (baseUrl, path, { originalUrl }) => {
|
|
568
|
+
const url = new URL(`${baseUrl}/${path}`);
|
|
569
|
+
if (originalUrl) {
|
|
570
|
+
url.search = originalUrl.search;
|
|
571
|
+
return url;
|
|
572
|
+
}
|
|
573
|
+
return url;
|
|
574
|
+
};
|
|
575
|
+
const prepareRequestHeaders = (request) => {
|
|
576
|
+
const headers$1 = new Headers();
|
|
577
|
+
for (const header of PROXY_HEADERS) if (request.headers.get(header)) headers$1.set(header, request.headers.get(header));
|
|
578
|
+
headers$1.set("Origin", getOrigin(request));
|
|
579
|
+
headers$1.set("Cookie", extractNeonAuthCookies(request.headers));
|
|
580
|
+
headers$1.set(NEON_AUTH_HEADER_MIDDLEWARE_NAME, "true");
|
|
581
|
+
return headers$1;
|
|
582
|
+
};
|
|
583
|
+
const getOrigin = (request) => {
|
|
584
|
+
return request.headers.get("origin") || request.headers.get("referer")?.split("/").slice(0, 3).join("/") || new URL(request.url).origin;
|
|
585
|
+
};
|
|
586
|
+
const parseRequestBody = async (request) => {
|
|
587
|
+
if (request.body) return request.text();
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/server/proxy/response.ts
|
|
592
|
+
const RESPONSE_HEADERS_ALLOWLIST = [
|
|
593
|
+
"content-type",
|
|
594
|
+
"content-length",
|
|
595
|
+
"content-encoding",
|
|
596
|
+
"transfer-encoding",
|
|
597
|
+
"connection",
|
|
598
|
+
"date",
|
|
599
|
+
"set-cookie",
|
|
600
|
+
"set-auth-jwt",
|
|
601
|
+
"set-auth-token",
|
|
602
|
+
"x-neon-ret-request-id"
|
|
603
|
+
];
|
|
604
|
+
/**
|
|
605
|
+
* Handles responses from upstream Neon Auth server
|
|
606
|
+
* - Proxies allowed headers to client
|
|
607
|
+
* - Mints session data cookie if session token is present
|
|
608
|
+
*
|
|
609
|
+
* @param response - Response from upstream Neon Auth server
|
|
610
|
+
* @param baseUrl - Base URL of Neon Auth server
|
|
611
|
+
* @param cookieConfig - Session cookie configuration
|
|
612
|
+
* @returns New Response with proxied headers and session data cookie
|
|
613
|
+
*/
|
|
614
|
+
const handleAuthResponse = async (response, baseUrl, cookieConfig) => {
|
|
615
|
+
const responseHeaders = prepareResponseHeaders(response, cookieConfig.domain);
|
|
616
|
+
const sessionDataCookie = await mintSessionData(response.headers, baseUrl, cookieConfig);
|
|
617
|
+
if (sessionDataCookie) responseHeaders.append("Set-Cookie", sessionDataCookie);
|
|
618
|
+
return new Response(response.body, {
|
|
619
|
+
status: response.status,
|
|
620
|
+
statusText: response.statusText,
|
|
621
|
+
headers: responseHeaders
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
const prepareResponseHeaders = (response, domain) => {
|
|
625
|
+
const headers$1 = new Headers();
|
|
626
|
+
for (const header of RESPONSE_HEADERS_ALLOWLIST) if (header === "set-cookie") {
|
|
627
|
+
const cookies$1 = response.headers.getSetCookie();
|
|
628
|
+
for (const cookieHeader of cookies$1) if (domain) {
|
|
629
|
+
const parsedCookies = parseSetCookies(cookieHeader);
|
|
630
|
+
for (const parsedCookie of parsedCookies) {
|
|
631
|
+
parsedCookie.domain = domain;
|
|
632
|
+
headers$1.append("Set-Cookie", serializeSetCookie(parsedCookie));
|
|
633
|
+
}
|
|
634
|
+
} else headers$1.append("Set-Cookie", cookieHeader);
|
|
635
|
+
} else {
|
|
636
|
+
const value = response.headers.get(header);
|
|
637
|
+
if (value) headers$1.set(header, value);
|
|
638
|
+
}
|
|
639
|
+
return headers$1;
|
|
640
|
+
};
|
|
641
|
+
async function mintSessionData(headers$1, baseUrl, cookieConfig) {
|
|
642
|
+
const { secret, sessionDataTtl, domain } = cookieConfig;
|
|
643
|
+
const sessionToken = headers$1.getSetCookie().find((cookie) => cookie.includes("session_token"));
|
|
644
|
+
if (!sessionToken) return null;
|
|
645
|
+
if (sessionToken.toLowerCase().includes("max-age=0")) return serializeSetCookie({
|
|
646
|
+
name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
|
|
647
|
+
value: "",
|
|
648
|
+
path: "/",
|
|
649
|
+
domain,
|
|
650
|
+
httpOnly: true,
|
|
651
|
+
secure: true,
|
|
652
|
+
sameSite: "lax",
|
|
653
|
+
maxAge: 0
|
|
654
|
+
});
|
|
655
|
+
try {
|
|
656
|
+
const sessionData = await fetchSessionWithCookie(sessionToken, baseUrl);
|
|
657
|
+
if (sessionData.session) {
|
|
658
|
+
const { value: signedData, expiresAt } = await signSessionDataCookie(sessionData, secret, sessionDataTtl);
|
|
659
|
+
return serializeSetCookie({
|
|
660
|
+
name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
|
|
661
|
+
value: signedData,
|
|
662
|
+
path: "/",
|
|
663
|
+
domain,
|
|
664
|
+
httpOnly: true,
|
|
665
|
+
secure: true,
|
|
666
|
+
sameSite: "lax",
|
|
667
|
+
maxAge: Math.floor((expiresAt.getTime() - Date.now()) / 1e3)
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
} catch (error) {
|
|
671
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
672
|
+
const errorContext = {
|
|
673
|
+
error: errorMessage,
|
|
674
|
+
setCookieHeaderLength: sessionToken?.length || 0
|
|
675
|
+
};
|
|
676
|
+
if (errorMessage.includes("session_token not found")) console.warn("[mintSessionData] Session token missing in set-cookie:", errorContext);
|
|
677
|
+
else if (errorMessage.includes("Failed to fetch session data")) console.error("[mintSessionData] Upstream /get-session request failed:", errorContext);
|
|
678
|
+
else if (errorMessage.includes("NEON_AUTH_COOKIE_SECRET")) console.error("[mintSessionData] Cookie secret configuration error:", errorContext);
|
|
679
|
+
else if (errorMessage.includes("Invalid date")) console.error("[mintSessionData] Date parsing error:", errorContext);
|
|
680
|
+
else console.error("[mintSessionData] Unexpected error:", {
|
|
681
|
+
...errorContext,
|
|
682
|
+
...process.env.NODE_ENV !== "production" && { stack: error instanceof Error ? error.stack : void 0 }
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
async function fetchSessionWithCookie(setCookieHeader, baseUrl) {
|
|
688
|
+
const sessionToken = parseSetCookies(setCookieHeader).find((c) => c.name.includes("session_token"));
|
|
689
|
+
if (!sessionToken) throw new Error("session_token not found in set-cookie header");
|
|
690
|
+
const response = await fetch(`${baseUrl}/get-session`, {
|
|
691
|
+
headers: { Cookie: `${sessionToken.name}=${sessionToken.value}` },
|
|
692
|
+
signal: AbortSignal.timeout(3e3)
|
|
693
|
+
});
|
|
694
|
+
if (!response.ok) throw new Error(`Failed to fetch session data: ${response.status} ${response.statusText}`);
|
|
695
|
+
let body;
|
|
696
|
+
try {
|
|
697
|
+
body = await response.json();
|
|
698
|
+
} catch (error) {
|
|
699
|
+
throw new Error(`Failed to parse /get-session response as JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
700
|
+
}
|
|
701
|
+
return parseSessionData(body);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region src/server/session/cache-handler.ts
|
|
706
|
+
/**
|
|
707
|
+
* Attempts to retrieve session data from cookie cache
|
|
708
|
+
* Returns Response with session data if cache hit, null otherwise
|
|
709
|
+
*
|
|
710
|
+
* This is the framework-agnostic session cache optimization used by API handlers.
|
|
711
|
+
*
|
|
712
|
+
* @param request - Standard Web API Request object
|
|
713
|
+
* @param cookieSecret - Secret for validating signed session cookies
|
|
714
|
+
* @returns Response with session data JSON if cache hit, null if miss/disabled
|
|
715
|
+
*/
|
|
716
|
+
async function trySessionCache(request, cookieSecret) {
|
|
717
|
+
if (new URL(request.url).searchParams.get("disableCookieCache") === "true") return null;
|
|
718
|
+
if (!(request.headers.get("cookie") || "").includes(NEON_AUTH_SESSION_COOKIE_NAME)) return null;
|
|
719
|
+
try {
|
|
720
|
+
const sessionData = await getSessionDataFromCookie(request, NEON_AUTH_SESSION_DATA_COOKIE_NAME, cookieSecret);
|
|
721
|
+
if (sessionData && sessionData.session) return Response.json(sessionData);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
724
|
+
const errorName = error instanceof Error ? error.name : "Unknown";
|
|
725
|
+
if (errorName === "JWTExpired") console.debug("[trySessionCache] Session cookie expired (expected):", {
|
|
726
|
+
error: errorMessage,
|
|
727
|
+
errorType: errorName,
|
|
728
|
+
url: request.url
|
|
729
|
+
});
|
|
730
|
+
else if (errorName === "JWTInvalid" || errorName === "JWTClaimValidationFailed") console.warn("[trySessionCache] Invalid session cookie (possible tampering):", {
|
|
731
|
+
error: errorMessage,
|
|
732
|
+
errorType: errorName,
|
|
733
|
+
url: request.url
|
|
734
|
+
});
|
|
735
|
+
else console.error("[trySessionCache] Unexpected cookie validation error:", {
|
|
736
|
+
error: errorMessage,
|
|
737
|
+
errorType: errorName,
|
|
738
|
+
url: request.url
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/server/proxy/handler.ts
|
|
746
|
+
/**
|
|
747
|
+
* Generic authentication proxy handler (framework-agnostic)
|
|
748
|
+
*
|
|
749
|
+
* Handles the complete flow:
|
|
750
|
+
* 1. Check if request is for getSession endpoint
|
|
751
|
+
* 2. Try session cache if applicable (< 1ms fast path)
|
|
752
|
+
* 3. Call upstream Neon Auth API
|
|
753
|
+
* 4. Handle response with cookie minting
|
|
754
|
+
*
|
|
755
|
+
* This is framework-agnostic and can be used by any server framework.
|
|
756
|
+
*
|
|
757
|
+
* @param config - Proxy configuration
|
|
758
|
+
* @returns Standard Web API Response
|
|
759
|
+
*/
|
|
760
|
+
async function handleAuthProxyRequest(config) {
|
|
761
|
+
const { request, path, baseUrl, cookieSecret, sessionDataTtl, domain } = config;
|
|
762
|
+
if (path === API_ENDPOINTS.getSession.path && request.method === API_ENDPOINTS.getSession.method) {
|
|
763
|
+
const cachedResponse = await trySessionCache(request, cookieSecret);
|
|
764
|
+
if (cachedResponse) return cachedResponse;
|
|
765
|
+
}
|
|
766
|
+
return await handleAuthResponse(await handleAuthRequest(baseUrl, request, path), baseUrl, {
|
|
767
|
+
secret: cookieSecret,
|
|
768
|
+
sessionDataTtl,
|
|
769
|
+
domain
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/server/client-factory.ts
|
|
775
|
+
function createAuthServerInternal(config) {
|
|
776
|
+
const { baseUrl, context: getContext, cookieSecret, sessionDataTtl, domain } = config;
|
|
777
|
+
const fetchWithAuth = async (path, method, args) => {
|
|
778
|
+
const ctx = await getContext();
|
|
779
|
+
const cookies$1 = await ctx.getCookies();
|
|
780
|
+
const origin = await ctx.getOrigin();
|
|
781
|
+
const framework = ctx.getFramework();
|
|
782
|
+
const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
783
|
+
const { query, fetchOptions: _fetchOptions, ...body } = args || {};
|
|
784
|
+
if (query && typeof query === "object") {
|
|
785
|
+
const queryParams = query;
|
|
786
|
+
for (const [key, value] of Object.entries(queryParams)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
787
|
+
}
|
|
788
|
+
const headers$1 = {
|
|
789
|
+
Cookie: cookies$1,
|
|
790
|
+
Origin: origin,
|
|
791
|
+
[NEON_AUTH_SERVER_PROXY_HEADER]: framework
|
|
792
|
+
};
|
|
793
|
+
let requestBody;
|
|
794
|
+
if (method === "POST") {
|
|
795
|
+
headers$1["Content-Type"] = "application/json";
|
|
796
|
+
requestBody = JSON.stringify(Object.keys(body).length > 0 ? body : {});
|
|
797
|
+
}
|
|
798
|
+
const response = await fetch(url.toString(), {
|
|
799
|
+
method,
|
|
800
|
+
headers: headers$1,
|
|
801
|
+
body: requestBody
|
|
802
|
+
});
|
|
803
|
+
const setCookieHeaders = response.headers.getSetCookie();
|
|
804
|
+
if (setCookieHeaders.length > 0) {
|
|
805
|
+
for (const setCookieHeader of setCookieHeaders) {
|
|
806
|
+
const parsedCookies = parseSetCookies(setCookieHeader);
|
|
807
|
+
for (const cookie of parsedCookies) {
|
|
808
|
+
const cookieOptions = domain ? {
|
|
809
|
+
...cookie,
|
|
810
|
+
domain
|
|
811
|
+
} : cookie;
|
|
812
|
+
await ctx.setCookie(cookie.name, cookie.value, cookieOptions);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const sessionDataCookie = await mintSessionData(response.headers, baseUrl, {
|
|
817
|
+
secret: cookieSecret,
|
|
818
|
+
sessionDataTtl,
|
|
819
|
+
domain
|
|
820
|
+
});
|
|
821
|
+
if (sessionDataCookie) {
|
|
822
|
+
const [parsedSessionData] = parseSetCookies(sessionDataCookie);
|
|
823
|
+
if (parsedSessionData) await ctx.setCookie(parsedSessionData.name, parsedSessionData.value, parsedSessionData);
|
|
824
|
+
}
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error("[fetchWithAuth] Failed to mint session data cookie:", error);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const responseData = await response.json().catch(() => null);
|
|
830
|
+
if (!response.ok) return {
|
|
831
|
+
data: null,
|
|
832
|
+
error: {
|
|
833
|
+
message: responseData?.message || response.statusText,
|
|
834
|
+
status: response.status,
|
|
835
|
+
statusText: response.statusText
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
return {
|
|
839
|
+
data: responseData,
|
|
840
|
+
error: null
|
|
841
|
+
};
|
|
842
|
+
};
|
|
843
|
+
const baseServer = createApiProxy(API_ENDPOINTS, fetchWithAuth);
|
|
844
|
+
const originalGetSession = baseServer.getSession;
|
|
845
|
+
baseServer.getSession = async (...args) => {
|
|
846
|
+
const [data] = args;
|
|
847
|
+
if (!(data?.query?.disableCookieCache === "true")) try {
|
|
848
|
+
const cookiesString = await (await getContext()).getCookies();
|
|
849
|
+
const hasSessionToken = cookiesString.includes(NEON_AUTH_SESSION_COOKIE_NAME);
|
|
850
|
+
const sessionDataCookie = parseCookieValue(cookiesString, NEON_AUTH_SESSION_DATA_COOKIE_NAME);
|
|
851
|
+
if (sessionDataCookie && hasSessionToken) {
|
|
852
|
+
const result = await validateSessionData(sessionDataCookie, cookieSecret);
|
|
853
|
+
if (result.valid && result.payload) return {
|
|
854
|
+
data: result.payload,
|
|
855
|
+
error: null
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.error("[auth.getSession] Cookie validation error:", error);
|
|
860
|
+
}
|
|
861
|
+
return originalGetSession(...args);
|
|
862
|
+
};
|
|
863
|
+
return baseServer;
|
|
864
|
+
}
|
|
865
|
+
function isEndpointConfig(value) {
|
|
866
|
+
return typeof value === "object" && value !== null && "path" in value && "method" in value;
|
|
867
|
+
}
|
|
868
|
+
function createApiProxy(endpoints, fetchFn) {
|
|
869
|
+
return new Proxy({}, {
|
|
870
|
+
get(target, prop) {
|
|
871
|
+
if (prop in target) return target[prop];
|
|
872
|
+
const endpoint = endpoints[prop];
|
|
873
|
+
if (!endpoint) return;
|
|
874
|
+
if (isEndpointConfig(endpoint)) return (args) => fetchFn(endpoint.path, endpoint.method, args);
|
|
875
|
+
return createApiProxy(endpoint, fetchFn);
|
|
876
|
+
},
|
|
877
|
+
set(target, prop, value) {
|
|
878
|
+
target[prop] = value;
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/next/server/adapter.ts
|
|
886
|
+
/**
|
|
887
|
+
* Creates a Next.js-specific RequestContext that reads cookies and headers
|
|
888
|
+
* from next/headers and handles cookie setting.
|
|
889
|
+
*/
|
|
890
|
+
async function createNextRequestContext() {
|
|
891
|
+
const cookieStore = await cookies();
|
|
892
|
+
const headerStore = await headers();
|
|
893
|
+
return {
|
|
894
|
+
getCookies() {
|
|
895
|
+
return extractNeonAuthCookies(headerStore);
|
|
896
|
+
},
|
|
897
|
+
setCookie(name, value, options) {
|
|
898
|
+
cookieStore.set(name, value, options);
|
|
899
|
+
},
|
|
900
|
+
getHeader(name) {
|
|
901
|
+
return headerStore.get(name) ?? null;
|
|
902
|
+
},
|
|
903
|
+
getOrigin() {
|
|
904
|
+
return headerStore.get("origin") || headerStore.get("referer")?.split("/").slice(0, 3).join("/") || "";
|
|
905
|
+
},
|
|
906
|
+
getFramework() {
|
|
907
|
+
return "nextjs";
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/server/config.ts
|
|
914
|
+
/**
|
|
915
|
+
* Framework-agnostic configuration types for Neon Auth
|
|
916
|
+
*/
|
|
917
|
+
/**
|
|
918
|
+
* Validates cookie configuration meets security requirements
|
|
919
|
+
* @param cookies - The cookie configuration to validate
|
|
920
|
+
* @throws Error if secret is too short (< 32 characters)
|
|
921
|
+
*/
|
|
922
|
+
function validateCookieConfig(cookies$1) {
|
|
923
|
+
if (!cookies$1.secret) throw new Error(ERRORS.MISSING_COOKIE_SECRET);
|
|
924
|
+
if (cookies$1.secret.length < 32) throw new Error(ERRORS.COOKIE_SECRET_TOO_SHORT);
|
|
925
|
+
if (cookies$1.sessionDataTtl !== void 0 && cookies$1.sessionDataTtl <= 0) throw new Error(ERRORS.INVALID_SESSION_DATA_TTL);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
//#endregion
|
|
929
|
+
//#region src/next/server/handler.ts
|
|
930
|
+
/**
|
|
931
|
+
* An API route handler to handle the auth requests from the client and proxy them to the Neon Auth.
|
|
932
|
+
*
|
|
933
|
+
* @param config - Required configuration
|
|
934
|
+
* @param config.baseUrl - Base URL of your Neon Auth instance
|
|
935
|
+
* @param config.cookies - Cookie configuration
|
|
936
|
+
* @param config.cookies.secret - Secret for signing session cookies (minimum 32 characters)
|
|
937
|
+
* @param config.cookies.sessionDataTtl - Optional TTL for session cache in seconds (default: 300)
|
|
938
|
+
* @returns A Next.js API handler functions that can be used in a Next.js route.
|
|
939
|
+
* @throws Error if `cookies.secret` is less than 32 characters
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* Mount the `authApiHandler` to an API route. Create a route file inside `/api/auth/[...all]/route.ts` directory.
|
|
943
|
+
* And add the following code:
|
|
944
|
+
*
|
|
945
|
+
* ```ts
|
|
946
|
+
* // app/api/auth/[...all]/route.ts
|
|
947
|
+
* import { authApiHandler } from '@neondatabase/auth/next';
|
|
948
|
+
*
|
|
949
|
+
* export const { GET, POST } = authApiHandler({
|
|
950
|
+
* baseUrl: process.env.NEON_AUTH_BASE_URL!,
|
|
951
|
+
* cookies: {
|
|
952
|
+
* secret: process.env.NEON_AUTH_COOKIE_SECRET!,
|
|
953
|
+
* },
|
|
954
|
+
* });
|
|
955
|
+
* ```
|
|
956
|
+
*/
|
|
957
|
+
function authApiHandler(config) {
|
|
958
|
+
const { baseUrl, cookies: cookies$1 } = config;
|
|
959
|
+
validateCookieConfig(cookies$1);
|
|
960
|
+
const handler = async (request, { params }) => {
|
|
961
|
+
return handleAuthProxyRequest({
|
|
962
|
+
request,
|
|
963
|
+
path: (await params).path.join("/"),
|
|
964
|
+
baseUrl,
|
|
965
|
+
cookieSecret: cookies$1.secret,
|
|
966
|
+
sessionDataTtl: cookies$1.sessionDataTtl,
|
|
967
|
+
domain: cookies$1.domain
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
return {
|
|
971
|
+
GET: handler,
|
|
972
|
+
POST: handler,
|
|
973
|
+
PUT: handler,
|
|
974
|
+
DELETE: handler,
|
|
975
|
+
PATCH: handler
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/server/middleware/oauth.ts
|
|
981
|
+
/**
|
|
982
|
+
* Checks if the current request needs OAuth session verification
|
|
983
|
+
* This happens when returning from OAuth provider with a verifier token
|
|
984
|
+
*
|
|
985
|
+
* @param request - Standard Web API Request object
|
|
986
|
+
* @returns true if session verification is needed
|
|
987
|
+
*/
|
|
988
|
+
function needsSessionVerification(request) {
|
|
989
|
+
const hasVerifier = new URL(request.url).searchParams.has(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
|
|
990
|
+
const cookieHeader = request.headers.get("cookie");
|
|
991
|
+
if (!cookieHeader) return false;
|
|
992
|
+
const hasChallenge = parseCookies(cookieHeader).has(NEON_AUTH_SESSION_CHALLENGE_COOKIE_NAME);
|
|
993
|
+
return hasVerifier && hasChallenge;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Exchanges OAuth verifier token for session cookie
|
|
997
|
+
* This completes the OAuth flow by verifying the session challenge
|
|
998
|
+
*
|
|
999
|
+
* @param request - Standard Web API Request object
|
|
1000
|
+
* @param baseUrl - Base URL of Neon Auth server
|
|
1001
|
+
* @param cookieSecret - Secret for signing session cookies
|
|
1002
|
+
* @param sessionDataTtl - Optional TTL for session data cache
|
|
1003
|
+
* @param domain - Optional cookie domain
|
|
1004
|
+
* @returns Exchange result with redirect URL and cookies, or null if exchange not needed/failed
|
|
1005
|
+
*/
|
|
1006
|
+
async function exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain) {
|
|
1007
|
+
const url = new URL(request.url);
|
|
1008
|
+
const verifier = url.searchParams.get(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
|
|
1009
|
+
const cookieHeader = request.headers.get("cookie");
|
|
1010
|
+
if (!cookieHeader) return null;
|
|
1011
|
+
const challenge = parseCookies(cookieHeader).get(NEON_AUTH_SESSION_CHALLENGE_COOKIE_NAME);
|
|
1012
|
+
if (!verifier || !challenge) return null;
|
|
1013
|
+
const response = await handleAuthResponse(await handleAuthRequest(baseUrl, new Request(request.url, {
|
|
1014
|
+
method: "GET",
|
|
1015
|
+
headers: request.headers
|
|
1016
|
+
}), "get-session"), baseUrl, {
|
|
1017
|
+
secret: cookieSecret,
|
|
1018
|
+
sessionDataTtl,
|
|
1019
|
+
domain
|
|
1020
|
+
});
|
|
1021
|
+
if (response.ok) {
|
|
1022
|
+
const setCookieHeaders = response.headers.getSetCookie();
|
|
1023
|
+
url.searchParams.delete(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
|
|
1024
|
+
return {
|
|
1025
|
+
redirectUrl: url,
|
|
1026
|
+
cookies: setCookieHeaders,
|
|
1027
|
+
success: true
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/server/middleware/route-protection.ts
|
|
1035
|
+
/**
|
|
1036
|
+
* Checks if a given pathname should be protected (require authentication)
|
|
1037
|
+
*
|
|
1038
|
+
* @param pathname - URL pathname to check
|
|
1039
|
+
* @param skipRoutes - Array of route prefixes to skip protection
|
|
1040
|
+
* @returns true if route should be protected, false if it should be skipped
|
|
1041
|
+
*/
|
|
1042
|
+
function shouldProtectRoute(pathname, skipRoutes) {
|
|
1043
|
+
return !skipRoutes.some((route) => pathname.startsWith(route));
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Checks if the current request requires a valid session
|
|
1047
|
+
* Returns result indicating if request should proceed, redirect, or continue
|
|
1048
|
+
*
|
|
1049
|
+
* @param pathname - URL pathname being accessed
|
|
1050
|
+
* @param skipRoutes - Routes that don't require authentication
|
|
1051
|
+
* @param loginUrl - URL to redirect to for login (if applicable)
|
|
1052
|
+
* @param session - Current session data (null if not authenticated)
|
|
1053
|
+
* @returns Session check result
|
|
1054
|
+
*/
|
|
1055
|
+
function checkSessionRequired(pathname, skipRoutes, loginUrl, session) {
|
|
1056
|
+
if (pathname.startsWith(loginUrl)) return {
|
|
1057
|
+
allowed: true,
|
|
1058
|
+
requiresRedirect: false
|
|
1059
|
+
};
|
|
1060
|
+
if (!shouldProtectRoute(pathname, skipRoutes)) return {
|
|
1061
|
+
allowed: true,
|
|
1062
|
+
requiresRedirect: false
|
|
1063
|
+
};
|
|
1064
|
+
if (!session || session.session === null) return {
|
|
1065
|
+
allowed: false,
|
|
1066
|
+
requiresRedirect: true
|
|
1067
|
+
};
|
|
1068
|
+
return {
|
|
1069
|
+
allowed: true,
|
|
1070
|
+
session,
|
|
1071
|
+
requiresRedirect: false
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/server/middleware/processor.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* Generic authentication middleware processor (framework-agnostic)
|
|
1079
|
+
*
|
|
1080
|
+
* Handles the complete middleware flow:
|
|
1081
|
+
* 1. Check if login URL (skip auth)
|
|
1082
|
+
* 2. Check OAuth verification (exchange token)
|
|
1083
|
+
* 3. Get session (delegates to handleAuthProxyRequest for cookie cache + upstream fallback)
|
|
1084
|
+
* 4. Check if route requires protection
|
|
1085
|
+
* 5. Return decision object
|
|
1086
|
+
*
|
|
1087
|
+
* This is framework-agnostic - it returns a decision, NOT a framework-specific response.
|
|
1088
|
+
* The calling framework converts the decision to its response type (NextResponse, etc.)
|
|
1089
|
+
*
|
|
1090
|
+
* @param config - Middleware configuration
|
|
1091
|
+
* @returns Decision object indicating what action to take
|
|
1092
|
+
*/
|
|
1093
|
+
async function processAuthMiddleware(config) {
|
|
1094
|
+
const { request, pathname, skipRoutes, loginUrl, baseUrl, cookieSecret, sessionDataTtl, domain } = config;
|
|
1095
|
+
if (pathname.startsWith(loginUrl)) return { action: "allow" };
|
|
1096
|
+
if (needsSessionVerification(request)) {
|
|
1097
|
+
const exchangeResult = await exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain);
|
|
1098
|
+
if (exchangeResult !== null) return {
|
|
1099
|
+
action: "redirect_oauth",
|
|
1100
|
+
redirectUrl: exchangeResult.redirectUrl,
|
|
1101
|
+
cookies: exchangeResult.cookies
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
1105
|
+
const hasSessionToken = cookieHeader.includes(NEON_AUTH_SESSION_COOKIE_NAME);
|
|
1106
|
+
const hasStaleSessionData = parseCookies(cookieHeader).has(NEON_AUTH_SESSION_DATA_COOKIE_NAME) && !hasSessionToken;
|
|
1107
|
+
let sessionData = {
|
|
1108
|
+
session: null,
|
|
1109
|
+
user: null
|
|
1110
|
+
};
|
|
1111
|
+
if (hasSessionToken) {
|
|
1112
|
+
const sessionResponse = await handleAuthProxyRequest({
|
|
1113
|
+
request,
|
|
1114
|
+
path: "get-session",
|
|
1115
|
+
baseUrl,
|
|
1116
|
+
cookieSecret,
|
|
1117
|
+
sessionDataTtl,
|
|
1118
|
+
domain
|
|
1119
|
+
});
|
|
1120
|
+
if (sessionResponse.ok) {
|
|
1121
|
+
const data = await sessionResponse.json().catch(() => null);
|
|
1122
|
+
if (data) sessionData = data;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (checkSessionRequired(pathname, skipRoutes, loginUrl, sessionData).allowed) return {
|
|
1126
|
+
action: "allow",
|
|
1127
|
+
headers: { [NEON_AUTH_HEADER_MIDDLEWARE_NAME]: "true" }
|
|
1128
|
+
};
|
|
1129
|
+
const cookies$1 = [];
|
|
1130
|
+
if (hasStaleSessionData) cookies$1.push(serializeSetCookie({
|
|
1131
|
+
name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
|
|
1132
|
+
value: "",
|
|
1133
|
+
path: "/",
|
|
1134
|
+
domain,
|
|
1135
|
+
httpOnly: true,
|
|
1136
|
+
secure: true,
|
|
1137
|
+
sameSite: "lax",
|
|
1138
|
+
maxAge: 0
|
|
1139
|
+
}));
|
|
1140
|
+
return {
|
|
1141
|
+
action: "redirect_login",
|
|
1142
|
+
redirectUrl: new URL(loginUrl, request.url),
|
|
1143
|
+
cookies: cookies$1.length > 0 ? cookies$1 : void 0
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
//#endregion
|
|
1148
|
+
//#region src/next/server/middleware.ts
|
|
1149
|
+
const SKIP_ROUTES = [
|
|
1150
|
+
"/api/auth",
|
|
1151
|
+
"/auth/callback",
|
|
1152
|
+
"/auth/sign-in",
|
|
1153
|
+
"/auth/sign-up",
|
|
1154
|
+
"/auth/magic-link",
|
|
1155
|
+
"/auth/email-otp",
|
|
1156
|
+
"/auth/forgot-password"
|
|
1157
|
+
];
|
|
1158
|
+
/**
|
|
1159
|
+
* A Next.js middleware to protect routes from unauthenticated requests and refresh the session if required.
|
|
1160
|
+
*
|
|
1161
|
+
* @param config - Required middleware configuration
|
|
1162
|
+
* @param config.baseUrl - Base URL of your Neon Auth instance
|
|
1163
|
+
* @param config.cookies - Cookie configuration
|
|
1164
|
+
* @param config.cookies.secret - Secret for signing session cookies (minimum 32 characters)
|
|
1165
|
+
* @param config.cookies.sessionDataTtl - Optional TTL for session cache in seconds (default: 300)
|
|
1166
|
+
* @param config.loginUrl - The URL to redirect to when the user is not authenticated (default: '/auth/sign-in')
|
|
1167
|
+
* @returns A middleware function that can be used in the Next.js app.
|
|
1168
|
+
* @throws Error if `cookies.secret` is less than 32 characters
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```ts
|
|
1172
|
+
* import { neonAuthMiddleware } from "@neondatabase/auth/next"
|
|
1173
|
+
*
|
|
1174
|
+
* export default neonAuthMiddleware({
|
|
1175
|
+
* baseUrl: process.env.NEON_AUTH_BASE_URL!,
|
|
1176
|
+
* cookies: {
|
|
1177
|
+
* secret: process.env.NEON_AUTH_COOKIE_SECRET!,
|
|
1178
|
+
* },
|
|
1179
|
+
* loginUrl: '/auth/sign-in',
|
|
1180
|
+
* });
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
function neonAuthMiddleware(config) {
|
|
1184
|
+
const { baseUrl, cookies: cookies$1, loginUrl = "/auth/sign-in" } = config;
|
|
1185
|
+
validateCookieConfig(cookies$1);
|
|
1186
|
+
return async (request) => {
|
|
1187
|
+
const pathname = request.nextUrl.pathname;
|
|
1188
|
+
const result = await processAuthMiddleware({
|
|
1189
|
+
request,
|
|
1190
|
+
pathname,
|
|
1191
|
+
skipRoutes: SKIP_ROUTES,
|
|
1192
|
+
loginUrl,
|
|
1193
|
+
baseUrl,
|
|
1194
|
+
cookieSecret: cookies$1.secret,
|
|
1195
|
+
sessionDataTtl: cookies$1.sessionDataTtl,
|
|
1196
|
+
domain: cookies$1.domain
|
|
1197
|
+
});
|
|
1198
|
+
switch (result.action) {
|
|
1199
|
+
case "allow": {
|
|
1200
|
+
const headers$1 = new Headers(request.headers);
|
|
1201
|
+
if (result.headers) for (const [key, value] of Object.entries(result.headers)) headers$1.set(key, value);
|
|
1202
|
+
return NextResponse.next({ request: { headers: headers$1 } });
|
|
1203
|
+
}
|
|
1204
|
+
case "redirect_oauth": {
|
|
1205
|
+
const oauthHeaders = new Headers();
|
|
1206
|
+
for (const cookie of result.cookies) oauthHeaders.append("Set-Cookie", cookie);
|
|
1207
|
+
return NextResponse.redirect(result.redirectUrl, { headers: oauthHeaders });
|
|
1208
|
+
}
|
|
1209
|
+
case "redirect_login":
|
|
1210
|
+
if (result.cookies && result.cookies.length > 0) {
|
|
1211
|
+
const loginHeaders = new Headers();
|
|
1212
|
+
for (const cookie of result.cookies) loginHeaders.append("Set-Cookie", cookie);
|
|
1213
|
+
return NextResponse.redirect(result.redirectUrl, { headers: loginHeaders });
|
|
1214
|
+
}
|
|
1215
|
+
return NextResponse.redirect(result.redirectUrl);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/next/server/index.ts
|
|
1222
|
+
/**
|
|
1223
|
+
* Unified entry point for Neon Auth in Next.js
|
|
1224
|
+
*
|
|
1225
|
+
* This is the recommended way to use Neon Auth in Next.js. It provides a single
|
|
1226
|
+
* entry point that combines all server-side functionality.
|
|
1227
|
+
*
|
|
1228
|
+
* **Features:**
|
|
1229
|
+
* - All Better Auth server methods (signIn, signUp, getSession, etc.)
|
|
1230
|
+
* - `.handler()` - API route handler for `/api/auth/[...path]`
|
|
1231
|
+
* - `.middleware(config?)` - Middleware for route protection
|
|
1232
|
+
*
|
|
1233
|
+
* **Where to use:**
|
|
1234
|
+
* - React Server Components
|
|
1235
|
+
* - Server Actions
|
|
1236
|
+
* - Route Handlers
|
|
1237
|
+
* - Middleware
|
|
1238
|
+
*
|
|
1239
|
+
* @param config - Required configuration
|
|
1240
|
+
* @param config.baseUrl - Base URL of your Neon Auth instance
|
|
1241
|
+
* @param config.cookies - Cookie configuration
|
|
1242
|
+
* @param config.cookies.secret - Secret for signing session cookies (minimum 32 characters)
|
|
1243
|
+
* @param config.cookies.sessionDataTtl - Optional TTL for session cache in seconds (default: 300)
|
|
1244
|
+
* @param config.cookies.domain - Optional cookie domain (default: current domain)
|
|
1245
|
+
* @returns Unified auth instance with server methods, handler, and middleware
|
|
1246
|
+
* @throws Error if `cookies.secret` is less than 32 characters
|
|
1247
|
+
*
|
|
1248
|
+
* @example
|
|
1249
|
+
* ```typescript
|
|
1250
|
+
* // lib/auth.ts - Create a singleton instance
|
|
1251
|
+
* import { createNeonAuth } from '@neondatabase/auth/next/server';
|
|
1252
|
+
*
|
|
1253
|
+
* export const auth = createNeonAuth({
|
|
1254
|
+
* baseUrl: process.env.NEON_AUTH_BASE_URL!,
|
|
1255
|
+
* cookies: {
|
|
1256
|
+
* secret: process.env.NEON_AUTH_COOKIE_SECRET!,
|
|
1257
|
+
* sessionDataTtl: 300, // 5 minutes (default)
|
|
1258
|
+
* },
|
|
1259
|
+
* });
|
|
1260
|
+
* ```
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```typescript
|
|
1264
|
+
* // app/api/auth/[...path]/route.ts - API handler
|
|
1265
|
+
* import { auth } from '@/lib/auth';
|
|
1266
|
+
*
|
|
1267
|
+
* export const { GET, POST } = auth.handler();
|
|
1268
|
+
* ```
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```typescript
|
|
1272
|
+
* // middleware.ts - Route protection
|
|
1273
|
+
* import { auth } from '@/lib/auth';
|
|
1274
|
+
*
|
|
1275
|
+
* export default auth.middleware({ loginUrl: '/auth/sign-in' });
|
|
1276
|
+
*
|
|
1277
|
+
* export const config = {
|
|
1278
|
+
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
1279
|
+
* };
|
|
1280
|
+
* ```
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* ```typescript
|
|
1284
|
+
* // app/page.tsx - Server Component
|
|
1285
|
+
* import { auth } from '@/lib/auth';
|
|
1286
|
+
*
|
|
1287
|
+
* // Server components using `auth` methods must be rendered dynamically
|
|
1288
|
+
* export const dynamic = 'force-dynamic'
|
|
1289
|
+
*
|
|
1290
|
+
* export default async function Page() {
|
|
1291
|
+
* const { data: session } = await auth.getSession();
|
|
1292
|
+
* if (!session?.user) return <div>Not logged in</div>;
|
|
1293
|
+
* return <div>Hello {session.user.name}</div>;
|
|
1294
|
+
* }
|
|
1295
|
+
* ```
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* ```typescript
|
|
1299
|
+
* // app/actions.ts - Server Action
|
|
1300
|
+
* 'use server';
|
|
1301
|
+
* import { auth } from '@/lib/auth';
|
|
1302
|
+
* import { redirect } from 'next/navigation';
|
|
1303
|
+
*
|
|
1304
|
+
* export async function signIn(formData: FormData) {
|
|
1305
|
+
* const { error } = await auth.signIn.email({
|
|
1306
|
+
* email: formData.get('email') as string,
|
|
1307
|
+
* password: formData.get('password') as string,
|
|
1308
|
+
* });
|
|
1309
|
+
* if (error) return { error: error.message };
|
|
1310
|
+
* redirect('/dashboard');
|
|
1311
|
+
* }
|
|
1312
|
+
* ```
|
|
1313
|
+
*/
|
|
1314
|
+
function createNeonAuth(config) {
|
|
1315
|
+
const { baseUrl, cookies: cookies$1 } = config;
|
|
1316
|
+
validateCookieConfig(cookies$1);
|
|
1317
|
+
const server = createAuthServerInternal({
|
|
1318
|
+
baseUrl,
|
|
1319
|
+
context: createNextRequestContext,
|
|
1320
|
+
cookieSecret: cookies$1.secret,
|
|
1321
|
+
sessionDataTtl: cookies$1.sessionDataTtl,
|
|
1322
|
+
domain: cookies$1.domain
|
|
1323
|
+
});
|
|
1324
|
+
/**
|
|
1325
|
+
* Creates API route handlers for Next.js
|
|
1326
|
+
*
|
|
1327
|
+
* Mount this in your API routes to handle auth requests:
|
|
1328
|
+
* - `/api/auth/[...path]/route.ts`
|
|
1329
|
+
*
|
|
1330
|
+
* @returns Object with GET, POST, PUT, DELETE, PATCH handlers
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* ```typescript
|
|
1334
|
+
* // app/api/auth/[...path]/route.ts
|
|
1335
|
+
* import { auth } from '@/lib/auth';
|
|
1336
|
+
*
|
|
1337
|
+
* export const { GET, POST } = auth.handler();
|
|
1338
|
+
* ```
|
|
1339
|
+
*/
|
|
1340
|
+
server.handler = () => authApiHandler(config);
|
|
1341
|
+
/**
|
|
1342
|
+
* Creates middleware for route protection
|
|
1343
|
+
*
|
|
1344
|
+
* Protects routes from unauthenticated access and handles:
|
|
1345
|
+
* - Session validation and refresh
|
|
1346
|
+
* - OAuth callback processing
|
|
1347
|
+
* - Login redirects
|
|
1348
|
+
*
|
|
1349
|
+
* @param middlewareConfig - Optional middleware configuration
|
|
1350
|
+
* @param middlewareConfig.loginUrl - URL to redirect to when not authenticated (default: '/auth/sign-in')
|
|
1351
|
+
* @returns Middleware function for Next.js
|
|
1352
|
+
*
|
|
1353
|
+
* @example
|
|
1354
|
+
* ```typescript
|
|
1355
|
+
* // middleware.ts
|
|
1356
|
+
* import { auth } from '@/lib/auth';
|
|
1357
|
+
*
|
|
1358
|
+
* export default auth.middleware({ loginUrl: '/auth/sign-in' });
|
|
1359
|
+
*
|
|
1360
|
+
* export const config = {
|
|
1361
|
+
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
1362
|
+
* };
|
|
1363
|
+
* ```
|
|
1364
|
+
*/
|
|
1365
|
+
server.middleware = (middlewareConfig) => neonAuthMiddleware({
|
|
1366
|
+
...config,
|
|
1367
|
+
...middlewareConfig
|
|
1368
|
+
});
|
|
1369
|
+
return server;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
//#endregion
|
|
1373
|
+
export { createNeonAuth };
|