@neondatabase/auth 0.1.0-beta.9 → 0.3.0-beta

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.
Files changed (46) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -18
  3. package/dist/{adapter-core-Bw9mn_AS.d.mts → adapter-core-B9uDhoYq.d.mts} +270 -375
  4. package/dist/{adapter-core-C_NEMs0b.mjs → adapter-core-D00qcqMo.mjs} +392 -67
  5. package/dist/better-auth-react-adapter-BO4jLN4H.d.mts +2170 -0
  6. package/dist/{better-auth-react-adapter-BbM3jLLv.mjs → better-auth-react-adapter-Xdj-69i9.mjs} +10 -8
  7. package/dist/constants-Cupc_bln.mjs +28 -0
  8. package/dist/{index-BXlAjlSt.d.mts → index-B_Q0Tp1D.d.mts} +0 -1
  9. package/dist/index.d.mts +13 -19
  10. package/dist/index.mjs +2 -1
  11. package/dist/{neon-auth-DdlToh7_.mjs → neon-auth-DBOB8sXF.mjs} +7 -4
  12. package/dist/next/index.d.mts +77 -186
  13. package/dist/next/index.mjs +4 -311
  14. package/dist/next/server/index.d.mts +536 -0
  15. package/dist/next/server/index.mjs +1461 -0
  16. package/dist/react/adapters/index.d.mts +4 -4
  17. package/dist/react/adapters/index.mjs +2 -1
  18. package/dist/react/index.d.mts +6 -5
  19. package/dist/react/index.mjs +5 -4
  20. package/dist/react/ui/index.d.mts +1 -2
  21. package/dist/react/ui/index.mjs +2 -4
  22. package/dist/react/ui/server.mjs +2 -2
  23. package/dist/{supabase-adapter-CAqbpOC7.mjs → supabase-adapter-CIBMebXB.mjs} +28 -45
  24. package/dist/supabase-adapter-CSDRL1ZU.d.mts +2227 -0
  25. package/dist/types/index.d.mts +2 -7
  26. package/dist/ui/.safelist.html +3 -0
  27. package/dist/ui/css.css +2 -2
  28. package/dist/ui/tailwind.css +2 -1
  29. package/dist/ui/theme-inline.css +44 -0
  30. package/dist/ui/theme.css +103 -76
  31. package/dist/ui-CnVnqGns.mjs +3 -0
  32. package/dist/vanilla/adapters/index.d.mts +3 -3
  33. package/dist/vanilla/adapters/index.mjs +2 -1
  34. package/dist/vanilla/index.d.mts +3 -3
  35. package/dist/vanilla/index.mjs +2 -1
  36. package/llms.txt +330 -0
  37. package/package.json +32 -23
  38. package/dist/better-auth-react-adapter-JoscqoDc.d.mts +0 -722
  39. package/dist/better-auth-types-CE4hLv9E.d.mts +0 -9
  40. package/dist/chunk-5DLVHPZS-Bxj7snpZ-DoVNlsyk.mjs +0 -533
  41. package/dist/supabase-adapter-Clxlqg1x.d.mts +0 -127
  42. package/dist/ui-aMoA-9nq.mjs +0 -9449
  43. /package/dist/{adapters-D0mxG3F-.mjs → adapters-CUvhsAvY.mjs} +0 -0
  44. /package/dist/{adapters-Df6Dd3KK.mjs → adapters-CivF9wql.mjs} +0 -0
  45. /package/dist/{index-ClXLQ1fw.d.mts → index-CPnFzULh.d.mts} +0 -0
  46. /package/dist/{index-DCQ5Y2ED.d.mts → index-UW23fDSn.d.mts} +0 -0
@@ -0,0 +1,1461 @@
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
+ * Fetch session data from upstream using session token cookie
478
+ *
479
+ * @param sessionTokenCookie - Session token cookie string (can be Set-Cookie header or "name=value" format)
480
+ * @param baseUrl - Auth server base URL
481
+ * @returns Session data from upstream
482
+ */
483
+ async function fetchSessionWithCookie(sessionTokenCookie, baseUrl) {
484
+ let cookieName;
485
+ let cookieValue;
486
+ if (sessionTokenCookie.includes("=")) {
487
+ const [name, ...valueParts] = sessionTokenCookie.split(";")[0].trim().split("=");
488
+ cookieName = name.trim();
489
+ cookieValue = valueParts.join("=").trim();
490
+ } else throw new Error("Invalid session token cookie format");
491
+ if (!cookieName.includes("session_token")) throw new Error("session_token not found in cookie");
492
+ const response = await fetch(`${baseUrl}/get-session`, {
493
+ headers: { Cookie: `${cookieName}=${cookieValue}` },
494
+ signal: AbortSignal.timeout(3e3)
495
+ });
496
+ if (!response.ok) throw new Error(`Failed to fetch session data: ${response.status} ${response.statusText}`);
497
+ let body;
498
+ try {
499
+ body = await response.json();
500
+ } catch (error) {
501
+ throw new Error(`Failed to parse /get-session response as JSON: ${error instanceof Error ? error.message : String(error)}`);
502
+ }
503
+ return parseSessionData(body);
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/server/errors.ts
508
+ const ERRORS = {
509
+ MISSING_AUTH_BASE_URL: "Missing required config: baseUrl. You must provide the auth URL of your Neon Auth instance in the config object.",
510
+ MISSING_COOKIE_SECRET: "Missing required config: cookies.secret. You must provide the cookie secret in the config object.",
511
+ COOKIE_SECRET_TOO_SHORT: "cookies.secret must be at least 32 characters long for security. Generate a secure secret with: openssl rand -base64 32",
512
+ INVALID_SESSION_DATA_TTL: "cookies.sessionDataTtl must be a positive number (in seconds)"
513
+ };
514
+
515
+ //#endregion
516
+ //#region src/server/session/validator.ts
517
+ /**
518
+ * Validate session data signature and expiry using jose
519
+ * @param sessionDataString - Session data string to validate
520
+ * @param cookieSecret - cookie secret for validation
521
+ * @returns Validation result with payload if valid
522
+ */
523
+ async function validateSessionData(sessionDataString, cookieSecret) {
524
+ try {
525
+ const { payload } = await jwtVerify(sessionDataString, new TextEncoder().encode(cookieSecret), { algorithms: ["HS256"] });
526
+ return {
527
+ valid: true,
528
+ payload: parseSessionData(payload)
529
+ };
530
+ } catch (error) {
531
+ return {
532
+ valid: false,
533
+ error: error instanceof Error ? error.message : "Invalid session data"
534
+ };
535
+ }
536
+ }
537
+
538
+ //#endregion
539
+ //#region src/server/session/minting.ts
540
+ /**
541
+ * Core minting logic - creates session_data cookie from session token
542
+ *
543
+ * @param sessionTokenCookie - Session token cookie string (format: "name=value")
544
+ * @param baseUrl - Auth server base URL
545
+ * @param cookieConfig - Cookie configuration
546
+ * @returns Set-Cookie string or null on error
547
+ */
548
+ async function mintSessionDataCookie(sessionTokenCookie, baseUrl, cookieConfig) {
549
+ try {
550
+ const sessionData = await fetchSessionWithCookie(sessionTokenCookie, baseUrl);
551
+ if (!sessionData.session) return null;
552
+ const { value: signedData, expiresAt } = await signSessionDataCookie(sessionData, cookieConfig.secret, cookieConfig.sessionDataTtl);
553
+ const maxAge = Math.floor((expiresAt.getTime() - Date.now()) / 1e3);
554
+ return serializeSetCookie({
555
+ name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
556
+ value: signedData,
557
+ path: "/",
558
+ domain: cookieConfig.domain,
559
+ httpOnly: true,
560
+ secure: true,
561
+ sameSite: "lax",
562
+ maxAge
563
+ });
564
+ } catch (error) {
565
+ const errorMessage = error instanceof Error ? error.message : String(error);
566
+ console.error("[mintSessionDataCookie] Failed to mint session_data cookie:", {
567
+ error: errorMessage,
568
+ ...process.env.NODE_ENV !== "production" && { stack: error instanceof Error ? error.stack : void 0 }
569
+ });
570
+ return null;
571
+ }
572
+ }
573
+ /**
574
+ * Utility A: Mint session_data cookie when session_token is updated by upstream
575
+ *
576
+ * Checks response headers for session_token in Set-Cookie, then mints session_data.
577
+ * Handles token deletion (max-age=0) by returning deletion cookie.
578
+ *
579
+ * Use case: Response handling in proxy/middleware after upstream auth calls
580
+ *
581
+ * @param responseHeaders - Response headers from upstream auth server
582
+ * @param baseUrl - Auth server base URL
583
+ * @param cookieConfig - Cookie configuration
584
+ * @returns Set-Cookie string for session_data or null if no action needed
585
+ */
586
+ async function mintSessionDataFromResponse(responseHeaders, baseUrl, cookieConfig) {
587
+ const sessionTokenCookie = responseHeaders.getSetCookie().find((cookie) => cookie.includes("session_token"));
588
+ if (!sessionTokenCookie) return null;
589
+ if (sessionTokenCookie.toLowerCase().includes("max-age=0")) return serializeSetCookie({
590
+ name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
591
+ value: "",
592
+ path: "/",
593
+ domain: cookieConfig.domain,
594
+ httpOnly: true,
595
+ secure: true,
596
+ sameSite: "lax",
597
+ maxAge: 0
598
+ });
599
+ return await mintSessionDataCookie(sessionTokenCookie, baseUrl, cookieConfig);
600
+ }
601
+ /**
602
+ * Utility B: Mint/refresh session_data cookie from existing session_token
603
+ *
604
+ * Extracts session_token from request cookies and mints a fresh session_data cookie.
605
+ * Use case: Cache misses, expired cookies, proactive refresh
606
+ *
607
+ * @param sessionTokenCookie - Session token cookie string (format: "name=value")
608
+ * @param baseUrl - Auth server base URL
609
+ * @param cookieConfig - Cookie configuration
610
+ * @returns Set-Cookie string for session_data or null on error
611
+ */
612
+ async function mintSessionDataFromToken(sessionTokenCookie, baseUrl, cookieConfig) {
613
+ return await mintSessionDataCookie(sessionTokenCookie, baseUrl, cookieConfig);
614
+ }
615
+
616
+ //#endregion
617
+ //#region src/server/client-factory.ts
618
+ function createAuthServerInternal(config) {
619
+ const { baseUrl, context: getContext, cookieSecret, sessionDataTtl, domain } = config;
620
+ const fetchWithAuth = async (path, method, args) => {
621
+ const ctx = await getContext();
622
+ const cookies$1 = await ctx.getCookies();
623
+ const origin = await ctx.getOrigin();
624
+ const framework = ctx.getFramework();
625
+ const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
626
+ const { query, fetchOptions: _fetchOptions, ...body } = args || {};
627
+ if (query && typeof query === "object") {
628
+ const queryParams = query;
629
+ for (const [key, value] of Object.entries(queryParams)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
630
+ }
631
+ const headers$1 = {
632
+ Cookie: cookies$1,
633
+ Origin: origin,
634
+ [NEON_AUTH_SERVER_PROXY_HEADER]: framework
635
+ };
636
+ let requestBody;
637
+ if (method === "POST") {
638
+ headers$1["Content-Type"] = "application/json";
639
+ requestBody = JSON.stringify(Object.keys(body).length > 0 ? body : {});
640
+ }
641
+ const response = await fetch(url.toString(), {
642
+ method,
643
+ headers: headers$1,
644
+ body: requestBody
645
+ });
646
+ const setCookieHeaders = response.headers.getSetCookie();
647
+ if (setCookieHeaders.length > 0) {
648
+ for (const setCookieHeader of setCookieHeaders) {
649
+ const parsedCookies = parseSetCookies(setCookieHeader);
650
+ for (const cookie of parsedCookies) {
651
+ const cookieOptions = {
652
+ ...cookie,
653
+ domain,
654
+ partitioned: void 0,
655
+ sameSite: "lax"
656
+ };
657
+ await ctx.setCookie(cookie.name, cookie.value, cookieOptions);
658
+ }
659
+ }
660
+ try {
661
+ const sessionDataCookie = await mintSessionDataFromResponse(response.headers, baseUrl, {
662
+ secret: cookieSecret,
663
+ sessionDataTtl,
664
+ domain
665
+ });
666
+ if (sessionDataCookie) {
667
+ const [parsedSessionData] = parseSetCookies(sessionDataCookie);
668
+ if (parsedSessionData) await ctx.setCookie(parsedSessionData.name, parsedSessionData.value, parsedSessionData);
669
+ }
670
+ } catch (error) {
671
+ console.error("[fetchWithAuth] Failed to mint session data cookie:", error);
672
+ }
673
+ }
674
+ const responseData = await response.json().catch(() => null);
675
+ if (!response.ok) return {
676
+ data: null,
677
+ error: {
678
+ message: responseData?.message || response.statusText,
679
+ status: response.status,
680
+ statusText: response.statusText
681
+ }
682
+ };
683
+ return {
684
+ data: responseData,
685
+ error: null
686
+ };
687
+ };
688
+ const baseServer = createApiProxy(API_ENDPOINTS, fetchWithAuth);
689
+ const originalGetSession = baseServer.getSession;
690
+ baseServer.getSession = async (...args) => {
691
+ const [data] = args;
692
+ if (!(data?.query?.disableCookieCache === "true")) try {
693
+ const cookiesString = await (await getContext()).getCookies();
694
+ const hasSessionToken = cookiesString.includes(NEON_AUTH_SESSION_COOKIE_NAME);
695
+ const sessionDataCookie = parseCookieValue(cookiesString, NEON_AUTH_SESSION_DATA_COOKIE_NAME);
696
+ if (sessionDataCookie && hasSessionToken) {
697
+ const result = await validateSessionData(sessionDataCookie, cookieSecret);
698
+ if (result.valid && result.payload) return {
699
+ data: result.payload,
700
+ error: null
701
+ };
702
+ }
703
+ } catch (error) {
704
+ console.error("[auth.getSession] Cookie validation error:", error);
705
+ }
706
+ return originalGetSession(...args);
707
+ };
708
+ return baseServer;
709
+ }
710
+ function isEndpointConfig(value) {
711
+ return typeof value === "object" && value !== null && "path" in value && "method" in value;
712
+ }
713
+ function createApiProxy(endpoints, fetchFn) {
714
+ return new Proxy({}, {
715
+ get(target, prop) {
716
+ if (prop in target) return target[prop];
717
+ const endpoint = endpoints[prop];
718
+ if (!endpoint) return;
719
+ if (isEndpointConfig(endpoint)) return (args) => fetchFn(endpoint.path, endpoint.method, args);
720
+ return createApiProxy(endpoint, fetchFn);
721
+ },
722
+ set(target, prop, value) {
723
+ target[prop] = value;
724
+ return true;
725
+ }
726
+ });
727
+ }
728
+
729
+ //#endregion
730
+ //#region src/next/server/adapter.ts
731
+ /**
732
+ * Creates a Next.js-specific RequestContext that reads cookies and headers
733
+ * from next/headers and handles cookie setting.
734
+ */
735
+ async function createNextRequestContext() {
736
+ const cookieStore = await cookies();
737
+ const headerStore = await headers();
738
+ return {
739
+ getCookies() {
740
+ return extractNeonAuthCookies(headerStore);
741
+ },
742
+ setCookie(name, value, options) {
743
+ cookieStore.set(name, value, options);
744
+ },
745
+ getHeader(name) {
746
+ return headerStore.get(name) ?? null;
747
+ },
748
+ getOrigin() {
749
+ return headerStore.get("origin") || headerStore.get("referer")?.split("/").slice(0, 3).join("/") || "";
750
+ },
751
+ getFramework() {
752
+ return "nextjs";
753
+ }
754
+ };
755
+ }
756
+
757
+ //#endregion
758
+ //#region src/server/config.ts
759
+ /**
760
+ * Framework-agnostic configuration types for Neon Auth
761
+ */
762
+ /**
763
+ * Validates cookie configuration meets security requirements
764
+ * @param cookies - The cookie configuration to validate
765
+ * @throws Error if secret is too short (< 32 characters)
766
+ */
767
+ function validateCookieConfig(cookies$1) {
768
+ if (!cookies$1.secret) throw new Error(ERRORS.MISSING_COOKIE_SECRET);
769
+ if (cookies$1.secret.length < 32) throw new Error(ERRORS.COOKIE_SECRET_TOO_SHORT);
770
+ if (cookies$1.sessionDataTtl !== void 0 && cookies$1.sessionDataTtl <= 0) throw new Error(ERRORS.INVALID_SESSION_DATA_TTL);
771
+ }
772
+
773
+ //#endregion
774
+ //#region src/server/proxy/request.ts
775
+ const PROXY_HEADERS = [
776
+ "user-agent",
777
+ "authorization",
778
+ "referer",
779
+ "content-type"
780
+ ];
781
+ /**
782
+ * Proxy header constant - indicates request went through Neon Auth middleware/handler
783
+ * This is framework-agnostic and can be used by any server framework
784
+ */
785
+ const NEON_AUTH_HEADER_MIDDLEWARE_NAME = "x-neon-auth-middleware";
786
+ /**
787
+ * Handles proxying authentication requests to the upstream Neon Auth server
788
+ *
789
+ * @param baseUrl - Base URL of the Neon Auth server
790
+ * @param request - Standard Web API Request object
791
+ * @param path - API path to proxy to (e.g., 'get-session', 'sign-in')
792
+ * @returns Response from upstream server or error response
793
+ */
794
+ const handleAuthRequest = async (baseUrl, request, path) => {
795
+ const headers$1 = prepareRequestHeaders(request);
796
+ const body = await parseRequestBody(request);
797
+ try {
798
+ const upstreamURL = getUpstreamURL(baseUrl, path, { originalUrl: new URL(request.url) });
799
+ return await fetch(upstreamURL.toString(), {
800
+ method: request.method,
801
+ headers: headers$1,
802
+ body
803
+ });
804
+ } catch (error) {
805
+ if (error instanceof Error && error.name === "TypeError" && error.message.includes("fetch")) return Response.json({
806
+ error: "Unable to connect to authentication server",
807
+ code: "NETWORK_ERROR"
808
+ }, {
809
+ status: 502,
810
+ headers: { "Content-Type": "application/json" }
811
+ });
812
+ const message = error instanceof Error ? error.message : "Internal Server Error";
813
+ console.error(`[AuthError] ${message}`, error);
814
+ return Response.json({
815
+ error: message,
816
+ code: "INTERNAL_ERROR"
817
+ }, {
818
+ status: 500,
819
+ headers: { "Content-Type": "application/json" }
820
+ });
821
+ }
822
+ };
823
+ /**
824
+ * Constructs the upstream URL for proxying to Neon Auth server
825
+ *
826
+ * @param baseUrl - Base URL of the Neon Auth server
827
+ * @param path - API path (e.g., 'get-session')
828
+ * @param options - Options including original URL for preserving query params
829
+ * @returns Constructed upstream URL
830
+ */
831
+ const getUpstreamURL = (baseUrl, path, { originalUrl }) => {
832
+ const url = new URL(`${baseUrl}/${path}`);
833
+ if (originalUrl) {
834
+ url.search = originalUrl.search;
835
+ return url;
836
+ }
837
+ return url;
838
+ };
839
+ const prepareRequestHeaders = (request) => {
840
+ const headers$1 = new Headers();
841
+ for (const header of PROXY_HEADERS) if (request.headers.get(header)) headers$1.set(header, request.headers.get(header));
842
+ headers$1.set("Origin", getOrigin(request));
843
+ headers$1.set("Cookie", extractNeonAuthCookies(request.headers));
844
+ headers$1.set(NEON_AUTH_HEADER_MIDDLEWARE_NAME, "true");
845
+ return headers$1;
846
+ };
847
+ const getOrigin = (request) => {
848
+ return request.headers.get("origin") || request.headers.get("referer")?.split("/").slice(0, 3).join("/") || new URL(request.url).origin;
849
+ };
850
+ const parseRequestBody = async (request) => {
851
+ if (request.body) return request.text();
852
+ };
853
+
854
+ //#endregion
855
+ //#region src/server/proxy/response.ts
856
+ const RESPONSE_HEADERS_ALLOWLIST = [
857
+ "content-type",
858
+ "content-length",
859
+ "content-encoding",
860
+ "transfer-encoding",
861
+ "connection",
862
+ "date",
863
+ "set-cookie",
864
+ "set-auth-jwt",
865
+ "set-auth-token",
866
+ "x-neon-ret-request-id"
867
+ ];
868
+ /**
869
+ * Handles responses from upstream Neon Auth server
870
+ * - Proxies allowed headers to client
871
+ * - Mints session data cookie if session token is present
872
+ *
873
+ * @param response - Response from upstream Neon Auth server
874
+ * @param baseUrl - Base URL of Neon Auth server
875
+ * @param cookieConfig - Session cookie configuration
876
+ * @returns New Response with proxied headers and session data cookie
877
+ */
878
+ const handleAuthResponse = async (response, baseUrl, cookieConfig) => {
879
+ const responseHeaders = prepareResponseHeaders(response, cookieConfig.domain);
880
+ const sessionDataCookie = await mintSessionDataFromResponse(response.headers, baseUrl, cookieConfig);
881
+ if (sessionDataCookie) responseHeaders.append("Set-Cookie", sessionDataCookie);
882
+ return new Response(response.body, {
883
+ status: response.status,
884
+ statusText: response.statusText,
885
+ headers: responseHeaders
886
+ });
887
+ };
888
+ const prepareResponseHeaders = (response, domain) => {
889
+ const headers$1 = new Headers();
890
+ for (const header of RESPONSE_HEADERS_ALLOWLIST) if (header === "set-cookie") {
891
+ const cookies$1 = response.headers.getSetCookie();
892
+ for (const cookieHeader of cookies$1) {
893
+ const parsedCookies = parseSetCookies(cookieHeader);
894
+ for (const parsedCookie of parsedCookies) {
895
+ parsedCookie.partitioned = void 0;
896
+ parsedCookie.sameSite = "lax";
897
+ if (domain) parsedCookie.domain = domain;
898
+ headers$1.append("Set-Cookie", serializeSetCookie(parsedCookie));
899
+ }
900
+ }
901
+ } else {
902
+ const value = response.headers.get(header);
903
+ if (value) headers$1.set(header, value);
904
+ }
905
+ return headers$1;
906
+ };
907
+
908
+ //#endregion
909
+ //#region src/server/session/cache-handler.ts
910
+ /**
911
+ * Attempts to retrieve session data from cookie cache
912
+ * Returns Response with session data if cache hit, null otherwise
913
+ *
914
+ * If session_data cookie is missing or invalid, attempts to mint a new one
915
+ * from the session_token cookie (reactive minting).
916
+ *
917
+ * This is the framework-agnostic session cache optimization used by API handlers.
918
+ *
919
+ * @param request - Standard Web API Request object
920
+ * @param baseUrl - Auth server base URL for upstream calls
921
+ * @param cookieConfig - Cookie configuration (secret, TTL, domain)
922
+ * @returns Response with session data JSON if cache hit, null if miss/disabled
923
+ */
924
+ async function trySessionCache(request, baseUrl, cookieConfig) {
925
+ if (new URL(request.url).searchParams.get("disableCookieCache") === "true") return null;
926
+ const cookieHeader = request.headers.get("cookie") || "";
927
+ if (!cookieHeader.includes(NEON_AUTH_SESSION_COOKIE_NAME)) return null;
928
+ const hasSessionData = parseCookies(cookieHeader).has(NEON_AUTH_SESSION_DATA_COOKIE_NAME);
929
+ const mintAndReturn = async () => {
930
+ const sessionTokenCookie = extractSessionTokenCookie(cookieHeader);
931
+ if (!sessionTokenCookie) return null;
932
+ const sessionDataCookieString = await mintSessionDataFromToken(sessionTokenCookie, baseUrl, cookieConfig);
933
+ if (!sessionDataCookieString) return null;
934
+ try {
935
+ const sessionData = await fetchSessionWithCookie(sessionTokenCookie, baseUrl);
936
+ if (!sessionData.session) return null;
937
+ const response = Response.json(sessionData);
938
+ response.headers.set("Set-Cookie", sessionDataCookieString);
939
+ return response;
940
+ } catch (error) {
941
+ console.error("[trySessionCache] Failed to fetch session after minting cookie:", error);
942
+ return null;
943
+ }
944
+ };
945
+ if (!hasSessionData) return await mintAndReturn();
946
+ try {
947
+ const sessionData = await getSessionDataFromCookie(request, NEON_AUTH_SESSION_DATA_COOKIE_NAME, cookieConfig.secret);
948
+ if (sessionData && sessionData.session) return Response.json(sessionData);
949
+ return await mintAndReturn();
950
+ } catch (error) {
951
+ const errorMessage = error instanceof Error ? error.message : String(error);
952
+ const errorName = error instanceof Error ? error.name : "Unknown";
953
+ if (errorName === "JWTExpired") console.debug("[trySessionCache] Session cookie expired, minting new one:", {
954
+ error: errorMessage,
955
+ url: request.url
956
+ });
957
+ else if (errorName === "JWTInvalid" || errorName === "JWTClaimValidationFailed") console.warn("[trySessionCache] Invalid session cookie, minting new one:", {
958
+ error: errorMessage,
959
+ url: request.url
960
+ });
961
+ else console.error("[trySessionCache] Unexpected validation error:", {
962
+ error: errorMessage,
963
+ url: request.url
964
+ });
965
+ return await mintAndReturn();
966
+ }
967
+ }
968
+ /**
969
+ * Extract session_token cookie value from cookie header
970
+ * @internal
971
+ */
972
+ function extractSessionTokenCookie(cookieHeader) {
973
+ const sessionToken = parseCookies(cookieHeader).get(NEON_AUTH_SESSION_COOKIE_NAME);
974
+ if (!sessionToken) return null;
975
+ return `${NEON_AUTH_SESSION_COOKIE_NAME}=${sessionToken}`;
976
+ }
977
+
978
+ //#endregion
979
+ //#region src/server/proxy/handler.ts
980
+ /**
981
+ * Generic authentication proxy handler (framework-agnostic)
982
+ *
983
+ * Handles the complete flow:
984
+ * 1. Check if request is for getSession endpoint
985
+ * 2. Try session cache if applicable (< 1ms fast path)
986
+ * 3. Call upstream Neon Auth API
987
+ * 4. Handle response with cookie minting
988
+ *
989
+ * This is framework-agnostic and can be used by any server framework.
990
+ *
991
+ * @param config - Proxy configuration
992
+ * @returns Standard Web API Response
993
+ */
994
+ async function handleAuthProxyRequest(config) {
995
+ const { request, path, baseUrl, cookieSecret, sessionDataTtl, domain } = config;
996
+ if (path === API_ENDPOINTS.getSession.path && request.method === API_ENDPOINTS.getSession.method) {
997
+ const cachedResponse = await trySessionCache(request, baseUrl, {
998
+ secret: cookieSecret,
999
+ sessionDataTtl,
1000
+ domain
1001
+ });
1002
+ if (cachedResponse) return cachedResponse;
1003
+ }
1004
+ return await handleAuthResponse(await handleAuthRequest(baseUrl, request, path), baseUrl, {
1005
+ secret: cookieSecret,
1006
+ sessionDataTtl,
1007
+ domain
1008
+ });
1009
+ }
1010
+
1011
+ //#endregion
1012
+ //#region src/next/server/handler.ts
1013
+ /**
1014
+ * An API route handler to handle the auth requests from the client and proxy them to the Neon Auth.
1015
+ *
1016
+ * @param config - Required configuration
1017
+ * @param config.baseUrl - Base URL of your Neon Auth instance
1018
+ * @param config.cookies - Cookie configuration
1019
+ * @param config.cookies.secret - Secret for signing session cookies (minimum 32 characters)
1020
+ * @param config.cookies.sessionDataTtl - Optional TTL for session cache in seconds (default: 300)
1021
+ * @returns A Next.js API handler functions that can be used in a Next.js route.
1022
+ * @throws Error if `cookies.secret` is less than 32 characters
1023
+ *
1024
+ * @example
1025
+ * Mount the `authApiHandler` to an API route. Create a route file inside `/api/auth/[...all]/route.ts` directory.
1026
+ * And add the following code:
1027
+ *
1028
+ * ```ts
1029
+ * // app/api/auth/[...all]/route.ts
1030
+ * import { authApiHandler } from '@neondatabase/auth/next';
1031
+ *
1032
+ * export const { GET, POST } = authApiHandler({
1033
+ * baseUrl: process.env.NEON_AUTH_BASE_URL!,
1034
+ * cookies: {
1035
+ * secret: process.env.NEON_AUTH_COOKIE_SECRET!,
1036
+ * },
1037
+ * });
1038
+ * ```
1039
+ */
1040
+ function authApiHandler(config) {
1041
+ const { baseUrl, cookies: cookies$1 } = config;
1042
+ validateCookieConfig(cookies$1);
1043
+ const handler = async (request, { params }) => {
1044
+ return handleAuthProxyRequest({
1045
+ request,
1046
+ path: (await params).path.join("/"),
1047
+ baseUrl,
1048
+ cookieSecret: cookies$1.secret,
1049
+ sessionDataTtl: cookies$1.sessionDataTtl,
1050
+ domain: cookies$1.domain
1051
+ });
1052
+ };
1053
+ return {
1054
+ GET: handler,
1055
+ POST: handler,
1056
+ PUT: handler,
1057
+ DELETE: handler,
1058
+ PATCH: handler
1059
+ };
1060
+ }
1061
+
1062
+ //#endregion
1063
+ //#region src/server/middleware/oauth.ts
1064
+ /**
1065
+ * Checks if the current request needs OAuth session verification
1066
+ * This happens when returning from OAuth provider with a verifier token
1067
+ *
1068
+ * @param request - Standard Web API Request object
1069
+ * @returns true if session verification is needed
1070
+ */
1071
+ function needsSessionVerification(request) {
1072
+ const hasVerifier = new URL(request.url).searchParams.has(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
1073
+ const cookieHeader = request.headers.get("cookie");
1074
+ if (!cookieHeader) return false;
1075
+ const hasChallenge = parseCookies(cookieHeader).has(NEON_AUTH_SESSION_CHALLENGE_COOKIE_NAME);
1076
+ return hasVerifier && hasChallenge;
1077
+ }
1078
+ /**
1079
+ * Exchanges OAuth verifier token for session cookie
1080
+ * This completes the OAuth flow by verifying the session challenge
1081
+ *
1082
+ * @param request - Standard Web API Request object
1083
+ * @param baseUrl - Base URL of Neon Auth server
1084
+ * @param cookieSecret - Secret for signing session cookies
1085
+ * @param sessionDataTtl - Optional TTL for session data cache
1086
+ * @param domain - Optional cookie domain
1087
+ * @returns Exchange result with redirect URL and cookies, or null if exchange not needed/failed
1088
+ */
1089
+ async function exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain) {
1090
+ const url = new URL(request.url);
1091
+ const verifier = url.searchParams.get(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
1092
+ const cookieHeader = request.headers.get("cookie");
1093
+ if (!cookieHeader) return null;
1094
+ const challenge = parseCookies(cookieHeader).get(NEON_AUTH_SESSION_CHALLENGE_COOKIE_NAME);
1095
+ if (!verifier || !challenge) return null;
1096
+ const response = await handleAuthResponse(await handleAuthRequest(baseUrl, new Request(request.url, {
1097
+ method: "GET",
1098
+ headers: request.headers
1099
+ }), "get-session"), baseUrl, {
1100
+ secret: cookieSecret,
1101
+ sessionDataTtl,
1102
+ domain
1103
+ });
1104
+ if (response.ok) {
1105
+ const setCookieHeaders = response.headers.getSetCookie();
1106
+ url.searchParams.delete(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
1107
+ return {
1108
+ redirectUrl: url,
1109
+ cookies: setCookieHeaders,
1110
+ success: true
1111
+ };
1112
+ }
1113
+ return null;
1114
+ }
1115
+
1116
+ //#endregion
1117
+ //#region src/server/middleware/route-protection.ts
1118
+ /**
1119
+ * Checks if a given pathname should be protected (require authentication)
1120
+ *
1121
+ * @param pathname - URL pathname to check
1122
+ * @param skipRoutes - Array of route prefixes to skip protection
1123
+ * @returns true if route should be protected, false if it should be skipped
1124
+ */
1125
+ function shouldProtectRoute(pathname, skipRoutes) {
1126
+ return !skipRoutes.some((route) => pathname.startsWith(route));
1127
+ }
1128
+ /**
1129
+ * Checks if the current request requires a valid session
1130
+ * Returns result indicating if request should proceed, redirect, or continue
1131
+ *
1132
+ * @param pathname - URL pathname being accessed
1133
+ * @param skipRoutes - Routes that don't require authentication
1134
+ * @param loginUrl - URL to redirect to for login (if applicable)
1135
+ * @param session - Current session data (null if not authenticated)
1136
+ * @returns Session check result
1137
+ */
1138
+ function checkSessionRequired(pathname, skipRoutes, loginUrl, session) {
1139
+ if (pathname.startsWith(loginUrl)) return {
1140
+ allowed: true,
1141
+ requiresRedirect: false
1142
+ };
1143
+ if (!shouldProtectRoute(pathname, skipRoutes)) return {
1144
+ allowed: true,
1145
+ requiresRedirect: false
1146
+ };
1147
+ if (!session || session.session === null) return {
1148
+ allowed: false,
1149
+ requiresRedirect: true
1150
+ };
1151
+ return {
1152
+ allowed: true,
1153
+ session,
1154
+ requiresRedirect: false
1155
+ };
1156
+ }
1157
+
1158
+ //#endregion
1159
+ //#region src/server/middleware/processor.ts
1160
+ /**
1161
+ * Generic authentication middleware processor (framework-agnostic)
1162
+ *
1163
+ * Handles the complete middleware flow:
1164
+ * 1. Check if login URL (skip auth)
1165
+ * 2. Check OAuth verification (exchange token)
1166
+ * 3. Get session (delegates to handleAuthProxyRequest for cookie cache + upstream fallback)
1167
+ * 4. Check if route requires protection
1168
+ * 5. Return decision object
1169
+ *
1170
+ * This is framework-agnostic - it returns a decision, NOT a framework-specific response.
1171
+ * The calling framework converts the decision to its response type (NextResponse, etc.)
1172
+ *
1173
+ * @param config - Middleware configuration
1174
+ * @returns Decision object indicating what action to take
1175
+ */
1176
+ async function processAuthMiddleware(config) {
1177
+ const { request, pathname, skipRoutes, loginUrl, baseUrl, cookieSecret, sessionDataTtl, domain } = config;
1178
+ if (pathname.startsWith(loginUrl)) return { action: "allow" };
1179
+ if (needsSessionVerification(request)) {
1180
+ const exchangeResult = await exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain);
1181
+ if (exchangeResult !== null) return {
1182
+ action: "redirect_oauth",
1183
+ redirectUrl: exchangeResult.redirectUrl,
1184
+ cookies: exchangeResult.cookies
1185
+ };
1186
+ }
1187
+ const cookieHeader = request.headers.get("cookie") || "";
1188
+ const hasSessionToken = cookieHeader.includes(NEON_AUTH_SESSION_COOKIE_NAME);
1189
+ const hasStaleSessionData = parseCookies(cookieHeader).has(NEON_AUTH_SESSION_DATA_COOKIE_NAME) && !hasSessionToken;
1190
+ let sessionData = {
1191
+ session: null,
1192
+ user: null
1193
+ };
1194
+ let sessionCookies = [];
1195
+ if (hasSessionToken) {
1196
+ const sessionResponse = await handleAuthProxyRequest({
1197
+ request,
1198
+ path: "get-session",
1199
+ baseUrl,
1200
+ cookieSecret,
1201
+ sessionDataTtl,
1202
+ domain
1203
+ });
1204
+ if (sessionResponse.ok) {
1205
+ const data = await sessionResponse.json().catch(() => null);
1206
+ if (data) sessionData = data;
1207
+ }
1208
+ sessionCookies = sessionResponse.headers.getSetCookie();
1209
+ }
1210
+ if (checkSessionRequired(pathname, skipRoutes, loginUrl, sessionData).allowed) return {
1211
+ action: "allow",
1212
+ headers: { [NEON_AUTH_HEADER_MIDDLEWARE_NAME]: "true" },
1213
+ cookies: sessionCookies
1214
+ };
1215
+ const cookies$1 = [];
1216
+ if (hasStaleSessionData) cookies$1.push(serializeSetCookie({
1217
+ name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
1218
+ value: "",
1219
+ path: "/",
1220
+ domain,
1221
+ httpOnly: true,
1222
+ secure: true,
1223
+ sameSite: "lax",
1224
+ maxAge: 0
1225
+ }));
1226
+ return {
1227
+ action: "redirect_login",
1228
+ redirectUrl: new URL(loginUrl, request.url),
1229
+ cookies: cookies$1.length > 0 ? cookies$1 : void 0
1230
+ };
1231
+ }
1232
+
1233
+ //#endregion
1234
+ //#region src/next/server/middleware.ts
1235
+ const SKIP_ROUTES = [
1236
+ "/api/auth",
1237
+ "/auth/callback",
1238
+ "/auth/sign-in",
1239
+ "/auth/sign-up",
1240
+ "/auth/magic-link",
1241
+ "/auth/email-otp",
1242
+ "/auth/forgot-password"
1243
+ ];
1244
+ /**
1245
+ * A Next.js middleware to protect routes from unauthenticated requests and refresh the session if required.
1246
+ *
1247
+ * @param config - Required middleware configuration
1248
+ * @param config.baseUrl - Base URL of your Neon Auth instance
1249
+ * @param config.cookies - Cookie configuration
1250
+ * @param config.cookies.secret - Secret for signing session cookies (minimum 32 characters)
1251
+ * @param config.cookies.sessionDataTtl - Optional TTL for session cache in seconds (default: 300)
1252
+ * @param config.loginUrl - The URL to redirect to when the user is not authenticated (default: '/auth/sign-in')
1253
+ * @returns A middleware function that can be used in the Next.js app.
1254
+ * @throws Error if `cookies.secret` is less than 32 characters
1255
+ *
1256
+ * @example
1257
+ * ```ts
1258
+ * import { neonAuthMiddleware } from "@neondatabase/auth/next"
1259
+ *
1260
+ * export default neonAuthMiddleware({
1261
+ * baseUrl: process.env.NEON_AUTH_BASE_URL!,
1262
+ * cookies: {
1263
+ * secret: process.env.NEON_AUTH_COOKIE_SECRET!,
1264
+ * },
1265
+ * loginUrl: '/auth/sign-in',
1266
+ * });
1267
+ * ```
1268
+ */
1269
+ function neonAuthMiddleware(config) {
1270
+ const { baseUrl, cookies: cookies$1, loginUrl = "/auth/sign-in" } = config;
1271
+ validateCookieConfig(cookies$1);
1272
+ return async (request) => {
1273
+ const pathname = request.nextUrl.pathname;
1274
+ const result = await processAuthMiddleware({
1275
+ request,
1276
+ pathname,
1277
+ skipRoutes: SKIP_ROUTES,
1278
+ loginUrl,
1279
+ baseUrl,
1280
+ cookieSecret: cookies$1.secret,
1281
+ sessionDataTtl: cookies$1.sessionDataTtl,
1282
+ domain: cookies$1.domain
1283
+ });
1284
+ switch (result.action) {
1285
+ case "allow": {
1286
+ const headers$1 = new Headers(request.headers);
1287
+ if (result.headers) for (const [key, value] of Object.entries(result.headers)) headers$1.set(key, value);
1288
+ const response = NextResponse.next({ request: { headers: headers$1 } });
1289
+ if (result.cookies) for (const cookie of result.cookies) response.headers.append("Set-Cookie", cookie);
1290
+ return response;
1291
+ }
1292
+ case "redirect_oauth": {
1293
+ const oauthHeaders = new Headers();
1294
+ for (const cookie of result.cookies) oauthHeaders.append("Set-Cookie", cookie);
1295
+ return NextResponse.redirect(result.redirectUrl, { headers: oauthHeaders });
1296
+ }
1297
+ case "redirect_login":
1298
+ if (result.cookies && result.cookies.length > 0) {
1299
+ const loginHeaders = new Headers();
1300
+ for (const cookie of result.cookies) loginHeaders.append("Set-Cookie", cookie);
1301
+ return NextResponse.redirect(result.redirectUrl, { headers: loginHeaders });
1302
+ }
1303
+ return NextResponse.redirect(result.redirectUrl);
1304
+ }
1305
+ };
1306
+ }
1307
+
1308
+ //#endregion
1309
+ //#region src/next/server/index.ts
1310
+ /**
1311
+ * Unified entry point for Neon Auth in Next.js
1312
+ *
1313
+ * This is the recommended way to use Neon Auth in Next.js. It provides a single
1314
+ * entry point that combines all server-side functionality.
1315
+ *
1316
+ * **Features:**
1317
+ * - All Better Auth server methods (signIn, signUp, getSession, etc.)
1318
+ * - `.handler()` - API route handler for `/api/auth/[...path]`
1319
+ * - `.middleware(config?)` - Middleware for route protection
1320
+ *
1321
+ * **Where to use:**
1322
+ * - React Server Components
1323
+ * - Server Actions
1324
+ * - Route Handlers
1325
+ * - Middleware
1326
+ *
1327
+ * @param config - Required configuration
1328
+ * @param config.baseUrl - Base URL of your Neon Auth instance
1329
+ * @param config.cookies - Cookie configuration
1330
+ * @param config.cookies.secret - Secret for signing session cookies (minimum 32 characters)
1331
+ * @param config.cookies.sessionDataTtl - Optional TTL for session cache in seconds (default: 300)
1332
+ * @param config.cookies.domain - Optional cookie domain (default: current domain)
1333
+ * @returns Unified auth instance with server methods, handler, and middleware
1334
+ * @throws Error if `cookies.secret` is less than 32 characters
1335
+ *
1336
+ * @example
1337
+ * ```typescript
1338
+ * // lib/auth.ts - Create a singleton instance
1339
+ * import { createNeonAuth } from '@neondatabase/auth/next/server';
1340
+ *
1341
+ * export const auth = createNeonAuth({
1342
+ * baseUrl: process.env.NEON_AUTH_BASE_URL!,
1343
+ * cookies: {
1344
+ * secret: process.env.NEON_AUTH_COOKIE_SECRET!,
1345
+ * sessionDataTtl: 300, // 5 minutes (default)
1346
+ * },
1347
+ * });
1348
+ * ```
1349
+ *
1350
+ * @example
1351
+ * ```typescript
1352
+ * // app/api/auth/[...path]/route.ts - API handler
1353
+ * import { auth } from '@/lib/auth';
1354
+ *
1355
+ * export const { GET, POST } = auth.handler();
1356
+ * ```
1357
+ *
1358
+ * @example
1359
+ * ```typescript
1360
+ * // middleware.ts - Route protection
1361
+ * import { auth } from '@/lib/auth';
1362
+ *
1363
+ * export default auth.middleware({ loginUrl: '/auth/sign-in' });
1364
+ *
1365
+ * export const config = {
1366
+ * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
1367
+ * };
1368
+ * ```
1369
+ *
1370
+ * @example
1371
+ * ```typescript
1372
+ * // app/page.tsx - Server Component
1373
+ * import { auth } from '@/lib/auth';
1374
+ *
1375
+ * // Server components using `auth` methods must be rendered dynamically
1376
+ * export const dynamic = 'force-dynamic'
1377
+ *
1378
+ * export default async function Page() {
1379
+ * const { data: session } = await auth.getSession();
1380
+ * if (!session?.user) return <div>Not logged in</div>;
1381
+ * return <div>Hello {session.user.name}</div>;
1382
+ * }
1383
+ * ```
1384
+ *
1385
+ * @example
1386
+ * ```typescript
1387
+ * // app/actions.ts - Server Action
1388
+ * 'use server';
1389
+ * import { auth } from '@/lib/auth';
1390
+ * import { redirect } from 'next/navigation';
1391
+ *
1392
+ * export async function signIn(formData: FormData) {
1393
+ * const { error } = await auth.signIn.email({
1394
+ * email: formData.get('email') as string,
1395
+ * password: formData.get('password') as string,
1396
+ * });
1397
+ * if (error) return { error: error.message };
1398
+ * redirect('/dashboard');
1399
+ * }
1400
+ * ```
1401
+ */
1402
+ function createNeonAuth(config) {
1403
+ const { baseUrl, cookies: cookies$1 } = config;
1404
+ validateCookieConfig(cookies$1);
1405
+ const server = createAuthServerInternal({
1406
+ baseUrl,
1407
+ context: createNextRequestContext,
1408
+ cookieSecret: cookies$1.secret,
1409
+ sessionDataTtl: cookies$1.sessionDataTtl,
1410
+ domain: cookies$1.domain
1411
+ });
1412
+ /**
1413
+ * Creates API route handlers for Next.js
1414
+ *
1415
+ * Mount this in your API routes to handle auth requests:
1416
+ * - `/api/auth/[...path]/route.ts`
1417
+ *
1418
+ * @returns Object with GET, POST, PUT, DELETE, PATCH handlers
1419
+ *
1420
+ * @example
1421
+ * ```typescript
1422
+ * // app/api/auth/[...path]/route.ts
1423
+ * import { auth } from '@/lib/auth';
1424
+ *
1425
+ * export const { GET, POST } = auth.handler();
1426
+ * ```
1427
+ */
1428
+ server.handler = () => authApiHandler(config);
1429
+ /**
1430
+ * Creates middleware for route protection
1431
+ *
1432
+ * Protects routes from unauthenticated access and handles:
1433
+ * - Session validation and refresh
1434
+ * - OAuth callback processing
1435
+ * - Login redirects
1436
+ *
1437
+ * @param middlewareConfig - Optional middleware configuration
1438
+ * @param middlewareConfig.loginUrl - URL to redirect to when not authenticated (default: '/auth/sign-in')
1439
+ * @returns Middleware function for Next.js
1440
+ *
1441
+ * @example
1442
+ * ```typescript
1443
+ * // middleware.ts
1444
+ * import { auth } from '@/lib/auth';
1445
+ *
1446
+ * export default auth.middleware({ loginUrl: '/auth/sign-in' });
1447
+ *
1448
+ * export const config = {
1449
+ * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
1450
+ * };
1451
+ * ```
1452
+ */
1453
+ server.middleware = (middlewareConfig) => neonAuthMiddleware({
1454
+ ...config,
1455
+ ...middlewareConfig
1456
+ });
1457
+ return server;
1458
+ }
1459
+
1460
+ //#endregion
1461
+ export { createNeonAuth };