@better-auth/infra 0.2.12 → 0.2.14
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/CHANGELOG.md +41 -0
- package/dist/client.mjs +2 -2
- package/dist/{constants-CvriWQVc.mjs → constants-CLYqEwMV.mjs} +7 -1
- package/dist/email.mjs +5 -2
- package/dist/{fetch-DiAhoiKA.mjs → fetch-Bl0S3xUi.mjs} +3 -3
- package/dist/index.d.mts +13 -2
- package/dist/index.mjs +475 -230
- package/dist/native.mjs +2 -2
- package/package.json +7 -5
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { n as INFRA_API_URL, r as INFRA_KV_URL } from "./constants-
|
|
2
|
-
import { n as createKV, t as createAPI } from "./fetch-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CLYqEwMV.mjs";
|
|
2
|
+
import { n as createKV, t as createAPI } from "./fetch-Bl0S3xUi.mjs";
|
|
3
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
5
|
+
import { APIError, generateId, getAuthTables, logger } from "better-auth";
|
|
5
6
|
import { env } from "@better-auth/core/env";
|
|
6
7
|
import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
|
|
7
|
-
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
8
8
|
import { createFetch } from "@better-fetch/fetch";
|
|
9
9
|
import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
|
|
10
10
|
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
11
11
|
import z$1, { z } from "zod";
|
|
12
12
|
import { setSessionCookie } from "better-auth/cookies";
|
|
13
|
+
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
|
|
14
|
+
import { createOTP } from "@better-auth/utils/otp";
|
|
13
15
|
//#region src/options.ts
|
|
14
16
|
function resolveConnectionOptions(options) {
|
|
15
17
|
return {
|
|
@@ -66,6 +68,7 @@ const EVENT_TYPES = {
|
|
|
66
68
|
USER_IMPERSONATED: "user_impersonated",
|
|
67
69
|
USER_IMPERSONATED_STOPPED: "user_impersonated_stopped"
|
|
68
70
|
};
|
|
71
|
+
const UNKNOWN_USER = "unknown";
|
|
69
72
|
const routes = {
|
|
70
73
|
SEND_VERIFICATION_EMAIL: "/send-verification-email",
|
|
71
74
|
SIGN_IN: "/sign-in",
|
|
@@ -106,8 +109,11 @@ const routes = {
|
|
|
106
109
|
API_KEY_CREATE: "/api-key/create",
|
|
107
110
|
LINK_SOCIAL: "/link-social",
|
|
108
111
|
DASH_ROUTE: "/dash",
|
|
112
|
+
DASH_ACCEPT_INVITATION: "/dash/accept-invitation",
|
|
113
|
+
DASH_COMPLETE_INVITATION_SOCIAL: "/dash/complete-invitation-social",
|
|
109
114
|
DASH_UPDATE_USER: "/dash/update-user",
|
|
110
115
|
DASH_REVOKE_SESSIONS_ALL: "/dash/sessions/revoke-all",
|
|
116
|
+
DASH_IMPERSONATE_USER: "/dash/impersonate-user",
|
|
111
117
|
DASH_BAN_USER: "/dash/ban-user",
|
|
112
118
|
DASH_UNBAN_USER: "/dash/unban-user",
|
|
113
119
|
ADMIN_ROUTE: "/admin",
|
|
@@ -180,34 +186,6 @@ async function getUserById(userId, ctx) {
|
|
|
180
186
|
}
|
|
181
187
|
return user;
|
|
182
188
|
}
|
|
183
|
-
const getUserByIdToken = async (providerId, idToken, ctx) => {
|
|
184
|
-
const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
|
|
185
|
-
let user = null;
|
|
186
|
-
if (provider) try {
|
|
187
|
-
user = await provider.getUserInfo(idToken);
|
|
188
|
-
} catch (error) {
|
|
189
|
-
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
190
|
-
}
|
|
191
|
-
return user;
|
|
192
|
-
};
|
|
193
|
-
const getUserByAuthorizationCode = async (providerId, ctx) => {
|
|
194
|
-
let userInfo = null;
|
|
195
|
-
const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
|
|
196
|
-
if (provider) try {
|
|
197
|
-
const codeVerifier = (await parseState(ctx)).codeVerifier;
|
|
198
|
-
const { code, device_id } = ctx.query ?? {};
|
|
199
|
-
const tokens = await provider.validateAuthorizationCode({
|
|
200
|
-
code,
|
|
201
|
-
codeVerifier,
|
|
202
|
-
deviceId: device_id,
|
|
203
|
-
redirectURI: `${ctx.context.baseURL}/callback/${provider.id}`
|
|
204
|
-
});
|
|
205
|
-
userInfo = await provider.getUserInfo({ ...tokens }).then((res) => res?.user);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
208
|
-
}
|
|
209
|
-
return userInfo;
|
|
210
|
-
};
|
|
211
189
|
//#endregion
|
|
212
190
|
//#region src/events/core/events-account.ts
|
|
213
191
|
const initAccountEvents = (tracker) => {
|
|
@@ -309,6 +287,7 @@ const routeToRegex = (route) => {
|
|
|
309
287
|
return new RegExp(`${pattern}(?:$|[/?])`);
|
|
310
288
|
};
|
|
311
289
|
const matchesAnyRoute = (path, routes) => {
|
|
290
|
+
if (!path) return false;
|
|
312
291
|
const cleanPath = stripQuery(path);
|
|
313
292
|
return routes.some((route) => routeToRegex(route).test(cleanPath));
|
|
314
293
|
};
|
|
@@ -344,6 +323,53 @@ const getLoginMethod = (ctx) => {
|
|
|
344
323
|
return null;
|
|
345
324
|
};
|
|
346
325
|
//#endregion
|
|
326
|
+
//#region src/events/core/oauth-callback-user.ts
|
|
327
|
+
const OAUTH_CALLBACK_USER = Symbol.for("dash.oauthCallbackUser");
|
|
328
|
+
/**
|
|
329
|
+
* Stash OAuth profile on the request context when better-auth calls getUserInfo
|
|
330
|
+
* during the callback, before the authorization code is consumed.
|
|
331
|
+
*/
|
|
332
|
+
function instrumentSocialProviders(providers) {
|
|
333
|
+
for (const provider of providers) {
|
|
334
|
+
const originalGetUserInfo = provider.getUserInfo.bind(provider);
|
|
335
|
+
provider.getUserInfo = async (token) => {
|
|
336
|
+
const result = await originalGetUserInfo(token);
|
|
337
|
+
const user = result?.user;
|
|
338
|
+
if (user) try {
|
|
339
|
+
const endpointCtx = await getCurrentAuthContext();
|
|
340
|
+
endpointCtx.context[OAUTH_CALLBACK_USER] = user;
|
|
341
|
+
} catch {}
|
|
342
|
+
return result;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function resolveOAuthUser(providerId, ctx) {
|
|
347
|
+
const oauthUser = ctx.context[OAUTH_CALLBACK_USER];
|
|
348
|
+
if (!oauthUser) return null;
|
|
349
|
+
const email = oauthUser.email?.toLowerCase();
|
|
350
|
+
const accountId = oauthUser.id !== void 0 && oauthUser.id !== null ? String(oauthUser.id) : void 0;
|
|
351
|
+
if (email && accountId) try {
|
|
352
|
+
const result = await ctx.context.internalAdapter.findOAuthUser(email, accountId, providerId);
|
|
353
|
+
if (result?.user) return {
|
|
354
|
+
id: result.user.id,
|
|
355
|
+
email: result.user.email,
|
|
356
|
+
name: result.user.name
|
|
357
|
+
};
|
|
358
|
+
} catch (error) {
|
|
359
|
+
logger.debug("[Dash] Failed to find OAuth user:", error);
|
|
360
|
+
}
|
|
361
|
+
if (email) {
|
|
362
|
+
const user = await getUserByEmail(email, ctx);
|
|
363
|
+
if (user) return user;
|
|
364
|
+
}
|
|
365
|
+
if (!email && !oauthUser.name) return null;
|
|
366
|
+
return {
|
|
367
|
+
id: UNKNOWN_USER,
|
|
368
|
+
email: oauthUser.email ?? null,
|
|
369
|
+
name: oauthUser.name ?? "unknown"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
347
373
|
//#region src/events/core/events-session.ts
|
|
348
374
|
const initSessionEvents = (tracker) => {
|
|
349
375
|
const { trackEvent } = tracker;
|
|
@@ -425,7 +451,7 @@ const initSessionEvents = (tracker) => {
|
|
|
425
451
|
};
|
|
426
452
|
ctx.context.runInBackground(track());
|
|
427
453
|
};
|
|
428
|
-
const trackSessionRevokedAll = (session, trigger, ctx,
|
|
454
|
+
const trackSessionRevokedAll = (session, trigger, ctx, location) => {
|
|
429
455
|
const track = async () => {
|
|
430
456
|
const user = await getUserById(session.userId, ctx);
|
|
431
457
|
trackEvent({
|
|
@@ -438,7 +464,11 @@ const initSessionEvents = (tracker) => {
|
|
|
438
464
|
userEmail: user?.email ?? "unknown",
|
|
439
465
|
triggeredBy: trigger.triggeredBy,
|
|
440
466
|
triggerContext: trigger.triggerContext
|
|
441
|
-
}
|
|
467
|
+
},
|
|
468
|
+
ipAddress: location?.ipAddress,
|
|
469
|
+
city: location?.city,
|
|
470
|
+
country: location?.country,
|
|
471
|
+
countryCode: location?.countryCode
|
|
442
472
|
});
|
|
443
473
|
};
|
|
444
474
|
ctx.context.runInBackground(track());
|
|
@@ -527,7 +557,7 @@ const initSessionEvents = (tracker) => {
|
|
|
527
557
|
};
|
|
528
558
|
ctx.context.runInBackground(track());
|
|
529
559
|
};
|
|
530
|
-
const trackEmailVerificationSent = (session, user, trigger,
|
|
560
|
+
const trackEmailVerificationSent = (session, user, trigger, location) => {
|
|
531
561
|
trackEvent({
|
|
532
562
|
eventKey: session.userId,
|
|
533
563
|
eventType: EVENT_TYPES.EMAIL_VERIFICATION_SENT,
|
|
@@ -539,10 +569,14 @@ const initSessionEvents = (tracker) => {
|
|
|
539
569
|
sessionId: session.id,
|
|
540
570
|
triggeredBy: trigger.triggeredBy,
|
|
541
571
|
triggerContext: trigger.triggerContext
|
|
542
|
-
}
|
|
572
|
+
},
|
|
573
|
+
ipAddress: location?.ipAddress,
|
|
574
|
+
city: location?.city,
|
|
575
|
+
country: location?.country,
|
|
576
|
+
countryCode: location?.countryCode
|
|
543
577
|
});
|
|
544
578
|
};
|
|
545
|
-
const trackEmailSignInAttempt = (ctx, trigger) => {
|
|
579
|
+
const trackEmailSignInAttempt = (ctx, trigger, location) => {
|
|
546
580
|
const track = async () => {
|
|
547
581
|
const user = await getUserByEmail(ctx.body.email, ctx);
|
|
548
582
|
trackEvent({
|
|
@@ -556,49 +590,44 @@ const initSessionEvents = (tracker) => {
|
|
|
556
590
|
loginMethod: getLoginMethod(ctx),
|
|
557
591
|
triggeredBy: user?.id ?? trigger.triggeredBy,
|
|
558
592
|
triggerContext: trigger.triggerContext
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const trackSocialSignInAttempt = (ctx, trigger) => {
|
|
565
|
-
const track = async () => {
|
|
566
|
-
const user = await getUserByIdToken(ctx.body.provider, ctx.body.idToken, ctx);
|
|
567
|
-
trackEvent({
|
|
568
|
-
eventKey: user?.user.id.toString() ?? "unknown",
|
|
569
|
-
eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
|
|
570
|
-
eventDisplayName: "User sign-in attempt failed",
|
|
571
|
-
eventData: {
|
|
572
|
-
userId: user?.user.id.toString() ?? "unknown",
|
|
573
|
-
userName: user?.user.name ?? "unknown",
|
|
574
|
-
userEmail: user?.user.email ?? "unknown",
|
|
575
|
-
loginMethod: getLoginMethod(ctx),
|
|
576
|
-
triggeredBy: user?.user.id ?? trigger.triggeredBy,
|
|
577
|
-
triggerContext: trigger.triggerContext
|
|
578
|
-
}
|
|
593
|
+
},
|
|
594
|
+
ipAddress: location?.ipAddress,
|
|
595
|
+
city: location?.city,
|
|
596
|
+
country: location?.country,
|
|
597
|
+
countryCode: location?.countryCode
|
|
579
598
|
});
|
|
580
599
|
};
|
|
581
600
|
ctx.context.runInBackground(track());
|
|
582
601
|
};
|
|
583
|
-
const
|
|
602
|
+
const trackSocialSignInFailure = (ctx, providerId, trigger, location) => {
|
|
584
603
|
const track = async () => {
|
|
585
|
-
const user = await
|
|
604
|
+
const user = await resolveOAuthUser(providerId, ctx);
|
|
586
605
|
trackEvent({
|
|
587
|
-
eventKey: user?.id
|
|
606
|
+
eventKey: user?.id ?? "unknown",
|
|
588
607
|
eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
|
|
589
608
|
eventDisplayName: "User sign-in attempt failed",
|
|
590
609
|
eventData: {
|
|
591
|
-
userId: user?.id
|
|
610
|
+
userId: user?.id ?? "unknown",
|
|
592
611
|
userName: user?.name ?? "unknown",
|
|
593
|
-
userEmail: user?.
|
|
612
|
+
userEmail: user?.email ?? "unknown",
|
|
594
613
|
loginMethod: getLoginMethod(ctx),
|
|
595
|
-
triggeredBy: trigger.triggeredBy,
|
|
614
|
+
triggeredBy: user && user.id !== "unknown" ? user.id : trigger.triggeredBy,
|
|
596
615
|
triggerContext: trigger.triggerContext
|
|
597
|
-
}
|
|
616
|
+
},
|
|
617
|
+
ipAddress: location?.ipAddress,
|
|
618
|
+
city: location?.city,
|
|
619
|
+
country: location?.country,
|
|
620
|
+
countryCode: location?.countryCode
|
|
598
621
|
});
|
|
599
622
|
};
|
|
600
623
|
ctx.context.runInBackground(track());
|
|
601
624
|
};
|
|
625
|
+
const trackSocialSignInAttempt = (ctx, trigger, location) => {
|
|
626
|
+
trackSocialSignInFailure(ctx, ctx.body.provider, trigger, location);
|
|
627
|
+
};
|
|
628
|
+
const trackSocialSignInRedirectionAttempt = (ctx, trigger, location) => {
|
|
629
|
+
trackSocialSignInFailure(ctx, tryDecode(ctx.params?.id), trigger, location);
|
|
630
|
+
};
|
|
602
631
|
return {
|
|
603
632
|
trackUserSignedIn,
|
|
604
633
|
trackUserSignedOut,
|
|
@@ -983,23 +1012,22 @@ function createIdentificationMiddleware($kv) {
|
|
|
983
1012
|
ctx.context.visitorId = visitorId;
|
|
984
1013
|
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
985
1014
|
let location;
|
|
1015
|
+
const requestIp = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
|
|
1016
|
+
const requestCountryCode = getCountryCodeFromRequest(ctx.request);
|
|
986
1017
|
if (ipConfig?.disableIpTracking === true) location = void 0;
|
|
987
1018
|
else if (requestId && identification) {
|
|
988
1019
|
const loc = getLocation(identification);
|
|
989
1020
|
location = {
|
|
990
|
-
ipAddress: identification.ip || void 0,
|
|
1021
|
+
ipAddress: identification.ip || requestIp || void 0,
|
|
991
1022
|
city: loc?.city || void 0,
|
|
992
1023
|
country: loc?.country?.name || void 0,
|
|
993
|
-
countryCode: loc?.country?.code || void 0
|
|
994
|
-
};
|
|
995
|
-
} else {
|
|
996
|
-
const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
|
|
997
|
-
if (ipAddress) location = {
|
|
998
|
-
ipAddress,
|
|
999
|
-
countryCode: getCountryCodeFromRequest(ctx.request)
|
|
1024
|
+
countryCode: loc?.country?.code || requestCountryCode || void 0
|
|
1000
1025
|
};
|
|
1001
|
-
|
|
1002
|
-
|
|
1026
|
+
} else if (requestIp) location = {
|
|
1027
|
+
ipAddress: requestIp,
|
|
1028
|
+
countryCode: requestCountryCode
|
|
1029
|
+
};
|
|
1030
|
+
else location = void 0;
|
|
1003
1031
|
ctx.context.location = location;
|
|
1004
1032
|
ctx.context.ip = resolveSecurityIp(identification, location);
|
|
1005
1033
|
ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
|
|
@@ -1071,9 +1099,11 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
|
|
|
1071
1099
|
ev.emitted = true;
|
|
1072
1100
|
const identification = ctx.context.identification ?? null;
|
|
1073
1101
|
const visitorId = ctx.context.visitorId ?? null;
|
|
1102
|
+
const location = ctx.context.location;
|
|
1074
1103
|
const path = ctx.path ?? "";
|
|
1104
|
+
const ipAddress = location?.ipAddress || identification?.ip || ctx.context.ip || void 0;
|
|
1075
1105
|
trackEvent({
|
|
1076
|
-
eventKey: visitorId ||
|
|
1106
|
+
eventKey: visitorId || ipAddress || "unknown",
|
|
1077
1107
|
eventType: "security_check",
|
|
1078
1108
|
eventDisplayName: ev.outcome === "blocked" ? "Security: blocked" : ev.outcome === "challenged" ? "Security: challenged" : "Security: passed",
|
|
1079
1109
|
eventData: {
|
|
@@ -1086,10 +1116,10 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
|
|
|
1086
1116
|
userAgent: options.userAgent,
|
|
1087
1117
|
details: ev.details
|
|
1088
1118
|
},
|
|
1089
|
-
ipAddress
|
|
1090
|
-
city: identification?.location?.city || void 0,
|
|
1091
|
-
country: identification?.location?.country?.name || void 0,
|
|
1092
|
-
countryCode: identification?.location?.country?.code || void 0
|
|
1119
|
+
ipAddress,
|
|
1120
|
+
city: location?.city || identification?.location?.city || void 0,
|
|
1121
|
+
country: location?.country || identification?.location?.country?.name || void 0,
|
|
1122
|
+
countryCode: location?.countryCode || identification?.location?.country?.code || void 0
|
|
1093
1123
|
});
|
|
1094
1124
|
}
|
|
1095
1125
|
//#endregion
|
|
@@ -1241,7 +1271,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1241
1271
|
return "";
|
|
1242
1272
|
}
|
|
1243
1273
|
},
|
|
1244
|
-
async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null) {
|
|
1274
|
+
async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null, powSolution) {
|
|
1245
1275
|
if (!options.impossibleTravel?.enabled || !currentLocation) return null;
|
|
1246
1276
|
try {
|
|
1247
1277
|
const data = await $api("/security/impossible-travel", {
|
|
@@ -1251,7 +1281,8 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1251
1281
|
visitorId,
|
|
1252
1282
|
location: currentLocation,
|
|
1253
1283
|
ip,
|
|
1254
|
-
config: options
|
|
1284
|
+
config: options,
|
|
1285
|
+
...powSolution ? { powSolution } : {}
|
|
1255
1286
|
}
|
|
1256
1287
|
});
|
|
1257
1288
|
if (data.isImpossible) {
|
|
@@ -2190,7 +2221,9 @@ const sentinel = (options) => {
|
|
|
2190
2221
|
const identification = ctx.context.identification;
|
|
2191
2222
|
if (session.userId && identification?.location && visitorId) {
|
|
2192
2223
|
recordCheck(ctx, "impossible_travel");
|
|
2193
|
-
const
|
|
2224
|
+
const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
|
|
2225
|
+
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution);
|
|
2226
|
+
if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
|
|
2194
2227
|
if (travelCheck?.isImpossible) {
|
|
2195
2228
|
if (travelCheck.action === "block") {
|
|
2196
2229
|
setOutcome(ctx, "blocked", "impossible_travel", {
|
|
@@ -2399,7 +2432,7 @@ const sentinel = (options) => {
|
|
|
2399
2432
|
//#region src/events/organization/events-invitation.ts
|
|
2400
2433
|
const initInvitationEvents = (tracker) => {
|
|
2401
2434
|
const { trackEvent } = tracker;
|
|
2402
|
-
const trackOrganizationMemberInvited = (organization, invitation, inviter, trigger) => {
|
|
2435
|
+
const trackOrganizationMemberInvited = (organization, invitation, inviter, trigger, location) => {
|
|
2403
2436
|
trackEvent({
|
|
2404
2437
|
eventKey: organization.id,
|
|
2405
2438
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITED,
|
|
@@ -2417,10 +2450,14 @@ const initInvitationEvents = (tracker) => {
|
|
|
2417
2450
|
inviterEmail: inviter.email ?? "unknown",
|
|
2418
2451
|
triggeredBy: trigger.triggeredBy,
|
|
2419
2452
|
triggerContext: trigger.triggerContext
|
|
2420
|
-
}
|
|
2453
|
+
},
|
|
2454
|
+
ipAddress: location?.ipAddress,
|
|
2455
|
+
city: location?.city,
|
|
2456
|
+
country: location?.country,
|
|
2457
|
+
countryCode: location?.countryCode
|
|
2421
2458
|
});
|
|
2422
2459
|
};
|
|
2423
|
-
const trackOrganizationMemberInviteAccepted = (organization, invitation, member, acceptedBy, trigger) => {
|
|
2460
|
+
const trackOrganizationMemberInviteAccepted = (organization, invitation, member, acceptedBy, trigger, location) => {
|
|
2424
2461
|
trackEvent({
|
|
2425
2462
|
eventKey: organization.id,
|
|
2426
2463
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_ACCEPTED,
|
|
@@ -2440,10 +2477,14 @@ const initInvitationEvents = (tracker) => {
|
|
|
2440
2477
|
memberRole: member.role,
|
|
2441
2478
|
triggeredBy: trigger.triggeredBy,
|
|
2442
2479
|
triggerContext: trigger.triggerContext
|
|
2443
|
-
}
|
|
2480
|
+
},
|
|
2481
|
+
ipAddress: location?.ipAddress,
|
|
2482
|
+
city: location?.city,
|
|
2483
|
+
country: location?.country,
|
|
2484
|
+
countryCode: location?.countryCode
|
|
2444
2485
|
});
|
|
2445
2486
|
};
|
|
2446
|
-
const trackOrganizationMemberInviteRejected = (organization, invitation, rejectedBy, trigger) => {
|
|
2487
|
+
const trackOrganizationMemberInviteRejected = (organization, invitation, rejectedBy, trigger, location) => {
|
|
2447
2488
|
trackEvent({
|
|
2448
2489
|
eventKey: organization.id,
|
|
2449
2490
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_REJECTED,
|
|
@@ -2461,10 +2502,14 @@ const initInvitationEvents = (tracker) => {
|
|
|
2461
2502
|
rejectedByName: rejectedBy.name,
|
|
2462
2503
|
triggeredBy: trigger.triggeredBy,
|
|
2463
2504
|
triggerContext: trigger.triggerContext
|
|
2464
|
-
}
|
|
2505
|
+
},
|
|
2506
|
+
ipAddress: location?.ipAddress,
|
|
2507
|
+
city: location?.city,
|
|
2508
|
+
country: location?.country,
|
|
2509
|
+
countryCode: location?.countryCode
|
|
2465
2510
|
});
|
|
2466
2511
|
};
|
|
2467
|
-
const trackOrganizationMemberInviteCanceled = (organization, invitation, cancelledBy, trigger) => {
|
|
2512
|
+
const trackOrganizationMemberInviteCanceled = (organization, invitation, cancelledBy, trigger, location) => {
|
|
2468
2513
|
trackEvent({
|
|
2469
2514
|
eventKey: organization.id,
|
|
2470
2515
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_CANCELED,
|
|
@@ -2482,7 +2527,11 @@ const initInvitationEvents = (tracker) => {
|
|
|
2482
2527
|
cancelledByEmail: cancelledBy.email ?? "unknown",
|
|
2483
2528
|
triggeredBy: trigger.triggeredBy,
|
|
2484
2529
|
triggerContext: trigger.triggerContext
|
|
2485
|
-
}
|
|
2530
|
+
},
|
|
2531
|
+
ipAddress: location?.ipAddress,
|
|
2532
|
+
city: location?.city,
|
|
2533
|
+
country: location?.country,
|
|
2534
|
+
countryCode: location?.countryCode
|
|
2486
2535
|
});
|
|
2487
2536
|
};
|
|
2488
2537
|
return {
|
|
@@ -2496,7 +2545,7 @@ const initInvitationEvents = (tracker) => {
|
|
|
2496
2545
|
//#region src/events/organization/events-member.ts
|
|
2497
2546
|
const initMemberEvents = (tracker) => {
|
|
2498
2547
|
const { trackEvent } = tracker;
|
|
2499
|
-
const trackOrganizationMemberAdded = (organization, member, user, trigger) => {
|
|
2548
|
+
const trackOrganizationMemberAdded = (organization, member, user, trigger, location) => {
|
|
2500
2549
|
trackEvent({
|
|
2501
2550
|
eventKey: organization.id,
|
|
2502
2551
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_ADDED,
|
|
@@ -2512,10 +2561,14 @@ const initMemberEvents = (tracker) => {
|
|
|
2512
2561
|
memberEmail: user.email ?? "unknown",
|
|
2513
2562
|
triggeredBy: trigger.triggeredBy,
|
|
2514
2563
|
triggerContext: trigger.triggerContext
|
|
2515
|
-
}
|
|
2564
|
+
},
|
|
2565
|
+
ipAddress: location?.ipAddress,
|
|
2566
|
+
city: location?.city,
|
|
2567
|
+
country: location?.country,
|
|
2568
|
+
countryCode: location?.countryCode
|
|
2516
2569
|
});
|
|
2517
2570
|
};
|
|
2518
|
-
const trackOrganizationMemberRemoved = (organization, member, user, trigger) => {
|
|
2571
|
+
const trackOrganizationMemberRemoved = (organization, member, user, trigger, location) => {
|
|
2519
2572
|
trackEvent({
|
|
2520
2573
|
eventKey: organization.id,
|
|
2521
2574
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_REMOVED,
|
|
@@ -2531,10 +2584,14 @@ const initMemberEvents = (tracker) => {
|
|
|
2531
2584
|
memberEmail: user.email ?? "unknown",
|
|
2532
2585
|
triggeredBy: trigger.triggeredBy,
|
|
2533
2586
|
triggerContext: trigger.triggerContext
|
|
2534
|
-
}
|
|
2587
|
+
},
|
|
2588
|
+
ipAddress: location?.ipAddress,
|
|
2589
|
+
city: location?.city,
|
|
2590
|
+
country: location?.country,
|
|
2591
|
+
countryCode: location?.countryCode
|
|
2535
2592
|
});
|
|
2536
2593
|
};
|
|
2537
|
-
const trackOrganizationMemberRoleUpdated = (organization, member, user, previousRole, trigger) => {
|
|
2594
|
+
const trackOrganizationMemberRoleUpdated = (organization, member, user, previousRole, trigger, location) => {
|
|
2538
2595
|
trackEvent({
|
|
2539
2596
|
eventKey: organization.id,
|
|
2540
2597
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_ROLE_UPDATED,
|
|
@@ -2551,7 +2608,11 @@ const initMemberEvents = (tracker) => {
|
|
|
2551
2608
|
memberEmail: user.email ?? "unknown",
|
|
2552
2609
|
triggeredBy: trigger.triggeredBy,
|
|
2553
2610
|
triggerContext: trigger.triggerContext
|
|
2554
|
-
}
|
|
2611
|
+
},
|
|
2612
|
+
ipAddress: location?.ipAddress,
|
|
2613
|
+
city: location?.city,
|
|
2614
|
+
country: location?.country,
|
|
2615
|
+
countryCode: location?.countryCode
|
|
2555
2616
|
});
|
|
2556
2617
|
};
|
|
2557
2618
|
return {
|
|
@@ -2564,7 +2625,7 @@ const initMemberEvents = (tracker) => {
|
|
|
2564
2625
|
//#region src/events/organization/events-organization.ts
|
|
2565
2626
|
const initOrganizationEvents = (tracker) => {
|
|
2566
2627
|
const { trackEvent } = tracker;
|
|
2567
|
-
const trackOrganizationCreated = (organization, trigger) => {
|
|
2628
|
+
const trackOrganizationCreated = (organization, trigger, location) => {
|
|
2568
2629
|
trackEvent({
|
|
2569
2630
|
eventKey: organization.id,
|
|
2570
2631
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_CREATED,
|
|
@@ -2575,10 +2636,14 @@ const initOrganizationEvents = (tracker) => {
|
|
|
2575
2636
|
organizationName: organization.name,
|
|
2576
2637
|
triggeredBy: trigger.triggeredBy,
|
|
2577
2638
|
triggerContext: trigger.triggerContext
|
|
2578
|
-
}
|
|
2639
|
+
},
|
|
2640
|
+
ipAddress: location?.ipAddress,
|
|
2641
|
+
city: location?.city,
|
|
2642
|
+
country: location?.country,
|
|
2643
|
+
countryCode: location?.countryCode
|
|
2579
2644
|
});
|
|
2580
2645
|
};
|
|
2581
|
-
const trackOrganizationUpdated = (organization, trigger) => {
|
|
2646
|
+
const trackOrganizationUpdated = (organization, trigger, location) => {
|
|
2582
2647
|
trackEvent({
|
|
2583
2648
|
eventKey: organization.id,
|
|
2584
2649
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_UPDATED,
|
|
@@ -2589,7 +2654,11 @@ const initOrganizationEvents = (tracker) => {
|
|
|
2589
2654
|
organizationName: organization.name,
|
|
2590
2655
|
triggeredBy: trigger.triggeredBy,
|
|
2591
2656
|
triggerContext: trigger.triggerContext
|
|
2592
|
-
}
|
|
2657
|
+
},
|
|
2658
|
+
ipAddress: location?.ipAddress,
|
|
2659
|
+
city: location?.city,
|
|
2660
|
+
country: location?.country,
|
|
2661
|
+
countryCode: location?.countryCode
|
|
2593
2662
|
});
|
|
2594
2663
|
};
|
|
2595
2664
|
return {
|
|
@@ -2601,7 +2670,7 @@ const initOrganizationEvents = (tracker) => {
|
|
|
2601
2670
|
//#region src/events/organization/events-team.ts
|
|
2602
2671
|
const initTeamEvents = (tracker) => {
|
|
2603
2672
|
const { trackEvent } = tracker;
|
|
2604
|
-
const trackOrganizationTeamCreated = (organization, team, trigger) => {
|
|
2673
|
+
const trackOrganizationTeamCreated = (organization, team, trigger, location) => {
|
|
2605
2674
|
trackEvent({
|
|
2606
2675
|
eventKey: organization.id,
|
|
2607
2676
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_CREATED,
|
|
@@ -2614,10 +2683,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2614
2683
|
teamName: team.name,
|
|
2615
2684
|
triggeredBy: trigger.triggeredBy,
|
|
2616
2685
|
triggerContext: trigger.triggerContext
|
|
2617
|
-
}
|
|
2686
|
+
},
|
|
2687
|
+
ipAddress: location?.ipAddress,
|
|
2688
|
+
city: location?.city,
|
|
2689
|
+
country: location?.country,
|
|
2690
|
+
countryCode: location?.countryCode
|
|
2618
2691
|
});
|
|
2619
2692
|
};
|
|
2620
|
-
const trackOrganizationTeamUpdated = (organization, team, trigger) => {
|
|
2693
|
+
const trackOrganizationTeamUpdated = (organization, team, trigger, location) => {
|
|
2621
2694
|
trackEvent({
|
|
2622
2695
|
eventKey: organization.id,
|
|
2623
2696
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_UPDATED,
|
|
@@ -2630,10 +2703,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2630
2703
|
teamName: team.name,
|
|
2631
2704
|
triggeredBy: trigger.triggeredBy,
|
|
2632
2705
|
triggerContext: trigger.triggerContext
|
|
2633
|
-
}
|
|
2706
|
+
},
|
|
2707
|
+
ipAddress: location?.ipAddress,
|
|
2708
|
+
city: location?.city,
|
|
2709
|
+
country: location?.country,
|
|
2710
|
+
countryCode: location?.countryCode
|
|
2634
2711
|
});
|
|
2635
2712
|
};
|
|
2636
|
-
const trackOrganizationTeamDeleted = (organization, team, trigger) => {
|
|
2713
|
+
const trackOrganizationTeamDeleted = (organization, team, trigger, location) => {
|
|
2637
2714
|
trackEvent({
|
|
2638
2715
|
eventKey: organization.id,
|
|
2639
2716
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_DELETED,
|
|
@@ -2646,10 +2723,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2646
2723
|
teamName: team.name,
|
|
2647
2724
|
triggeredBy: trigger.triggeredBy,
|
|
2648
2725
|
triggerContext: trigger.triggerContext
|
|
2649
|
-
}
|
|
2726
|
+
},
|
|
2727
|
+
ipAddress: location?.ipAddress,
|
|
2728
|
+
city: location?.city,
|
|
2729
|
+
country: location?.country,
|
|
2730
|
+
countryCode: location?.countryCode
|
|
2650
2731
|
});
|
|
2651
2732
|
};
|
|
2652
|
-
const trackOrganizationTeamMemberAdded = (organization, team, user, teamMember, trigger) => {
|
|
2733
|
+
const trackOrganizationTeamMemberAdded = (organization, team, user, teamMember, trigger, location) => {
|
|
2653
2734
|
trackEvent({
|
|
2654
2735
|
eventKey: organization.id,
|
|
2655
2736
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_MEMBER_ADDED,
|
|
@@ -2664,10 +2745,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2664
2745
|
memberName: user.name,
|
|
2665
2746
|
triggeredBy: trigger.triggeredBy,
|
|
2666
2747
|
triggerContext: trigger.triggerContext
|
|
2667
|
-
}
|
|
2748
|
+
},
|
|
2749
|
+
ipAddress: location?.ipAddress,
|
|
2750
|
+
city: location?.city,
|
|
2751
|
+
country: location?.country,
|
|
2752
|
+
countryCode: location?.countryCode
|
|
2668
2753
|
});
|
|
2669
2754
|
};
|
|
2670
|
-
const trackOrganizationTeamMemberRemoved = (organization, team, user, teamMember, trigger) => {
|
|
2755
|
+
const trackOrganizationTeamMemberRemoved = (organization, team, user, teamMember, trigger, location) => {
|
|
2671
2756
|
trackEvent({
|
|
2672
2757
|
eventKey: organization.id,
|
|
2673
2758
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_MEMBER_REMOVED,
|
|
@@ -2682,7 +2767,11 @@ const initTeamEvents = (tracker) => {
|
|
|
2682
2767
|
memberName: user.name,
|
|
2683
2768
|
triggeredBy: trigger.triggeredBy,
|
|
2684
2769
|
triggerContext: trigger.triggerContext
|
|
2685
|
-
}
|
|
2770
|
+
},
|
|
2771
|
+
ipAddress: location?.ipAddress,
|
|
2772
|
+
city: location?.city,
|
|
2773
|
+
country: location?.country,
|
|
2774
|
+
countryCode: location?.countryCode
|
|
2686
2775
|
});
|
|
2687
2776
|
};
|
|
2688
2777
|
return {
|
|
@@ -2694,11 +2783,10 @@ const initTeamEvents = (tracker) => {
|
|
|
2694
2783
|
};
|
|
2695
2784
|
};
|
|
2696
2785
|
//#endregion
|
|
2697
|
-
//#region
|
|
2786
|
+
//#region ../utils/dist/crypto/index.mjs
|
|
2698
2787
|
/**
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
2701
|
-
* @param value - The value to hash
|
|
2788
|
+
* One-way hash for storage and lookup. Returns hex-encoded digest.
|
|
2789
|
+
* Use to verify values without ever storing plaintext.
|
|
2702
2790
|
*/
|
|
2703
2791
|
async function hash(value) {
|
|
2704
2792
|
const data = new TextEncoder().encode(value);
|
|
@@ -2706,6 +2794,32 @@ async function hash(value) {
|
|
|
2706
2794
|
const hashArray = new Uint8Array(hashBuffer);
|
|
2707
2795
|
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2708
2796
|
}
|
|
2797
|
+
function hexToBytes(hex) {
|
|
2798
|
+
if (hex.length % 2 !== 0) return null;
|
|
2799
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
2800
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2801
|
+
const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
2802
|
+
if (Number.isNaN(byte)) return null;
|
|
2803
|
+
bytes[i] = byte;
|
|
2804
|
+
}
|
|
2805
|
+
return bytes;
|
|
2806
|
+
}
|
|
2807
|
+
function timingSafeEqualBytes(a, b) {
|
|
2808
|
+
if (a.length !== b.length) return false;
|
|
2809
|
+
let diff = 0;
|
|
2810
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
2811
|
+
return diff === 0;
|
|
2812
|
+
}
|
|
2813
|
+
/** Constant-time comparison for hex-encoded digests (e.g. SHA-256 hashes). */
|
|
2814
|
+
function timingSafeEqualHash(a, b) {
|
|
2815
|
+
if (a.length !== b.length) return false;
|
|
2816
|
+
const aBytes = hexToBytes(a);
|
|
2817
|
+
const bBytes = hexToBytes(b);
|
|
2818
|
+
if (!aBytes || !bBytes || aBytes.length !== bBytes.length) return false;
|
|
2819
|
+
return timingSafeEqualBytes(aBytes, bBytes);
|
|
2820
|
+
}
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/jwt.ts
|
|
2709
2823
|
/**
|
|
2710
2824
|
* Skip JTI check for JWTs issued within this many seconds.
|
|
2711
2825
|
* This is safe because replay attacks require time for interception.
|
|
@@ -2773,9 +2887,8 @@ const jwtMiddleware = (options, schema, getJWT) => {
|
|
|
2773
2887
|
});
|
|
2774
2888
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2775
2889
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
ctx.context.logger.warn("[Dash] API key hash is invalid", apiKeyHash, expectedHash);
|
|
2890
|
+
if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
|
|
2891
|
+
ctx.context.logger.warn("[Dash] API key hash mismatch");
|
|
2779
2892
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2780
2893
|
}
|
|
2781
2894
|
if (!isRecentlyIssued(payload)) {
|
|
@@ -2828,7 +2941,7 @@ const jwtValidateMiddleware = (options) => {
|
|
|
2828
2941
|
});
|
|
2829
2942
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2830
2943
|
}
|
|
2831
|
-
if (apiKeyHash
|
|
2944
|
+
if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
|
|
2832
2945
|
ctx.context.logger.warn("[Dash] API key hash mismatch");
|
|
2833
2946
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2834
2947
|
}
|
|
@@ -2836,9 +2949,6 @@ const jwtValidateMiddleware = (options) => {
|
|
|
2836
2949
|
});
|
|
2837
2950
|
};
|
|
2838
2951
|
//#endregion
|
|
2839
|
-
//#region src/version.ts
|
|
2840
|
-
const PLUGIN_VERSION = "0.2.12";
|
|
2841
|
-
//#endregion
|
|
2842
2952
|
//#region src/routes/auth/config.ts
|
|
2843
2953
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
2844
2954
|
const EXACT_SENSITIVE_STRING_KEYS_LOWER = new Set([...new Set([
|
|
@@ -5717,7 +5827,7 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
|
|
|
5717
5827
|
}, async (ctx) => {
|
|
5718
5828
|
const { userId } = ctx.body;
|
|
5719
5829
|
if (!await ctx.context.internalAdapter.findUserById(userId)) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
5720
|
-
await ctx.context.internalAdapter.
|
|
5830
|
+
await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
5721
5831
|
return ctx.json({ success: true });
|
|
5722
5832
|
});
|
|
5723
5833
|
const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
|
|
@@ -5726,7 +5836,7 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
|
|
|
5726
5836
|
}, async (ctx) => {
|
|
5727
5837
|
const { userIds } = ctx.context.payload;
|
|
5728
5838
|
await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
|
|
5729
|
-
for (const userId of chunk) await ctx.context.internalAdapter.
|
|
5839
|
+
for (const userId of chunk) await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
5730
5840
|
}, { concurrency: 3 });
|
|
5731
5841
|
return ctx.json({
|
|
5732
5842
|
success: true,
|
|
@@ -5848,39 +5958,41 @@ async function resolveHostnameAddresses(hostname) {
|
|
|
5848
5958
|
return null;
|
|
5849
5959
|
}
|
|
5850
5960
|
}
|
|
5851
|
-
function validateUrlSync(
|
|
5852
|
-
const trimmed = rawUrl.trim();
|
|
5961
|
+
function validateUrlSync(input, options = {}) {
|
|
5853
5962
|
let parsed;
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5963
|
+
if (input instanceof URL) parsed = input;
|
|
5964
|
+
else {
|
|
5965
|
+
const trimmed = input.trim();
|
|
5966
|
+
try {
|
|
5967
|
+
parsed = new URL(trimmed);
|
|
5968
|
+
} catch {
|
|
5969
|
+
return {
|
|
5970
|
+
ok: false,
|
|
5971
|
+
reason: "invalid_url"
|
|
5972
|
+
};
|
|
5973
|
+
}
|
|
5862
5974
|
}
|
|
5863
5975
|
if (!isAllowedProtocol(parsed.protocol, options.allowedProtocols)) return {
|
|
5864
5976
|
ok: false,
|
|
5865
5977
|
reason: "disallowed_protocol",
|
|
5866
|
-
url:
|
|
5978
|
+
url: parsed,
|
|
5867
5979
|
protocol: parsed.protocol
|
|
5868
5980
|
};
|
|
5869
5981
|
if (parsed.username !== "" || parsed.password !== "") return {
|
|
5870
5982
|
ok: false,
|
|
5871
5983
|
reason: "embedded_credentials",
|
|
5872
|
-
url:
|
|
5984
|
+
url: parsed
|
|
5873
5985
|
};
|
|
5874
5986
|
if (isPrivateHost(parsed.hostname)) return {
|
|
5875
5987
|
ok: false,
|
|
5876
5988
|
reason: "private_host",
|
|
5877
|
-
url:
|
|
5989
|
+
url: parsed
|
|
5878
5990
|
};
|
|
5879
5991
|
if (options.allowedOrigins?.length) {
|
|
5880
5992
|
if (!isAllowedOrigin(parsed.origin, options.allowedOrigins)) return {
|
|
5881
5993
|
ok: false,
|
|
5882
5994
|
reason: "disallowed_origin",
|
|
5883
|
-
url:
|
|
5995
|
+
url: parsed,
|
|
5884
5996
|
hostname: parsed.hostname,
|
|
5885
5997
|
origin: parsed.origin
|
|
5886
5998
|
};
|
|
@@ -5891,8 +6003,8 @@ function validateUrlSync(rawUrl, options = {}) {
|
|
|
5891
6003
|
};
|
|
5892
6004
|
}
|
|
5893
6005
|
/** SSRF checks with DNS resolution when available. */
|
|
5894
|
-
async function validateUrl(
|
|
5895
|
-
const sync = validateUrlSync(
|
|
6006
|
+
async function validateUrl(input, options = {}) {
|
|
6007
|
+
const sync = validateUrlSync(input, options);
|
|
5896
6008
|
if (!sync.ok) return sync;
|
|
5897
6009
|
if (options.dns === false) return sync;
|
|
5898
6010
|
const hostname = sync.url.hostname;
|
|
@@ -5900,19 +6012,23 @@ async function validateUrl(rawUrl, options = {}) {
|
|
|
5900
6012
|
if (!isIpLiteralHostname(hostname) && !addresses?.length) return {
|
|
5901
6013
|
ok: false,
|
|
5902
6014
|
reason: "unresolved_host",
|
|
5903
|
-
url: sync.url
|
|
6015
|
+
url: sync.url,
|
|
5904
6016
|
hostname
|
|
5905
6017
|
};
|
|
5906
6018
|
if (addresses) {
|
|
5907
6019
|
for (const addr of addresses) if (isPrivateHost(addr)) return {
|
|
5908
6020
|
ok: false,
|
|
5909
6021
|
reason: "private_dns",
|
|
5910
|
-
url: sync.url
|
|
6022
|
+
url: sync.url,
|
|
5911
6023
|
hostname,
|
|
5912
6024
|
address: addr
|
|
5913
6025
|
};
|
|
5914
6026
|
}
|
|
5915
|
-
return
|
|
6027
|
+
return {
|
|
6028
|
+
ok: true,
|
|
6029
|
+
url: sync.url,
|
|
6030
|
+
...addresses ? { addresses } : {}
|
|
6031
|
+
};
|
|
5916
6032
|
}
|
|
5917
6033
|
function safeResolveUrl(raw, baseURL) {
|
|
5918
6034
|
const trim = raw.trim();
|
|
@@ -5928,12 +6044,6 @@ function safeResolveUrl(raw, baseURL) {
|
|
|
5928
6044
|
}
|
|
5929
6045
|
//#endregion
|
|
5930
6046
|
//#region src/validation/ssrf.ts
|
|
5931
|
-
/**
|
|
5932
|
-
* SSRF protection for dash plugin outbound fetches.
|
|
5933
|
-
*
|
|
5934
|
-
* Uses @infra/utils at build time; tsdown bundles it into published dist so
|
|
5935
|
-
* consumers do not install the private workspace package.
|
|
5936
|
-
*/
|
|
5937
6047
|
const REDIRECT_STATUSES = new Set([
|
|
5938
6048
|
301,
|
|
5939
6049
|
302,
|
|
@@ -5948,11 +6058,6 @@ var SsrfBlockedError = class extends Error {
|
|
|
5948
6058
|
function isSsrfBlockedError(error) {
|
|
5949
6059
|
return error instanceof SsrfBlockedError;
|
|
5950
6060
|
}
|
|
5951
|
-
/** Validates URL hostname literals and resolved DNS addresses. */
|
|
5952
|
-
async function parseAndValidateUrl(url) {
|
|
5953
|
-
const result = await validateUrl(url);
|
|
5954
|
-
return result.ok ? result.url : null;
|
|
5955
|
-
}
|
|
5956
6061
|
/**
|
|
5957
6062
|
* Fetch with SSRF checks on the initial URL and every redirect target.
|
|
5958
6063
|
*/
|
|
@@ -5960,8 +6065,9 @@ async function $outbound(url, options = {}) {
|
|
|
5960
6065
|
const { timeout, signal: callerSignal, ...fetchInit } = options;
|
|
5961
6066
|
let currentUrl = url;
|
|
5962
6067
|
for (let hop = 0;; hop++) {
|
|
5963
|
-
const
|
|
5964
|
-
if (!
|
|
6068
|
+
const result = await validateUrl(currentUrl);
|
|
6069
|
+
if (!result.ok) throw new SsrfBlockedError("Invalid or blocked URL");
|
|
6070
|
+
const validated = result.url;
|
|
5965
6071
|
const signals = [];
|
|
5966
6072
|
if (callerSignal) signals.push(callerSignal);
|
|
5967
6073
|
if (timeout !== void 0 && timeout > 0) signals.push(AbortSignal.timeout(timeout));
|
|
@@ -5976,7 +6082,7 @@ async function $outbound(url, options = {}) {
|
|
|
5976
6082
|
const location = response.headers.get("location");
|
|
5977
6083
|
const next = location ? safeResolveUrl(location, validated.href) : null;
|
|
5978
6084
|
if (!next) throw new SsrfBlockedError("Invalid redirect location");
|
|
5979
|
-
currentUrl = next
|
|
6085
|
+
currentUrl = next;
|
|
5980
6086
|
}
|
|
5981
6087
|
}
|
|
5982
6088
|
//#endregion
|
|
@@ -6091,23 +6197,19 @@ const oidcConfigSchema = z$1.object({
|
|
|
6091
6197
|
});
|
|
6092
6198
|
async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
6093
6199
|
let idpMetadataXml = samlConfig.idpMetadata?.metadata;
|
|
6094
|
-
if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) {
|
|
6095
|
-
const
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
if (e instanceof APIError$1) throw e;
|
|
6108
|
-
ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
|
|
6109
|
-
throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
|
|
6110
|
-
}
|
|
6200
|
+
if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) try {
|
|
6201
|
+
const metadataResponse = await $outbound(samlConfig.idpMetadata.metadataUrl, {
|
|
6202
|
+
method: "GET",
|
|
6203
|
+
headers: { Accept: "application/xml, text/xml" },
|
|
6204
|
+
timeout: 15e3
|
|
6205
|
+
});
|
|
6206
|
+
if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
|
|
6207
|
+
idpMetadataXml = await metadataResponse.text();
|
|
6208
|
+
} catch (e) {
|
|
6209
|
+
if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
|
|
6210
|
+
if (e instanceof APIError$1) throw e;
|
|
6211
|
+
ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
|
|
6212
|
+
throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
|
|
6111
6213
|
}
|
|
6112
6214
|
const metadataAlgorithmWarnings = [];
|
|
6113
6215
|
if (idpMetadataXml) {
|
|
@@ -6574,6 +6676,64 @@ const markSsoProviderDomainVerified = (options) => {
|
|
|
6574
6676
|
});
|
|
6575
6677
|
};
|
|
6576
6678
|
//#endregion
|
|
6679
|
+
//#region src/routes/two-factor/backup-codes.ts
|
|
6680
|
+
/** Mirrors better-auth/plugins/two-factor/backup-codes generateBackupCodesFn. */
|
|
6681
|
+
function generateBackupCodesPlain(options) {
|
|
6682
|
+
if (options?.customBackupCodesGenerate) return options.customBackupCodesGenerate();
|
|
6683
|
+
const amount = options?.amount ?? 10;
|
|
6684
|
+
const length = options?.length ?? 10;
|
|
6685
|
+
return Array.from({ length: amount }, () => {
|
|
6686
|
+
const code = generateRandomString(length, "a-z", "0-9", "A-Z");
|
|
6687
|
+
return `${code.slice(0, 5)}-${code.slice(5)}`;
|
|
6688
|
+
});
|
|
6689
|
+
}
|
|
6690
|
+
/** Mirrors better-auth/plugins/two-factor/backup-codes generateBackupCodes. */
|
|
6691
|
+
async function generateBackupCodes$1(secret, options) {
|
|
6692
|
+
const backupCodes = generateBackupCodesPlain(options);
|
|
6693
|
+
const json = JSON.stringify(backupCodes);
|
|
6694
|
+
const storeBackupCodes = options?.storeBackupCodes ?? "encrypted";
|
|
6695
|
+
if (storeBackupCodes === "encrypted") return {
|
|
6696
|
+
backupCodes,
|
|
6697
|
+
encryptedBackupCodes: await symmetricEncrypt({
|
|
6698
|
+
key: secret,
|
|
6699
|
+
data: json
|
|
6700
|
+
})
|
|
6701
|
+
};
|
|
6702
|
+
if (typeof storeBackupCodes === "object" && "encrypt" in storeBackupCodes) return {
|
|
6703
|
+
backupCodes,
|
|
6704
|
+
encryptedBackupCodes: await storeBackupCodes.encrypt(json)
|
|
6705
|
+
};
|
|
6706
|
+
return {
|
|
6707
|
+
backupCodes,
|
|
6708
|
+
encryptedBackupCodes: json
|
|
6709
|
+
};
|
|
6710
|
+
}
|
|
6711
|
+
function resolveBackupCodeOptions(twoFactorPlugin) {
|
|
6712
|
+
return {
|
|
6713
|
+
storeBackupCodes: "encrypted",
|
|
6714
|
+
...twoFactorPlugin?.options?.backupCodeOptions
|
|
6715
|
+
};
|
|
6716
|
+
}
|
|
6717
|
+
//#endregion
|
|
6718
|
+
//#region src/routes/two-factor/two-factor-options.ts
|
|
6719
|
+
function resolveTotpIssuer(twoFactorPlugin, appName) {
|
|
6720
|
+
const options = twoFactorPlugin?.options;
|
|
6721
|
+
return (options?.totpOptions)?.issuer ?? options?.issuer ?? appName ?? "BetterAuth";
|
|
6722
|
+
}
|
|
6723
|
+
function resolveTotpConfig(twoFactorPlugin) {
|
|
6724
|
+
const totpOptions = twoFactorPlugin?.options?.totpOptions;
|
|
6725
|
+
return {
|
|
6726
|
+
digits: totpOptions?.digits ?? 6,
|
|
6727
|
+
period: totpOptions?.period ?? 30
|
|
6728
|
+
};
|
|
6729
|
+
}
|
|
6730
|
+
function buildTotpUri(params) {
|
|
6731
|
+
return createOTP(params.secret, {
|
|
6732
|
+
digits: params.digits,
|
|
6733
|
+
period: params.period
|
|
6734
|
+
}).url(params.issuer, params.account);
|
|
6735
|
+
}
|
|
6736
|
+
//#endregion
|
|
6577
6737
|
//#region src/routes/two-factor/index.ts
|
|
6578
6738
|
const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor", {
|
|
6579
6739
|
method: "POST",
|
|
@@ -6582,31 +6742,37 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
|
|
|
6582
6742
|
const { userId } = ctx.context.payload;
|
|
6583
6743
|
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6584
6744
|
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6745
|
+
if (twoFactorPlugin.options?.totpOptions?.disable === true) throw new APIError("BAD_REQUEST", {
|
|
6746
|
+
message: "TOTP is not configured",
|
|
6747
|
+
code: "TOTP_NOT_CONFIGURED"
|
|
6748
|
+
});
|
|
6749
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6750
|
+
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6751
|
+
if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
6752
|
+
if (user.twoFactorEnabled === true) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
|
|
6585
6753
|
if (await ctx.context.adapter.findOne({
|
|
6586
|
-
model:
|
|
6754
|
+
model: twoFactorTable,
|
|
6587
6755
|
where: [{
|
|
6588
6756
|
field: "userId",
|
|
6589
6757
|
value: userId
|
|
6590
6758
|
}]
|
|
6591
|
-
}))
|
|
6759
|
+
})) await ctx.context.adapter.delete({
|
|
6760
|
+
model: twoFactorTable,
|
|
6761
|
+
where: [{
|
|
6762
|
+
field: "userId",
|
|
6763
|
+
value: userId
|
|
6764
|
+
}]
|
|
6765
|
+
});
|
|
6592
6766
|
const { generateRandomString, symmetricEncrypt } = await import("better-auth/crypto");
|
|
6593
|
-
const secret = generateRandomString(32
|
|
6594
|
-
const
|
|
6595
|
-
const backupCodeLength = 10;
|
|
6596
|
-
const backupCodes = [];
|
|
6597
|
-
for (let i = 0; i < backupCodeAmount; i++) backupCodes.push(generateId(backupCodeLength).toUpperCase());
|
|
6767
|
+
const secret = generateRandomString(32);
|
|
6768
|
+
const { backupCodes, encryptedBackupCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
|
|
6598
6769
|
const encryptedSecret = await symmetricEncrypt({
|
|
6599
6770
|
key: ctx.context.secret,
|
|
6600
6771
|
data: secret
|
|
6601
6772
|
});
|
|
6602
|
-
const encryptedBackupCodes = await symmetricEncrypt({
|
|
6603
|
-
key: ctx.context.secret,
|
|
6604
|
-
data: JSON.stringify(backupCodes)
|
|
6605
|
-
});
|
|
6606
6773
|
await ctx.context.adapter.create({
|
|
6607
|
-
model:
|
|
6774
|
+
model: twoFactorTable,
|
|
6608
6775
|
data: {
|
|
6609
|
-
id: generateId(32),
|
|
6610
6776
|
userId,
|
|
6611
6777
|
secret: encryptedSecret,
|
|
6612
6778
|
backupCodes: encryptedBackupCodes
|
|
@@ -6616,15 +6782,47 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
|
|
|
6616
6782
|
twoFactorEnabled: true,
|
|
6617
6783
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6618
6784
|
});
|
|
6619
|
-
const
|
|
6620
|
-
const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
|
|
6785
|
+
const totpConfig = resolveTotpConfig(twoFactorPlugin);
|
|
6621
6786
|
return {
|
|
6622
6787
|
success: true,
|
|
6623
|
-
totpURI:
|
|
6788
|
+
totpURI: buildTotpUri({
|
|
6789
|
+
secret,
|
|
6790
|
+
issuer: resolveTotpIssuer(twoFactorPlugin, ctx.context.appName),
|
|
6791
|
+
account: user?.email || userId,
|
|
6792
|
+
digits: totpConfig.digits,
|
|
6793
|
+
period: totpConfig.period
|
|
6794
|
+
}),
|
|
6624
6795
|
secret,
|
|
6625
6796
|
backupCodes
|
|
6626
6797
|
};
|
|
6627
6798
|
});
|
|
6799
|
+
const completeTwoFactorSetup = (options) => createAuthEndpoint("/dash/complete-two-factor-setup", {
|
|
6800
|
+
method: "POST",
|
|
6801
|
+
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6802
|
+
}, async (ctx) => {
|
|
6803
|
+
const { userId } = ctx.context.payload;
|
|
6804
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6805
|
+
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6806
|
+
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6807
|
+
if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
6808
|
+
if (user.twoFactorEnabled === true) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
|
|
6809
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6810
|
+
if (!await ctx.context.adapter.findOne({
|
|
6811
|
+
model: twoFactorTable,
|
|
6812
|
+
where: [{
|
|
6813
|
+
field: "userId",
|
|
6814
|
+
value: userId
|
|
6815
|
+
}]
|
|
6816
|
+
})) throw new APIError("BAD_REQUEST", {
|
|
6817
|
+
message: "Two-factor authentication setup has not been started",
|
|
6818
|
+
code: "TWO_FACTOR_SETUP_NOT_PENDING"
|
|
6819
|
+
});
|
|
6820
|
+
await ctx.context.internalAdapter.updateUser(userId, {
|
|
6821
|
+
twoFactorEnabled: true,
|
|
6822
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6823
|
+
});
|
|
6824
|
+
return { success: true };
|
|
6825
|
+
});
|
|
6628
6826
|
const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-factor-totp-uri", {
|
|
6629
6827
|
method: "POST",
|
|
6630
6828
|
metadata: { scope: "http" },
|
|
@@ -6633,8 +6831,13 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
|
|
|
6633
6831
|
const { userId } = ctx.context.payload;
|
|
6634
6832
|
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6635
6833
|
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6834
|
+
if (twoFactorPlugin.options?.totpOptions?.disable === true) throw new APIError("BAD_REQUEST", {
|
|
6835
|
+
message: "TOTP is not configured",
|
|
6836
|
+
code: "TOTP_NOT_CONFIGURED"
|
|
6837
|
+
});
|
|
6838
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6636
6839
|
const twoFactorRecord = await ctx.context.adapter.findOne({
|
|
6637
|
-
model:
|
|
6840
|
+
model: twoFactorTable,
|
|
6638
6841
|
where: [{
|
|
6639
6842
|
field: "userId",
|
|
6640
6843
|
value: userId
|
|
@@ -6650,8 +6853,14 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
|
|
|
6650
6853
|
});
|
|
6651
6854
|
} catch {}
|
|
6652
6855
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6653
|
-
const
|
|
6654
|
-
return { totpURI:
|
|
6856
|
+
const totpConfig = resolveTotpConfig(twoFactorPlugin);
|
|
6857
|
+
return { totpURI: buildTotpUri({
|
|
6858
|
+
secret,
|
|
6859
|
+
issuer: resolveTotpIssuer(twoFactorPlugin, ctx.context.appName),
|
|
6860
|
+
account: user?.email || userId,
|
|
6861
|
+
digits: totpConfig.digits,
|
|
6862
|
+
period: totpConfig.period
|
|
6863
|
+
}) };
|
|
6655
6864
|
});
|
|
6656
6865
|
const viewBackupCodes = (options) => createAuthEndpoint("/dash/view-backup-codes", {
|
|
6657
6866
|
method: "POST",
|
|
@@ -6664,9 +6873,11 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
|
|
|
6664
6873
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6665
6874
|
}, async (ctx) => {
|
|
6666
6875
|
const { userId } = ctx.context.payload;
|
|
6667
|
-
|
|
6876
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6877
|
+
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
|
|
6878
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6668
6879
|
await ctx.context.adapter.delete({
|
|
6669
|
-
model:
|
|
6880
|
+
model: twoFactorTable,
|
|
6670
6881
|
where: [{
|
|
6671
6882
|
field: "userId",
|
|
6672
6883
|
value: userId
|
|
@@ -6683,25 +6894,19 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
|
|
|
6683
6894
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6684
6895
|
}, async (ctx) => {
|
|
6685
6896
|
const { userId } = ctx.context.payload;
|
|
6897
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6898
|
+
const twoFactorTable = twoFactorPlugin?.options?.twoFactorTable ?? "twoFactor";
|
|
6686
6899
|
const twoFactorRecord = await ctx.context.adapter.findOne({
|
|
6687
|
-
model:
|
|
6900
|
+
model: twoFactorTable,
|
|
6688
6901
|
where: [{
|
|
6689
6902
|
field: "userId",
|
|
6690
6903
|
value: userId
|
|
6691
6904
|
}]
|
|
6692
6905
|
});
|
|
6693
6906
|
if (!twoFactorRecord) throw new APIError("NOT_FOUND", { message: "Two-factor authentication not set up for this user" });
|
|
6694
|
-
const
|
|
6695
|
-
const backupCodeLength = 10;
|
|
6696
|
-
const newBackupCodes = [];
|
|
6697
|
-
for (let i = 0; i < backupCodeAmount; i++) newBackupCodes.push(generateId(backupCodeLength).toUpperCase());
|
|
6698
|
-
const { symmetricEncrypt } = await import("better-auth/crypto");
|
|
6699
|
-
const encryptedCodes = await symmetricEncrypt({
|
|
6700
|
-
key: ctx.context.secret,
|
|
6701
|
-
data: JSON.stringify(newBackupCodes)
|
|
6702
|
-
});
|
|
6907
|
+
const { backupCodes: newBackupCodes, encryptedBackupCodes: encryptedCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
|
|
6703
6908
|
await ctx.context.adapter.update({
|
|
6704
|
-
model:
|
|
6909
|
+
model: twoFactorTable,
|
|
6705
6910
|
where: [{
|
|
6706
6911
|
field: "id",
|
|
6707
6912
|
value: twoFactorRecord.id
|
|
@@ -6711,6 +6916,20 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
|
|
|
6711
6916
|
return { backupCodes: newBackupCodes };
|
|
6712
6917
|
});
|
|
6713
6918
|
//#endregion
|
|
6919
|
+
//#region src/routes/two-factor/two-factor-status.ts
|
|
6920
|
+
async function resolveTwoFactorStatus(context, userId, user) {
|
|
6921
|
+
if (!hasPlugin(context, "two-factor")) return;
|
|
6922
|
+
if (user.twoFactorEnabled === true) return "enabled";
|
|
6923
|
+
const twoFactorTable = context.getPlugin("two-factor")?.options?.twoFactorTable ?? "twoFactor";
|
|
6924
|
+
return await context.adapter.findOne({
|
|
6925
|
+
model: twoFactorTable,
|
|
6926
|
+
where: [{
|
|
6927
|
+
field: "userId",
|
|
6928
|
+
value: userId
|
|
6929
|
+
}]
|
|
6930
|
+
}) ? "pending" : "disabled";
|
|
6931
|
+
}
|
|
6932
|
+
//#endregion
|
|
6714
6933
|
//#region src/routes/users/index.ts
|
|
6715
6934
|
function parseWhereClause(val) {
|
|
6716
6935
|
if (!val) return [];
|
|
@@ -7064,6 +7283,7 @@ const getUserDetails = (options) => {
|
|
|
7064
7283
|
} }
|
|
7065
7284
|
});
|
|
7066
7285
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
7286
|
+
const twoFactorStatus = await resolveTwoFactorStatus(ctx.context, userId, user);
|
|
7067
7287
|
if (minimal) return {
|
|
7068
7288
|
...user,
|
|
7069
7289
|
lastActiveAt: user.lastActiveAt ?? null,
|
|
@@ -7071,7 +7291,8 @@ const getUserDetails = (options) => {
|
|
|
7071
7291
|
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7072
7292
|
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
7073
7293
|
account: [],
|
|
7074
|
-
session: []
|
|
7294
|
+
session: [],
|
|
7295
|
+
twoFactorStatus
|
|
7075
7296
|
};
|
|
7076
7297
|
const activityTrackingEnabled = !!options.activityTracking?.enabled;
|
|
7077
7298
|
const sessions = secondaryStorageOnly ? await ctx.context.internalAdapter.listSessions(userId) : user.session || [];
|
|
@@ -7109,7 +7330,8 @@ const getUserDetails = (options) => {
|
|
|
7109
7330
|
lastActiveAt,
|
|
7110
7331
|
banned: hasAdminPlugin ? user.banned ?? false : false,
|
|
7111
7332
|
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7112
|
-
banExpires: hasAdminPlugin ? user.banExpires ?? null : null
|
|
7333
|
+
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
7334
|
+
twoFactorStatus
|
|
7113
7335
|
};
|
|
7114
7336
|
});
|
|
7115
7337
|
};
|
|
@@ -7651,7 +7873,7 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
|
|
|
7651
7873
|
banExpires: banExpires ? new Date(banExpires) : null,
|
|
7652
7874
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7653
7875
|
});
|
|
7654
|
-
await ctx.context.internalAdapter.
|
|
7876
|
+
await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
7655
7877
|
return { success: true };
|
|
7656
7878
|
});
|
|
7657
7879
|
const banManyUsers = (options) => {
|
|
@@ -7906,7 +8128,10 @@ function createSMSSender(config) {
|
|
|
7906
8128
|
if (!apiKey) logger.warn("[Dash] No API key provided for SMS sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
|
|
7907
8129
|
const $api = createFetch({
|
|
7908
8130
|
baseURL: apiUrl,
|
|
7909
|
-
headers: {
|
|
8131
|
+
headers: {
|
|
8132
|
+
"user-agent": INFRA_USER_AGENT,
|
|
8133
|
+
Authorization: `Bearer ${apiKey}`
|
|
8134
|
+
},
|
|
7910
8135
|
timeout: config?.apiTimeout ?? 3e3
|
|
7911
8136
|
});
|
|
7912
8137
|
/**
|
|
@@ -7981,6 +8206,13 @@ async function sendSMS(options, config) {
|
|
|
7981
8206
|
}
|
|
7982
8207
|
//#endregion
|
|
7983
8208
|
//#region src/index.ts
|
|
8209
|
+
async function getRequestLocation() {
|
|
8210
|
+
try {
|
|
8211
|
+
return (await getCurrentAuthContext()).context.location;
|
|
8212
|
+
} catch {
|
|
8213
|
+
return;
|
|
8214
|
+
}
|
|
8215
|
+
}
|
|
7984
8216
|
const dash = (options) => {
|
|
7985
8217
|
const processedBulkOperationContexts = /* @__PURE__ */ new WeakSet();
|
|
7986
8218
|
const opts = resolveDashOptions(options);
|
|
@@ -8005,6 +8237,7 @@ const dash = (options) => {
|
|
|
8005
8237
|
options: opts,
|
|
8006
8238
|
version: PLUGIN_VERSION,
|
|
8007
8239
|
init(ctx) {
|
|
8240
|
+
instrumentSocialProviders(ctx.socialProviders);
|
|
8008
8241
|
const organizationPlugin = ctx.getPlugin("organization");
|
|
8009
8242
|
if (organizationPlugin) {
|
|
8010
8243
|
const instrumentOrganizationHooks = (organizationPluginOptions) => {
|
|
@@ -8012,85 +8245,85 @@ const dash = (options) => {
|
|
|
8012
8245
|
const afterCreateOrganization = organizationHooks.afterCreateOrganization;
|
|
8013
8246
|
organizationHooks.afterCreateOrganization = async (...args) => {
|
|
8014
8247
|
const [{ organization, user }] = args;
|
|
8015
|
-
trackOrganizationCreated(organization, getOrganizationTriggerInfo(user));
|
|
8248
|
+
trackOrganizationCreated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8016
8249
|
if (afterCreateOrganization) return afterCreateOrganization(...args);
|
|
8017
8250
|
};
|
|
8018
8251
|
const afterUpdateOrganization = organizationHooks.afterUpdateOrganization;
|
|
8019
8252
|
organizationHooks.afterUpdateOrganization = async (...args) => {
|
|
8020
8253
|
const [{ organization, user }] = args;
|
|
8021
|
-
if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user));
|
|
8254
|
+
if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8022
8255
|
if (afterUpdateOrganization) return afterUpdateOrganization(...args);
|
|
8023
8256
|
};
|
|
8024
8257
|
const afterAddMember = organizationHooks.afterAddMember;
|
|
8025
8258
|
organizationHooks.afterAddMember = async (...args) => {
|
|
8026
8259
|
const [{ organization, member, user }] = args;
|
|
8027
|
-
trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user));
|
|
8260
|
+
trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8028
8261
|
if (afterAddMember) return afterAddMember(...args);
|
|
8029
8262
|
};
|
|
8030
8263
|
const afterRemoveMember = organizationHooks.afterRemoveMember;
|
|
8031
8264
|
organizationHooks.afterRemoveMember = async (...args) => {
|
|
8032
8265
|
const [{ organization, member, user }] = args;
|
|
8033
|
-
trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user));
|
|
8266
|
+
trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8034
8267
|
if (afterRemoveMember) return afterRemoveMember(...args);
|
|
8035
8268
|
};
|
|
8036
8269
|
const afterUpdateMemberRole = organizationHooks.afterUpdateMemberRole;
|
|
8037
8270
|
organizationHooks.afterUpdateMemberRole = async (...args) => {
|
|
8038
8271
|
const [{ organization, member, user, previousRole }] = args;
|
|
8039
|
-
trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user));
|
|
8272
|
+
trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8040
8273
|
if (afterUpdateMemberRole) return afterUpdateMemberRole(...args);
|
|
8041
8274
|
};
|
|
8042
8275
|
const afterCreateInvitation = organizationHooks.afterCreateInvitation;
|
|
8043
8276
|
organizationHooks.afterCreateInvitation = async (...args) => {
|
|
8044
8277
|
const [{ organization, invitation, inviter }] = args;
|
|
8045
|
-
trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter));
|
|
8278
|
+
trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter), await getRequestLocation());
|
|
8046
8279
|
if (afterCreateInvitation) return afterCreateInvitation(...args);
|
|
8047
8280
|
};
|
|
8048
8281
|
const afterAcceptInvitation = organizationHooks.afterAcceptInvitation;
|
|
8049
8282
|
organizationHooks.afterAcceptInvitation = async (...args) => {
|
|
8050
8283
|
const [{ organization, invitation, member, user }] = args;
|
|
8051
|
-
trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user));
|
|
8284
|
+
trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8052
8285
|
if (afterAcceptInvitation) return afterAcceptInvitation(...args);
|
|
8053
8286
|
};
|
|
8054
8287
|
const afterRejectInvitation = organizationHooks.afterRejectInvitation;
|
|
8055
8288
|
organizationHooks.afterRejectInvitation = async (...args) => {
|
|
8056
8289
|
const [{ organization, invitation, user }] = args;
|
|
8057
|
-
trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user));
|
|
8290
|
+
trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8058
8291
|
if (afterRejectInvitation) return afterRejectInvitation(...args);
|
|
8059
8292
|
};
|
|
8060
8293
|
const afterCancelInvitation = organizationHooks.afterCancelInvitation;
|
|
8061
8294
|
organizationHooks.afterCancelInvitation = async (...args) => {
|
|
8062
8295
|
const [{ organization, invitation, cancelledBy }] = args;
|
|
8063
|
-
trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy));
|
|
8296
|
+
trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy), await getRequestLocation());
|
|
8064
8297
|
if (afterCancelInvitation) return afterCancelInvitation(...args);
|
|
8065
8298
|
};
|
|
8066
8299
|
const afterCreateTeam = organizationHooks.afterCreateTeam;
|
|
8067
8300
|
organizationHooks.afterCreateTeam = async (...args) => {
|
|
8068
8301
|
const [{ organization, team, user }] = args;
|
|
8069
|
-
trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user));
|
|
8302
|
+
trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8070
8303
|
if (afterCreateTeam) return afterCreateTeam(...args);
|
|
8071
8304
|
};
|
|
8072
8305
|
const afterUpdateTeam = organizationHooks.afterUpdateTeam;
|
|
8073
8306
|
organizationHooks.afterUpdateTeam = async (...args) => {
|
|
8074
8307
|
const [{ organization, team, user }] = args;
|
|
8075
|
-
if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user));
|
|
8308
|
+
if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8076
8309
|
if (afterUpdateTeam) return afterUpdateTeam(...args);
|
|
8077
8310
|
};
|
|
8078
8311
|
const afterDeleteTeam = organizationHooks.afterDeleteTeam;
|
|
8079
8312
|
organizationHooks.afterDeleteTeam = async (...args) => {
|
|
8080
8313
|
const [{ organization, team, user }] = args;
|
|
8081
|
-
trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user));
|
|
8314
|
+
trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8082
8315
|
if (afterDeleteTeam) return afterDeleteTeam(...args);
|
|
8083
8316
|
};
|
|
8084
8317
|
const afterAddTeamMember = organizationHooks.afterAddTeamMember;
|
|
8085
8318
|
organizationHooks.afterAddTeamMember = async (...args) => {
|
|
8086
8319
|
const [{ organization, team, user, teamMember }] = args;
|
|
8087
|
-
trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user));
|
|
8320
|
+
trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8088
8321
|
if (afterAddTeamMember) return afterAddTeamMember(...args);
|
|
8089
8322
|
};
|
|
8090
8323
|
const afterRemoveTeamMember = organizationHooks.afterRemoveTeamMember;
|
|
8091
8324
|
organizationHooks.afterRemoveTeamMember = async (...args) => {
|
|
8092
8325
|
const [{ organization, team, user, teamMember }] = args;
|
|
8093
|
-
trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user));
|
|
8326
|
+
trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8094
8327
|
if (afterRemoveTeamMember) return afterRemoveTeamMember(...args);
|
|
8095
8328
|
};
|
|
8096
8329
|
};
|
|
@@ -8270,8 +8503,15 @@ const dash = (options) => {
|
|
|
8270
8503
|
before: [{
|
|
8271
8504
|
matcher: (ctx) => {
|
|
8272
8505
|
if (ctx.request?.method !== "GET") return true;
|
|
8273
|
-
|
|
8274
|
-
|
|
8506
|
+
return matchesAnyRoute(ctx.path, [
|
|
8507
|
+
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8508
|
+
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
8509
|
+
routes.DASH_IMPERSONATE_USER,
|
|
8510
|
+
routes.VERIFY_EMAIL,
|
|
8511
|
+
routes.MAGIC_LINK_VERIFY,
|
|
8512
|
+
routes.DASH_ACCEPT_INVITATION,
|
|
8513
|
+
routes.DASH_COMPLETE_INVITATION_SOCIAL
|
|
8514
|
+
]);
|
|
8275
8515
|
},
|
|
8276
8516
|
handler: createIdentificationMiddleware($kv)
|
|
8277
8517
|
}],
|
|
@@ -8279,16 +8519,20 @@ const dash = (options) => {
|
|
|
8279
8519
|
matcher: (ctx) => {
|
|
8280
8520
|
if (ctx.request?.method !== "GET") return true;
|
|
8281
8521
|
const path = new URL(ctx.request.url).pathname;
|
|
8282
|
-
return matchesAnyRoute(path, [
|
|
8522
|
+
return matchesAnyRoute(path, [
|
|
8523
|
+
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8524
|
+
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
8525
|
+
routes.DASH_IMPERSONATE_USER
|
|
8526
|
+
]);
|
|
8283
8527
|
},
|
|
8284
8528
|
handler: createAuthMiddleware(async (_ctx) => {
|
|
8285
8529
|
const ctx = _ctx;
|
|
8286
8530
|
const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
|
|
8287
|
-
if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx);
|
|
8531
|
+
if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
|
|
8288
8532
|
const body = ctx.body;
|
|
8289
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
|
|
8290
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
|
|
8291
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
8533
|
+
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger, ctx.context.location);
|
|
8534
|
+
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger, ctx.context.location);
|
|
8535
|
+
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger, ctx.context.location);
|
|
8292
8536
|
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
8293
8537
|
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
8294
8538
|
maxAge: 600,
|
|
@@ -8383,6 +8627,7 @@ const dash = (options) => {
|
|
|
8383
8627
|
dashSendManyVerificationEmails: sendManyVerificationEmails(settings),
|
|
8384
8628
|
dashSendResetPasswordEmail: sendResetPasswordEmail(settings),
|
|
8385
8629
|
dashEnableTwoFactor: enableTwoFactor(settings),
|
|
8630
|
+
dashCompleteTwoFactorSetup: completeTwoFactorSetup(settings),
|
|
8386
8631
|
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(settings),
|
|
8387
8632
|
dashViewBackupCodes: viewBackupCodes(settings),
|
|
8388
8633
|
dashDisableTwoFactor: disableTwoFactor(settings),
|