vinextauth 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -84,14 +84,15 @@ function resolveConfig(config) {
84
84
  "[VinextAuth] No secret provided. Set NEXTAUTH_SECRET or VINEXTAUTH_SECRET env var, or pass `secret` to VinextAuth()."
85
85
  );
86
86
  }
87
- const baseUrl = (typeof process !== "undefined" ? process.env.NEXTAUTH_URL ?? process.env.VINEXTAUTH_URL ?? process.env.VERCEL_URL : void 0) ?? "http://localhost:3000";
88
- const normalizedBaseUrl = baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}`;
89
- const useSecureCookies = config.useSecureCookies ?? normalizedBaseUrl.startsWith("https://");
87
+ const baseUrl = config.baseUrl ?? (typeof process !== "undefined" ? process.env.NEXTAUTH_URL ?? process.env.VINEXTAUTH_URL ?? process.env.VERCEL_URL : void 0) ?? "http://localhost:3000";
88
+ const resolvedBaseUrl = typeof baseUrl === "string" ? baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}` : baseUrl;
89
+ const staticBaseUrl = typeof resolvedBaseUrl === "string" ? resolvedBaseUrl : "http://localhost:3000";
90
+ const useSecureCookies = config.useSecureCookies ?? staticBaseUrl.startsWith("https://");
90
91
  const sessionMaxAge = config.session?.maxAge ?? DEFAULT_MAX_AGE;
91
92
  return {
92
93
  providers: config.providers,
93
94
  secret,
94
- baseUrl: normalizedBaseUrl,
95
+ baseUrl: resolvedBaseUrl,
95
96
  basePath: "/api/auth",
96
97
  callbacks: config.callbacks ?? {},
97
98
  pages: {
@@ -119,9 +120,21 @@ function resolveConfig(config) {
119
120
  ...buildCookieNames(useSecureCookies),
120
121
  ...config.cookies
121
122
  },
122
- adapter: config.adapter
123
+ adapter: config.adapter,
124
+ accountLinking: {
125
+ enabled: config.accountLinking?.enabled ?? false,
126
+ requireVerification: config.accountLinking?.requireVerification ?? true
127
+ },
128
+ credentials: config.credentials ?? {}
123
129
  };
124
130
  }
131
+ async function resolveBaseUrl(config, request) {
132
+ if (typeof config.baseUrl === "function") {
133
+ const resolved = await config.baseUrl(request);
134
+ return resolved.startsWith("http") ? resolved : `https://${resolved}`;
135
+ }
136
+ return config.baseUrl;
137
+ }
125
138
 
126
139
  // src/cookies/index.ts
127
140
  function getSessionToken(request, config) {
@@ -165,10 +178,6 @@ function applyStateCookie(headers, state, config) {
165
178
  const { name, options } = config.cookies.state;
166
179
  headers.append("Set-Cookie", serializeCookie(name, state, options));
167
180
  }
168
- function clearSessionCookie(headers, config) {
169
- const { name, options } = config.cookies.sessionToken;
170
- headers.append("Set-Cookie", deleteCookieString(name, options));
171
- }
172
181
  function clearStateCookie(headers, config) {
173
182
  const { name, options } = config.cookies.state;
174
183
  headers.append("Set-Cookie", deleteCookieString(name, options));
@@ -303,6 +312,26 @@ async function verify(token, secret) {
303
312
  }
304
313
  }
305
314
 
315
+ // src/core/refresh-lock.ts
316
+ var locks = /* @__PURE__ */ new Map();
317
+ async function withRefreshLock(tokenId, fn) {
318
+ const existing = locks.get(tokenId);
319
+ if (existing) {
320
+ await existing;
321
+ }
322
+ let resolve;
323
+ const lock = new Promise((r) => {
324
+ resolve = r;
325
+ });
326
+ locks.set(tokenId, lock);
327
+ try {
328
+ return await fn();
329
+ } finally {
330
+ resolve();
331
+ locks.delete(tokenId);
332
+ }
333
+ }
334
+
306
335
  // src/core/session.ts
307
336
  async function encodeSession(payload, config) {
308
337
  if (config.jwt.encode) {
@@ -327,7 +356,7 @@ async function buildSession(jwt, config) {
327
356
  }
328
357
  return baseSession;
329
358
  }
330
- async function buildJWT(user, account, profile, config) {
359
+ async function buildJWT(user, account, profile, config, trigger = "signIn") {
331
360
  const now = Math.floor(Date.now() / 1e3);
332
361
  let token = {
333
362
  sub: user.id,
@@ -344,11 +373,30 @@ async function buildJWT(user, account, profile, config) {
344
373
  user,
345
374
  account,
346
375
  profile,
347
- trigger: "signIn"
376
+ trigger
348
377
  });
349
378
  }
350
379
  return token;
351
380
  }
381
+ async function refreshTokenIfNeeded(jwt, config) {
382
+ if (!config.callbacks.refreshToken) return jwt;
383
+ const accessTokenExpires = jwt.accessTokenExpires;
384
+ const now = Date.now();
385
+ if (!accessTokenExpires || now < accessTokenExpires - 6e4) {
386
+ return jwt;
387
+ }
388
+ const tokenId = jwt.jti ?? jwt.sub ?? "unknown";
389
+ return withRefreshLock(tokenId, async () => {
390
+ const result = await config.callbacks.refreshToken({ token: jwt });
391
+ if (result.error) {
392
+ if (config.debug) {
393
+ console.warn("[VinextAuth] Token refresh failed:", result.error);
394
+ }
395
+ return { ...result.token, refreshError: result.error };
396
+ }
397
+ return result.token;
398
+ });
399
+ }
352
400
  function generateId() {
353
401
  const bytes = new Uint8Array(16);
354
402
  crypto.getRandomValues(bytes);
@@ -357,16 +405,18 @@ function generateId() {
357
405
 
358
406
  // src/handlers/callback.ts
359
407
  async function handleCallback(request, providerId, config) {
360
- const provider = config.providers.find((p) => p.id === providerId);
408
+ const provider = config.providers.find(
409
+ (p) => p.id === providerId && p.type === "oauth"
410
+ );
361
411
  if (!provider) return new Response("Unknown provider", { status: 404 });
412
+ const baseUrl = await resolveBaseUrl(config, request);
362
413
  const url = new URL(request.url);
363
414
  const code = url.searchParams.get("code");
364
415
  const stateParam = url.searchParams.get("state");
365
416
  const error = url.searchParams.get("error");
417
+ const errorBase = `${baseUrl}${config.pages.error}`;
366
418
  if (error) {
367
- const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
368
- errorUrl.searchParams.set("error", error);
369
- return Response.redirect(errorUrl.toString(), 302);
419
+ return Response.redirect(`${errorBase}?error=${encodeURIComponent(error)}`, 302);
370
420
  }
371
421
  if (!code) {
372
422
  return new Response("Missing code", { status: 400 });
@@ -374,12 +424,10 @@ async function handleCallback(request, providerId, config) {
374
424
  const storedState = getStateCookie(request, config);
375
425
  if (provider.checks?.includes("state")) {
376
426
  if (!storedState || storedState !== stateParam) {
377
- const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
378
- errorUrl.searchParams.set("error", "OAuthStateError");
379
- return Response.redirect(errorUrl.toString(), 302);
427
+ return Response.redirect(`${errorBase}?error=OAuthStateError`, 302);
380
428
  }
381
429
  }
382
- const redirectUri = `${config.baseUrl}${config.basePath}/callback/${providerId}`;
430
+ const redirectUri = `${baseUrl}${config.basePath}/callback/${providerId}`;
383
431
  let tokenData;
384
432
  try {
385
433
  const tokenResponse = await fetch(provider.token.url, {
@@ -402,27 +450,23 @@ async function handleCallback(request, providerId, config) {
402
450
  tokenData = await tokenResponse.json();
403
451
  } catch (err) {
404
452
  if (config.debug) console.error("[VinextAuth] Token exchange error:", err);
405
- const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
406
- errorUrl.searchParams.set("error", "OAuthCallbackError");
407
- return Response.redirect(errorUrl.toString(), 302);
453
+ return Response.redirect(`${errorBase}?error=OAuthCallbackError`, 302);
408
454
  }
409
455
  let rawProfile;
410
456
  try {
411
457
  const userInfoResponse = await fetch(provider.userinfo.url, {
412
458
  headers: {
413
459
  Authorization: `Bearer ${tokenData.access_token}`,
414
- Accept: "application/json"
460
+ Accept: "application/json",
461
+ // GitHub needs this header
462
+ "User-Agent": "VinextAuth/0.2"
415
463
  }
416
464
  });
417
- if (!userInfoResponse.ok) {
418
- throw new Error(`UserInfo fetch failed: ${userInfoResponse.status}`);
419
- }
465
+ if (!userInfoResponse.ok) throw new Error(`UserInfo failed: ${userInfoResponse.status}`);
420
466
  rawProfile = await userInfoResponse.json();
421
467
  } catch (err) {
422
468
  if (config.debug) console.error("[VinextAuth] UserInfo error:", err);
423
- const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
424
- errorUrl.searchParams.set("error", "OAuthCallbackError");
425
- return Response.redirect(errorUrl.toString(), 302);
469
+ return Response.redirect(`${errorBase}?error=OAuthCallbackError`, 302);
426
470
  }
427
471
  const user = provider.profile(rawProfile);
428
472
  const account = {
@@ -436,12 +480,30 @@ async function handleCallback(request, providerId, config) {
436
480
  scope: tokenData.scope,
437
481
  id_token: tokenData.id_token
438
482
  };
483
+ if (config.adapter?.getAccountByProvider) {
484
+ const existingAccount = await config.adapter.getAccountByProvider(providerId, user.id);
485
+ if (!existingAccount && user.email && config.adapter.getUserByEmail) {
486
+ const existingUser = await config.adapter.getUserByEmail(user.email);
487
+ if (existingUser) {
488
+ if (!config.accountLinking.enabled) {
489
+ return Response.redirect(
490
+ `${errorBase}?error=OAuthAccountNotLinked&provider=${providerId}&hint=enableAccountLinking`,
491
+ 302
492
+ );
493
+ }
494
+ if (config.adapter.linkAccount) {
495
+ await config.adapter.linkAccount(existingUser.id, providerId, user.id);
496
+ }
497
+ user.id = existingUser.id;
498
+ user.name = user.name ?? existingUser.name;
499
+ user.image = user.image ?? existingUser.image;
500
+ }
501
+ }
502
+ }
439
503
  if (config.callbacks.signIn) {
440
504
  const result = await config.callbacks.signIn({ user, account, profile: rawProfile });
441
505
  if (result === false) {
442
- const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
443
- errorUrl.searchParams.set("error", "AccessDenied");
444
- return Response.redirect(errorUrl.toString(), 302);
506
+ return Response.redirect(`${errorBase}?error=AccessDenied`, 302);
445
507
  }
446
508
  if (typeof result === "string") {
447
509
  return Response.redirect(result, 302);
@@ -449,22 +511,24 @@ async function handleCallback(request, providerId, config) {
449
511
  }
450
512
  const jwtPayload = await buildJWT(user, account, rawProfile, config);
451
513
  const sessionToken = await encodeSession(jwtPayload, config);
452
- const session = await buildSession(jwtPayload, config);
453
514
  const callbackUrl = getCallbackUrl(request, config) ?? config.pages.newUser ?? "/";
454
- const redirectUrl = isAbsoluteUrl(callbackUrl) ? callbackUrl : `${config.baseUrl}${callbackUrl}`;
515
+ const redirectUrl = isAbsoluteUrl(callbackUrl) ? callbackUrl : `${baseUrl}${callbackUrl}`;
455
516
  const headers = new Headers();
456
517
  applySessionCookie(headers, sessionToken, config);
457
518
  clearStateCookie(headers, config);
458
519
  clearCallbackUrlCookie(headers, config);
459
520
  headers.set("Location", redirectUrl);
460
521
  if (config.debug) {
461
- console.log("[VinextAuth] Signed in:", session.user.email);
522
+ console.log("[VinextAuth] Signed in:", session(jwtPayload));
462
523
  }
463
524
  return new Response(null, { status: 302, headers });
464
525
  }
465
526
  function isAbsoluteUrl(url) {
466
527
  return url.startsWith("http://") || url.startsWith("https://");
467
528
  }
529
+ function session(jwt, _config) {
530
+ return `${jwt.email ?? jwt.sub ?? "unknown"}`;
531
+ }
468
532
 
469
533
  // src/core/csrf.ts
470
534
  function bytesToHex(bytes) {
@@ -500,7 +564,13 @@ async function verifyCsrfToken(submittedToken, cookieValue, secret) {
500
564
 
501
565
  // src/handlers/signout.ts
502
566
  async function handleSignOut(request, config) {
503
- let callbackUrl = config.baseUrl;
567
+ let callbackUrl;
568
+ if (typeof config.baseUrl === "function") {
569
+ const resolved = await config.baseUrl(request);
570
+ callbackUrl = resolved.startsWith("http") ? resolved : `https://${resolved}`;
571
+ } else {
572
+ callbackUrl = config.baseUrl;
573
+ }
504
574
  if (request.method === "POST") {
505
575
  let body = {};
506
576
  try {
@@ -515,18 +585,26 @@ async function handleSignOut(request, config) {
515
585
  }
516
586
  const csrfCookie = getCsrfCookie(request, config);
517
587
  const submittedToken = body.csrfToken ?? "";
518
- if (csrfCookie) {
588
+ if (csrfCookie && submittedToken) {
519
589
  const valid = await verifyCsrfToken(submittedToken, csrfCookie, config.secret);
520
590
  if (!valid && config.debug) {
521
591
  console.warn("[VinextAuth] CSRF verification failed on signout");
522
592
  }
523
593
  }
524
- callbackUrl = body.callbackUrl ?? config.baseUrl;
594
+ if (body.callbackUrl) {
595
+ callbackUrl = body.callbackUrl;
596
+ }
525
597
  }
598
+ const redirectUrl = isAbsoluteUrl2(callbackUrl) ? callbackUrl : `${typeof config.baseUrl === "string" ? config.baseUrl : ""}${callbackUrl}`;
526
599
  const headers = new Headers();
527
- clearSessionCookie(headers, config);
528
- clearCallbackUrlCookie(headers, config);
529
- const redirectUrl = isAbsoluteUrl2(callbackUrl) ? callbackUrl : `${config.baseUrl}${callbackUrl}`;
600
+ const { sessionToken, callbackUrl: cbCookie, csrfToken, state, nonce } = config.cookies;
601
+ for (const cookie of [sessionToken, cbCookie, csrfToken, state, nonce]) {
602
+ headers.append("Set-Cookie", deleteCookieString(cookie.name, cookie.options));
603
+ const unprefixed = cookie.name.replace("__Secure-", "");
604
+ if (unprefixed !== cookie.name) {
605
+ headers.append("Set-Cookie", deleteCookieString(unprefixed, { ...cookie.options, secure: false }));
606
+ }
607
+ }
530
608
  headers.set("Location", redirectUrl);
531
609
  return new Response(null, { status: 302, headers });
532
610
  }
@@ -538,18 +616,26 @@ function isAbsoluteUrl2(url) {
538
616
  async function handleSessionRoute(request, config) {
539
617
  const token = getSessionToken(request, config);
540
618
  if (!token) {
541
- return Response.json({});
619
+ return Response.json({}, { headers: noCacheHeaders() });
542
620
  }
543
- const jwt = await decodeSession(token, config);
621
+ let jwt = await decodeSession(token, config);
544
622
  if (!jwt) {
545
- return Response.json({});
623
+ return Response.json({}, { headers: noCacheHeaders() });
546
624
  }
547
- const session = await buildSession(jwt, config);
548
- return Response.json(session, {
549
- headers: {
550
- "Cache-Control": "no-store, max-age=0"
551
- }
552
- });
625
+ const refreshed = await refreshTokenIfNeeded(jwt, config);
626
+ const headers = new Headers(noCacheHeaders());
627
+ if (refreshed !== jwt) {
628
+ const newToken = await encodeSession(refreshed, config);
629
+ applySessionCookie(headers, newToken, config);
630
+ jwt = refreshed;
631
+ }
632
+ const session2 = await buildSession(jwt, config);
633
+ const refreshError = jwt.refreshError;
634
+ const responseBody = refreshError ? { ...session2, refreshError } : session2;
635
+ return Response.json(responseBody, { headers });
636
+ }
637
+ function noCacheHeaders() {
638
+ return { "Cache-Control": "no-store, max-age=0" };
553
639
  }
554
640
 
555
641
  // src/handlers/csrf-route.ts
@@ -564,6 +650,140 @@ async function handleCsrfRoute(request, config) {
564
650
  applyCsrfCookie(headers, cookieValue, config);
565
651
  return Response.json({ csrfToken: token }, { headers });
566
652
  }
653
+
654
+ // src/core/rate-limiter.ts
655
+ var InMemoryRateLimiter = class {
656
+ store = /* @__PURE__ */ new Map();
657
+ maxAttempts;
658
+ windowMs;
659
+ constructor(maxAttempts = 5, windowMs = 15 * 60 * 1e3) {
660
+ this.maxAttempts = maxAttempts;
661
+ this.windowMs = windowMs;
662
+ if (typeof setInterval !== "undefined") {
663
+ setInterval(() => this.cleanup(), 5 * 60 * 1e3);
664
+ }
665
+ }
666
+ async check(key) {
667
+ const now = Date.now();
668
+ const attempt = this.store.get(key);
669
+ if (!attempt || now > attempt.resetAt) {
670
+ this.store.set(key, { count: 1, resetAt: now + this.windowMs });
671
+ return { allowed: true };
672
+ }
673
+ if (attempt.count >= this.maxAttempts) {
674
+ const retryAfter = Math.ceil((attempt.resetAt - now) / 1e3);
675
+ return { allowed: false, retryAfter };
676
+ }
677
+ attempt.count++;
678
+ return { allowed: true };
679
+ }
680
+ async reset(key) {
681
+ this.store.delete(key);
682
+ }
683
+ cleanup() {
684
+ const now = Date.now();
685
+ for (const [key, attempt] of this.store) {
686
+ if (now > attempt.resetAt) this.store.delete(key);
687
+ }
688
+ }
689
+ };
690
+ var defaultLimiter = null;
691
+ function getDefaultRateLimiter(maxAttempts = 5, windowMs = 15 * 60 * 1e3) {
692
+ if (!defaultLimiter) {
693
+ defaultLimiter = new InMemoryRateLimiter(maxAttempts, windowMs);
694
+ }
695
+ return defaultLimiter;
696
+ }
697
+ function getClientIp(request) {
698
+ return request.headers.get("cf-connecting-ip") ?? request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
699
+ }
700
+
701
+ // src/handlers/credentials.ts
702
+ async function handleCredentials(request, provider, config) {
703
+ if (request.method !== "POST") {
704
+ return new Response("Method Not Allowed", { status: 405 });
705
+ }
706
+ const rateLimitConfig = config.credentials.rateLimit;
707
+ const limiter = rateLimitConfig?.store ?? getDefaultRateLimiter(
708
+ rateLimitConfig?.maxAttempts ?? 5,
709
+ rateLimitConfig?.windowMs ?? 15 * 60 * 1e3
710
+ );
711
+ const ip = getClientIp(request);
712
+ const rateLimitKey = `credentials:${provider.id}:${ip}`;
713
+ const { allowed, retryAfter } = await limiter.check(rateLimitKey);
714
+ if (!allowed) {
715
+ const errorUrl = new URL(`${await resolveBase(config, request)}${config.pages.error}`);
716
+ errorUrl.searchParams.set("error", "RateLimitExceeded");
717
+ if (retryAfter) errorUrl.searchParams.set("retryAfter", String(retryAfter));
718
+ return Response.redirect(errorUrl.toString(), 302);
719
+ }
720
+ let body = {};
721
+ try {
722
+ const contentType = request.headers.get("content-type") ?? "";
723
+ if (contentType.includes("application/json")) {
724
+ body = await request.json();
725
+ } else {
726
+ const text = await request.text();
727
+ body = Object.fromEntries(new URLSearchParams(text));
728
+ }
729
+ } catch {
730
+ return new Response("Bad Request", { status: 400 });
731
+ }
732
+ const callbackUrl = body.callbackUrl ?? config.pages.newUser ?? "/";
733
+ delete body.callbackUrl;
734
+ delete body.csrfToken;
735
+ let user;
736
+ try {
737
+ user = await provider.authorize(body, request);
738
+ } catch (err) {
739
+ if (config.debug) console.error("[VinextAuth] Credentials authorize error:", err);
740
+ const errorUrl = new URL(`${await resolveBase(config, request)}${config.pages.error}`);
741
+ errorUrl.searchParams.set("error", "InvalidCredentials");
742
+ return Response.redirect(errorUrl.toString(), 302);
743
+ }
744
+ if (!user) {
745
+ const errorUrl = new URL(`${await resolveBase(config, request)}${config.pages.error}`);
746
+ errorUrl.searchParams.set("error", "InvalidCredentials");
747
+ return Response.redirect(errorUrl.toString(), 302);
748
+ }
749
+ await limiter.reset(rateLimitKey);
750
+ const account = {
751
+ provider: provider.id,
752
+ type: "credentials",
753
+ providerAccountId: user.id
754
+ };
755
+ if (config.callbacks.signIn) {
756
+ const result = await config.callbacks.signIn({ user, account, profile: void 0 });
757
+ if (result === false) {
758
+ const errorUrl = new URL(`${await resolveBase(config, request)}${config.pages.error}`);
759
+ errorUrl.searchParams.set("error", "AccessDenied");
760
+ return Response.redirect(errorUrl.toString(), 302);
761
+ }
762
+ if (typeof result === "string") {
763
+ return Response.redirect(result, 302);
764
+ }
765
+ }
766
+ const jwtPayload = await buildJWT(user, account, void 0, config);
767
+ const sessionToken = await encodeSession(jwtPayload, config);
768
+ const baseUrl = await resolveBase(config, request);
769
+ const redirectUrl = isAbsoluteUrl3(callbackUrl) ? callbackUrl : `${baseUrl}${callbackUrl}`;
770
+ const headers = new Headers();
771
+ applySessionCookie(headers, sessionToken, config);
772
+ clearStateCookie(headers, config);
773
+ clearCallbackUrlCookie(headers, config);
774
+ headers.set("Location", redirectUrl);
775
+ return new Response(null, { status: 302, headers });
776
+ }
777
+ async function resolveBase(config, request) {
778
+ if (typeof config.baseUrl === "function") {
779
+ const r = await config.baseUrl(request);
780
+ return r.startsWith("http") ? r : `https://${r}`;
781
+ }
782
+ return config.baseUrl;
783
+ }
784
+ function isAbsoluteUrl3(url) {
785
+ return url.startsWith("http://") || url.startsWith("https://");
786
+ }
567
787
  function VinextAuth(config) {
568
788
  const resolved = resolveConfig(config);
569
789
  async function handler(request) {
@@ -577,19 +797,23 @@ function VinextAuth(config) {
577
797
  const parts = action.split("/");
578
798
  const verb = parts[0];
579
799
  const param = parts[1];
800
+ if (resolved.debug) {
801
+ console.log(`[VinextAuth] ${request.method} ${pathname} \u2192 ${verb}/${param ?? ""}`);
802
+ }
580
803
  if (verb === "signin" && param) {
804
+ const provider = resolved.providers.find((p) => p.id === param);
805
+ if (provider?.type === "credentials") {
806
+ return handleCredentials(request, provider, resolved);
807
+ }
581
808
  return handleSignIn(request, param, resolved);
582
809
  }
583
810
  if (verb === "signin" && !param) {
584
- return handleSignInPage(resolved);
811
+ return handleSignInPage(resolved, request);
585
812
  }
586
813
  if (verb === "callback" && param) {
587
814
  return handleCallback(request, param, resolved);
588
815
  }
589
816
  if (verb === "signout") {
590
- if (request.method === "POST") {
591
- return handleSignOut(request, resolved);
592
- }
593
817
  return handleSignOut(request, resolved);
594
818
  }
595
819
  if (verb === "session") {
@@ -598,31 +822,117 @@ function VinextAuth(config) {
598
822
  if (verb === "csrf") {
599
823
  return handleCsrfRoute(request, resolved);
600
824
  }
825
+ if (verb === "providers") {
826
+ const providers = resolved.providers.map((p) => ({
827
+ id: p.id,
828
+ name: p.name,
829
+ type: p.type,
830
+ signinUrl: `${resolved.basePath}/signin/${p.id}`,
831
+ callbackUrl: p.type === "oauth" ? `${resolved.basePath}/callback/${p.id}` : null
832
+ }));
833
+ return Response.json(providers);
834
+ }
601
835
  if (verb === "error") {
602
- const error = url.searchParams.get("error") ?? "Unknown";
603
- return new Response(
604
- `<!DOCTYPE html><html><body><h1>Authentication Error</h1><p>${error}</p><a href="${resolved.pages.signIn}">Try again</a></body></html>`,
605
- { status: 400, headers: { "Content-Type": "text/html" } }
606
- );
836
+ return handleErrorPage(url, resolved);
607
837
  }
608
838
  return new Response("Not Found", { status: 404 });
609
839
  }
610
840
  return { GET: handler, POST: handler };
611
841
  }
612
- function handleSignInPage(config) {
613
- const providers = config.providers.map((p) => `
614
- <a href="${config.basePath}/signin/${p.id}" style="display:block;margin:8px 0;padding:12px 24px;border:1px solid #ccc;border-radius:6px;text-decoration:none;color:#000;">
842
+ async function handleSignInPage(config, request) {
843
+ const baseUrl = await resolveBaseUrl(config, request);
844
+ const callbackUrl = new URL(request.url).searchParams.get("callbackUrl") ?? "/";
845
+ const providers = config.providers.map(
846
+ (p) => `
847
+ <a href="${baseUrl}${config.basePath}/signin/${p.id}?callbackUrl=${encodeURIComponent(callbackUrl)}"
848
+ style="display:flex;align-items:center;gap:8px;margin:8px 0;padding:12px 20px;
849
+ border:1px solid #e2e8f0;border-radius:8px;text-decoration:none;color:#1a202c;
850
+ font-weight:500;transition:background 0.15s;"
851
+ onmouseover="this.style.background='#f7fafc'" onmouseout="this.style.background=''">
615
852
  Sign in with ${p.name}
616
- </a>
617
- `).join("");
853
+ </a>`
854
+ ).join("");
618
855
  return new Response(
619
- `<!DOCTYPE html><html><body style="font-family:sans-serif;max-width:400px;margin:80px auto;padding:24px">
620
- <h1>Sign In</h1>${providers}
621
- </body></html>`,
856
+ `<!DOCTYPE html>
857
+ <html lang="en">
858
+ <head>
859
+ <meta charset="UTF-8">
860
+ <meta name="viewport" content="width=device-width,initial-scale=1">
861
+ <title>Sign In</title>
862
+ <style>
863
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
864
+ background: #f7fafc; display: flex; align-items: center;
865
+ justify-content: center; min-height: 100vh; margin: 0; }
866
+ .card { background: white; border-radius: 12px; padding: 32px 40px;
867
+ box-shadow: 0 1px 3px rgba(0,0,0,.1); width: 100%; max-width: 380px; }
868
+ h1 { margin: 0 0 24px; font-size: 24px; text-align: center; }
869
+ </style>
870
+ </head>
871
+ <body>
872
+ <div class="card">
873
+ <h1>Sign In</h1>
874
+ ${providers}
875
+ </div>
876
+ </body>
877
+ </html>`,
622
878
  { headers: { "Content-Type": "text/html" } }
623
879
  );
624
880
  }
881
+ function handleErrorPage(url, config) {
882
+ const error = url.searchParams.get("error") ?? "Unknown";
883
+ const retryAfter = url.searchParams.get("retryAfter");
884
+ const messages = {
885
+ OAuthAccountNotLinked: "This email is already associated with another account. Enable account linking or sign in with your original provider.",
886
+ OAuthCallbackError: "Authentication failed. Please try again.",
887
+ OAuthStateError: "Authentication state mismatch. Please try again.",
888
+ AccessDenied: "You do not have permission to sign in.",
889
+ RateLimitExceeded: `Too many sign-in attempts. Please wait${retryAfter ? ` ${retryAfter} seconds` : ""} before trying again.`,
890
+ InvalidCredentials: "Invalid email or password.",
891
+ SessionExpired: "Your session has expired. Please sign in again.",
892
+ Configuration: "Server configuration error.",
893
+ Unknown: "An unexpected error occurred."
894
+ };
895
+ const message = messages[error] ?? messages.Unknown;
896
+ return new Response(
897
+ `<!DOCTYPE html>
898
+ <html lang="en">
899
+ <head>
900
+ <meta charset="UTF-8">
901
+ <title>Authentication Error</title>
902
+ <style>
903
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
904
+ background: #f7fafc; display: flex; align-items: center;
905
+ justify-content: center; min-height: 100vh; margin: 0; }
906
+ .card { background: white; border-radius: 12px; padding: 32px 40px;
907
+ box-shadow: 0 1px 3px rgba(0,0,0,.1); width: 100%; max-width: 420px; text-align: center; }
908
+ h1 { color: #e53e3e; margin-bottom: 12px; }
909
+ p { color: #4a5568; line-height: 1.6; }
910
+ a { display:inline-block; margin-top:20px; padding:10px 24px;
911
+ background:#3182ce; color:white; border-radius:6px; text-decoration:none; }
912
+ </style>
913
+ </head>
914
+ <body>
915
+ <div class="card">
916
+ <h1>Authentication Error</h1>
917
+ <p>${message}</p>
918
+ <a href="${config.pages.signIn}">Try again</a>
919
+ </div>
920
+ </body>
921
+ </html>`,
922
+ { status: 400, headers: { "Content-Type": "text/html" } }
923
+ );
924
+ }
925
+
926
+ // src/types.ts
927
+ var VinextAuthError = class extends Error {
928
+ code;
929
+ constructor(code, message) {
930
+ super(message);
931
+ this.name = "VinextAuthError";
932
+ this.code = code;
933
+ }
934
+ };
625
935
 
626
- export { VinextAuth, VinextAuth as default };
936
+ export { InMemoryRateLimiter, VinextAuth, VinextAuthError, VinextAuth as default };
627
937
  //# sourceMappingURL=index.js.map
628
938
  //# sourceMappingURL=index.js.map