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/adapters/cloudflare-kv.d.ts +1 -1
- package/dist/index.d.ts +41 -6
- package/dist/index.js +378 -68
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +1 -1
- package/dist/providers/credentials.d.ts +38 -0
- package/dist/providers/credentials.js +15 -0
- package/dist/providers/credentials.js.map +1 -0
- package/dist/providers/github.d.ts +1 -1
- package/dist/providers/google.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/server/index.d.ts +32 -12
- package/dist/server/index.js +136 -8
- package/dist/server/index.js.map +1 -1
- package/dist/types-Bsno2U1C.d.ts +270 -0
- package/package.json +5 -1
- package/dist/types-G_m6Z3Iz.d.ts +0 -180
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
|
|
89
|
-
const
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 = `${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 : `${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
614
|
-
|
|
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
|
-
|
|
853
|
+
</a>`
|
|
854
|
+
).join("");
|
|
618
855
|
return new Response(
|
|
619
|
-
`<!DOCTYPE html
|
|
620
|
-
|
|
621
|
-
|
|
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
|