@better-auth/infra 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +50 -8
- package/dist/index.mjs +434 -294
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { n as INFRA_API_URL, r as INFRA_KV_URL } from "./constants-CvriWQVc.mjs";
|
|
2
2
|
import { n as createKV, t as createAPI } from "./fetch-DiAhoiKA.mjs";
|
|
3
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
|
+
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
4
5
|
import { APIError, generateId, getAuthTables, logger, parseState } 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";
|
|
@@ -108,6 +108,7 @@ const routes = {
|
|
|
108
108
|
DASH_ROUTE: "/dash",
|
|
109
109
|
DASH_UPDATE_USER: "/dash/update-user",
|
|
110
110
|
DASH_REVOKE_SESSIONS_ALL: "/dash/sessions/revoke-all",
|
|
111
|
+
DASH_IMPERSONATE_USER: "/dash/impersonate-user",
|
|
111
112
|
DASH_BAN_USER: "/dash/ban-user",
|
|
112
113
|
DASH_UNBAN_USER: "/dash/unban-user",
|
|
113
114
|
ADMIN_ROUTE: "/admin",
|
|
@@ -425,7 +426,7 @@ const initSessionEvents = (tracker) => {
|
|
|
425
426
|
};
|
|
426
427
|
ctx.context.runInBackground(track());
|
|
427
428
|
};
|
|
428
|
-
const trackSessionRevokedAll = (session, trigger, ctx,
|
|
429
|
+
const trackSessionRevokedAll = (session, trigger, ctx, location) => {
|
|
429
430
|
const track = async () => {
|
|
430
431
|
const user = await getUserById(session.userId, ctx);
|
|
431
432
|
trackEvent({
|
|
@@ -438,7 +439,11 @@ const initSessionEvents = (tracker) => {
|
|
|
438
439
|
userEmail: user?.email ?? "unknown",
|
|
439
440
|
triggeredBy: trigger.triggeredBy,
|
|
440
441
|
triggerContext: trigger.triggerContext
|
|
441
|
-
}
|
|
442
|
+
},
|
|
443
|
+
ipAddress: location?.ipAddress,
|
|
444
|
+
city: location?.city,
|
|
445
|
+
country: location?.country,
|
|
446
|
+
countryCode: location?.countryCode
|
|
442
447
|
});
|
|
443
448
|
};
|
|
444
449
|
ctx.context.runInBackground(track());
|
|
@@ -527,7 +532,7 @@ const initSessionEvents = (tracker) => {
|
|
|
527
532
|
};
|
|
528
533
|
ctx.context.runInBackground(track());
|
|
529
534
|
};
|
|
530
|
-
const trackEmailVerificationSent = (session, user, trigger,
|
|
535
|
+
const trackEmailVerificationSent = (session, user, trigger, location) => {
|
|
531
536
|
trackEvent({
|
|
532
537
|
eventKey: session.userId,
|
|
533
538
|
eventType: EVENT_TYPES.EMAIL_VERIFICATION_SENT,
|
|
@@ -539,10 +544,14 @@ const initSessionEvents = (tracker) => {
|
|
|
539
544
|
sessionId: session.id,
|
|
540
545
|
triggeredBy: trigger.triggeredBy,
|
|
541
546
|
triggerContext: trigger.triggerContext
|
|
542
|
-
}
|
|
547
|
+
},
|
|
548
|
+
ipAddress: location?.ipAddress,
|
|
549
|
+
city: location?.city,
|
|
550
|
+
country: location?.country,
|
|
551
|
+
countryCode: location?.countryCode
|
|
543
552
|
});
|
|
544
553
|
};
|
|
545
|
-
const trackEmailSignInAttempt = (ctx, trigger) => {
|
|
554
|
+
const trackEmailSignInAttempt = (ctx, trigger, location) => {
|
|
546
555
|
const track = async () => {
|
|
547
556
|
const user = await getUserByEmail(ctx.body.email, ctx);
|
|
548
557
|
trackEvent({
|
|
@@ -556,12 +565,16 @@ const initSessionEvents = (tracker) => {
|
|
|
556
565
|
loginMethod: getLoginMethod(ctx),
|
|
557
566
|
triggeredBy: user?.id ?? trigger.triggeredBy,
|
|
558
567
|
triggerContext: trigger.triggerContext
|
|
559
|
-
}
|
|
568
|
+
},
|
|
569
|
+
ipAddress: location?.ipAddress,
|
|
570
|
+
city: location?.city,
|
|
571
|
+
country: location?.country,
|
|
572
|
+
countryCode: location?.countryCode
|
|
560
573
|
});
|
|
561
574
|
};
|
|
562
575
|
ctx.context.runInBackground(track());
|
|
563
576
|
};
|
|
564
|
-
const trackSocialSignInAttempt = (ctx, trigger) => {
|
|
577
|
+
const trackSocialSignInAttempt = (ctx, trigger, location) => {
|
|
565
578
|
const track = async () => {
|
|
566
579
|
const user = await getUserByIdToken(ctx.body.provider, ctx.body.idToken, ctx);
|
|
567
580
|
trackEvent({
|
|
@@ -575,12 +588,16 @@ const initSessionEvents = (tracker) => {
|
|
|
575
588
|
loginMethod: getLoginMethod(ctx),
|
|
576
589
|
triggeredBy: user?.user.id ?? trigger.triggeredBy,
|
|
577
590
|
triggerContext: trigger.triggerContext
|
|
578
|
-
}
|
|
591
|
+
},
|
|
592
|
+
ipAddress: location?.ipAddress,
|
|
593
|
+
city: location?.city,
|
|
594
|
+
country: location?.country,
|
|
595
|
+
countryCode: location?.countryCode
|
|
579
596
|
});
|
|
580
597
|
};
|
|
581
598
|
ctx.context.runInBackground(track());
|
|
582
599
|
};
|
|
583
|
-
const trackSocialSignInRedirectionAttempt = (ctx, trigger) => {
|
|
600
|
+
const trackSocialSignInRedirectionAttempt = (ctx, trigger, location) => {
|
|
584
601
|
const track = async () => {
|
|
585
602
|
const user = await getUserByAuthorizationCode(tryDecode(ctx.params?.id), ctx);
|
|
586
603
|
trackEvent({
|
|
@@ -594,7 +611,11 @@ const initSessionEvents = (tracker) => {
|
|
|
594
611
|
loginMethod: getLoginMethod(ctx),
|
|
595
612
|
triggeredBy: trigger.triggeredBy,
|
|
596
613
|
triggerContext: trigger.triggerContext
|
|
597
|
-
}
|
|
614
|
+
},
|
|
615
|
+
ipAddress: location?.ipAddress,
|
|
616
|
+
city: location?.city,
|
|
617
|
+
country: location?.country,
|
|
618
|
+
countryCode: location?.countryCode
|
|
598
619
|
});
|
|
599
620
|
};
|
|
600
621
|
ctx.context.runInBackground(track());
|
|
@@ -942,45 +963,67 @@ function extractIdentificationHeaders(request) {
|
|
|
942
963
|
};
|
|
943
964
|
}
|
|
944
965
|
/**
|
|
966
|
+
* Visitor ID for security enforcement from a loaded identification record —
|
|
967
|
+
* not a client-supplied X-Visitor-Id header alone.
|
|
968
|
+
*/
|
|
969
|
+
function resolveSecurityVisitorId(headerVisitorId, identification) {
|
|
970
|
+
const identified = identification?.visitorId ?? null;
|
|
971
|
+
if (!identified) return null;
|
|
972
|
+
if (headerVisitorId && headerVisitorId !== identified) logger.warn("[Sentinel] X-Visitor-Id does not match identification; using identification visitorId for security checks.");
|
|
973
|
+
return identified;
|
|
974
|
+
}
|
|
975
|
+
/** IP for security checks: prefer identify record, then request-derived fallback. */
|
|
976
|
+
function resolveSecurityIp(identification, location) {
|
|
977
|
+
return identification?.ip ?? location?.ipAddress ?? null;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Visitor id for observability and failed-login tracking when identification
|
|
981
|
+
* is unavailable. Prefer bound visitor id, then a stable IP bucket; header is
|
|
982
|
+
* legacy fallback only — not used for enforcement.
|
|
983
|
+
*/
|
|
984
|
+
function resolveUntrustedVisitorId(visitorId, ip, headerVisitorId) {
|
|
985
|
+
if (visitorId) return visitorId;
|
|
986
|
+
if (ip) return `ip:${ip}`;
|
|
987
|
+
return headerVisitorId;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
945
990
|
* Early middleware that loads identification data.
|
|
946
991
|
*
|
|
947
992
|
* @param $kv — KV client from {@link createKV} for the same plugin options (one instance per plugin).
|
|
948
993
|
*/
|
|
949
994
|
function createIdentificationMiddleware($kv) {
|
|
950
995
|
return createAuthMiddleware(async (ctx) => {
|
|
951
|
-
const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
|
|
996
|
+
const { visitorId: headerVisitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
|
|
952
997
|
const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
|
|
953
|
-
ctx.context.visitorId = visitorId;
|
|
954
998
|
ctx.context.requestId = requestId;
|
|
955
999
|
if (requestId) {
|
|
956
1000
|
if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv) ?? null;
|
|
957
1001
|
} else ctx.context.identification = null;
|
|
958
|
-
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
959
|
-
if (ipConfig?.disableIpTracking === true) {
|
|
960
|
-
ctx.context.location = void 0;
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
1002
|
const identification = ctx.context.identification;
|
|
964
|
-
|
|
1003
|
+
const visitorId = resolveSecurityVisitorId(headerVisitorId, identification);
|
|
1004
|
+
ctx.context.visitorId = visitorId;
|
|
1005
|
+
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
1006
|
+
let location;
|
|
1007
|
+
if (ipConfig?.disableIpTracking === true) location = void 0;
|
|
1008
|
+
else if (requestId && identification) {
|
|
965
1009
|
const loc = getLocation(identification);
|
|
966
|
-
|
|
1010
|
+
location = {
|
|
967
1011
|
ipAddress: identification.ip || void 0,
|
|
968
1012
|
city: loc?.city || void 0,
|
|
969
1013
|
country: loc?.country?.name || void 0,
|
|
970
1014
|
countryCode: loc?.country?.code || void 0
|
|
971
1015
|
};
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if (ipAddress) {
|
|
976
|
-
const countryCode = getCountryCodeFromRequest(ctx.request);
|
|
977
|
-
ctx.context.location = {
|
|
1016
|
+
} else {
|
|
1017
|
+
const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
|
|
1018
|
+
if (ipAddress) location = {
|
|
978
1019
|
ipAddress,
|
|
979
|
-
countryCode
|
|
1020
|
+
countryCode: getCountryCodeFromRequest(ctx.request)
|
|
980
1021
|
};
|
|
981
|
-
|
|
1022
|
+
else location = void 0;
|
|
982
1023
|
}
|
|
983
|
-
ctx.context.location =
|
|
1024
|
+
ctx.context.location = location;
|
|
1025
|
+
ctx.context.ip = resolveSecurityIp(identification, location);
|
|
1026
|
+
ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
|
|
984
1027
|
});
|
|
985
1028
|
}
|
|
986
1029
|
/**
|
|
@@ -1024,7 +1067,13 @@ function getEvaluation(ctx) {
|
|
|
1024
1067
|
function recordCheck(ctx, name) {
|
|
1025
1068
|
getEvaluation(ctx).checks.add(name);
|
|
1026
1069
|
}
|
|
1027
|
-
/**
|
|
1070
|
+
/**
|
|
1071
|
+
* Call immediately before throwing so the buffer reflects the final outcome.
|
|
1072
|
+
*
|
|
1073
|
+
* `details.reason`, when set, is surfaced as the top-level dashboard reason
|
|
1074
|
+
* (see `emitEvaluation`) — so it must be a renderable reason, not a free-form
|
|
1075
|
+
* value. Put internal sub-states under a different key (e.g. `phase`).
|
|
1076
|
+
*/
|
|
1028
1077
|
function setOutcome(ctx, outcome, triggeredBy, details) {
|
|
1029
1078
|
const ev = getEvaluation(ctx);
|
|
1030
1079
|
ev.outcome = outcome;
|
|
@@ -1052,6 +1101,7 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
|
|
|
1052
1101
|
outcome: ev.outcome,
|
|
1053
1102
|
evaluatedChecks: [...ev.checks].sort(),
|
|
1054
1103
|
triggeredBy: ev.triggeredBy,
|
|
1104
|
+
reason: ev.details?.reason ?? ev.triggeredBy,
|
|
1055
1105
|
identifier: options.identifier,
|
|
1056
1106
|
path,
|
|
1057
1107
|
userAgent: options.userAgent,
|
|
@@ -1171,9 +1221,11 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1171
1221
|
logger.error("[Dash] Clear failed attempts error:", error);
|
|
1172
1222
|
}
|
|
1173
1223
|
},
|
|
1174
|
-
async isBlocked(visitorId) {
|
|
1224
|
+
async isBlocked(visitorId, ip) {
|
|
1175
1225
|
try {
|
|
1176
|
-
|
|
1226
|
+
const params = new URLSearchParams({ visitorId });
|
|
1227
|
+
if (ip) params.set("ip", ip);
|
|
1228
|
+
return (await $api(`/security/is-blocked?${params.toString()}`, { method: "GET" })).blocked ?? false;
|
|
1177
1229
|
} catch (error) {
|
|
1178
1230
|
logger.warn("[Dash] Security is-blocked check failed:", error);
|
|
1179
1231
|
return false;
|
|
@@ -1210,7 +1262,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1210
1262
|
return "";
|
|
1211
1263
|
}
|
|
1212
1264
|
},
|
|
1213
|
-
async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null) {
|
|
1265
|
+
async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null, powSolution) {
|
|
1214
1266
|
if (!options.impossibleTravel?.enabled || !currentLocation) return null;
|
|
1215
1267
|
try {
|
|
1216
1268
|
const data = await $api("/security/impossible-travel", {
|
|
@@ -1220,7 +1272,8 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1220
1272
|
visitorId,
|
|
1221
1273
|
location: currentLocation,
|
|
1222
1274
|
ip,
|
|
1223
|
-
config: options
|
|
1275
|
+
config: options,
|
|
1276
|
+
...powSolution ? { powSolution } : {}
|
|
1224
1277
|
}
|
|
1225
1278
|
});
|
|
1226
1279
|
if (data.isImpossible) {
|
|
@@ -1483,52 +1536,47 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1483
1536
|
}
|
|
1484
1537
|
//#endregion
|
|
1485
1538
|
//#region src/validation/matchers.ts
|
|
1486
|
-
|
|
1539
|
+
/**
|
|
1540
|
+
* Routes where a new or changed email address enters the system. Deliverability
|
|
1541
|
+
* and disposable validation runs here so invalid addresses are caught at the
|
|
1542
|
+
* source.
|
|
1543
|
+
*/
|
|
1544
|
+
const registrationPaths = [
|
|
1487
1545
|
"/sign-up/email",
|
|
1488
|
-
"/email-otp/verify-email",
|
|
1489
|
-
"/sign-in/email-otp",
|
|
1490
|
-
"/sign-in/magic-link",
|
|
1491
|
-
"/sign-in/email",
|
|
1492
|
-
"/forget-password/email-otp",
|
|
1493
|
-
"/email-otp/reset-password",
|
|
1494
|
-
"/email-otp/create-verification-otp",
|
|
1495
|
-
"/email-otp/get-verification-otp",
|
|
1496
|
-
"/email-otp/send-verification-otp",
|
|
1497
|
-
"/forget-password",
|
|
1498
|
-
"/request-password-reset",
|
|
1499
|
-
"/send-verification-email",
|
|
1500
1546
|
"/change-email",
|
|
1501
1547
|
"/organization/invite-member",
|
|
1502
1548
|
"/dash/organization/invite-member",
|
|
1503
1549
|
"/dash/create-user",
|
|
1504
1550
|
"/dash/update-user"
|
|
1505
|
-
]
|
|
1551
|
+
];
|
|
1506
1552
|
/**
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
* '/sign-in/email-otp',
|
|
1511
|
-
* '/sign-in/magic-link',
|
|
1512
|
-
* '/sign-in/email',
|
|
1513
|
-
* '/forget-password/email-otp',
|
|
1514
|
-
* '/email-otp/reset-password',
|
|
1515
|
-
* '/email-otp/create-verification-otp',
|
|
1516
|
-
* '/email-otp/get-verification-otp',
|
|
1517
|
-
* '/email-otp/send-verification-otp',
|
|
1518
|
-
* '/forget-password',
|
|
1519
|
-
* '/request-password-reset',
|
|
1520
|
-
* '/send-verification-email',
|
|
1521
|
-
* '/change-email',
|
|
1522
|
-
* '/organization/invite-member',
|
|
1523
|
-
* '/dash/organization/invite-member',
|
|
1524
|
-
* '/dash/create-user',
|
|
1525
|
-
* '/dash/update-user'
|
|
1526
|
-
* ]`.
|
|
1527
|
-
* @param context Request context
|
|
1528
|
-
* @param context.path Request path
|
|
1529
|
-
* @returns boolean
|
|
1553
|
+
* Authentication and account-recovery routes. These act on an already-known
|
|
1554
|
+
* address, so only the syntax check runs — a deliverability false-positive must
|
|
1555
|
+
* never lock out an existing user (better-auth#9803).
|
|
1530
1556
|
*/
|
|
1557
|
+
const authPaths = [
|
|
1558
|
+
"/sign-in/email",
|
|
1559
|
+
"/sign-in/email-otp",
|
|
1560
|
+
"/sign-in/magic-link",
|
|
1561
|
+
"/forget-password",
|
|
1562
|
+
"/forget-password/email-otp",
|
|
1563
|
+
"/request-password-reset",
|
|
1564
|
+
"/send-verification-email",
|
|
1565
|
+
"/email-otp/verify-email",
|
|
1566
|
+
"/email-otp/reset-password",
|
|
1567
|
+
"/email-otp/create-verification-otp",
|
|
1568
|
+
"/email-otp/get-verification-otp",
|
|
1569
|
+
"/email-otp/send-verification-otp"
|
|
1570
|
+
];
|
|
1571
|
+
const registration = new Set(registrationPaths);
|
|
1572
|
+
const all = new Set([...registrationPaths, ...authPaths]);
|
|
1573
|
+
/** Path carries an email we hook for normalization + syntax validation. */
|
|
1531
1574
|
const allEmail = ({ path }) => !!path && all.has(path);
|
|
1575
|
+
/**
|
|
1576
|
+
* Path introduces a new or changed address — run full deliverability/disposable
|
|
1577
|
+
* validation. Authentication routes are intentionally excluded.
|
|
1578
|
+
*/
|
|
1579
|
+
const registrationEmail = ({ path }) => !!path && registration.has(path);
|
|
1532
1580
|
//#endregion
|
|
1533
1581
|
//#region src/validation/email.ts
|
|
1534
1582
|
/**
|
|
@@ -1624,7 +1672,7 @@ async validate(email, checkMx = true) {
|
|
|
1624
1672
|
policy
|
|
1625
1673
|
};
|
|
1626
1674
|
try {
|
|
1627
|
-
const { data } = await $kv("/email/validate", {
|
|
1675
|
+
const { data, error } = await $kv("/email/validate", {
|
|
1628
1676
|
method: "POST",
|
|
1629
1677
|
body: {
|
|
1630
1678
|
email: trimmed,
|
|
@@ -1632,11 +1680,15 @@ async validate(email, checkMx = true) {
|
|
|
1632
1680
|
strictness: policy.strictness
|
|
1633
1681
|
}
|
|
1634
1682
|
});
|
|
1683
|
+
if (error || !data) {
|
|
1684
|
+
logger.warn("[Dash] Email validation unavailable, allowing email:", error);
|
|
1685
|
+
return {
|
|
1686
|
+
valid: true,
|
|
1687
|
+
policy
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1635
1690
|
return {
|
|
1636
|
-
...data
|
|
1637
|
-
valid: false,
|
|
1638
|
-
reason: "invalid_format"
|
|
1639
|
-
},
|
|
1691
|
+
...data,
|
|
1640
1692
|
policy
|
|
1641
1693
|
};
|
|
1642
1694
|
} catch (error) {
|
|
@@ -1649,6 +1701,26 @@ async validate(email, checkMx = true) {
|
|
|
1649
1701
|
} };
|
|
1650
1702
|
}
|
|
1651
1703
|
/**
|
|
1704
|
+
* Map a raw validation result to a single dashboard reason and user-facing
|
|
1705
|
+
* message. One source so the recorded reason and the thrown message can't drift,
|
|
1706
|
+
* and a block always carries a populated reason (better-auth#9803).
|
|
1707
|
+
*/
|
|
1708
|
+
function resolveEmailBlock(result) {
|
|
1709
|
+
const raw = result.reason ?? "";
|
|
1710
|
+
if (result.disposable || raw === "blocklist" || raw.startsWith("known_disposable")) return {
|
|
1711
|
+
reason: "disposable_email",
|
|
1712
|
+
message: "Disposable email addresses are not allowed"
|
|
1713
|
+
};
|
|
1714
|
+
if (raw === "no_mx_records") return {
|
|
1715
|
+
reason: "no_mx_records",
|
|
1716
|
+
message: "This email domain cannot receive emails"
|
|
1717
|
+
};
|
|
1718
|
+
return {
|
|
1719
|
+
reason: "invalid_email",
|
|
1720
|
+
message: "This email address appears to be invalid"
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1652
1724
|
* Basic local email format validation (fallback)
|
|
1653
1725
|
*/
|
|
1654
1726
|
function isValidEmailFormatLocal(email) {
|
|
@@ -1714,14 +1786,15 @@ function createEmailValidationHook(validator, trackEvent) {
|
|
|
1714
1786
|
if (!isValidEmailFormatLocal(trimmed)) {
|
|
1715
1787
|
if (trackEvent) {
|
|
1716
1788
|
setOutcome(ctx, "blocked", "email_validity", {
|
|
1717
|
-
reason: "
|
|
1789
|
+
reason: "invalid_email",
|
|
1790
|
+
rawReason: "invalid_format",
|
|
1718
1791
|
email: trimmed
|
|
1719
1792
|
});
|
|
1720
1793
|
emitEvaluation(ctx, trackEvent, { identifier: trimmed });
|
|
1721
1794
|
}
|
|
1722
1795
|
throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
|
|
1723
1796
|
}
|
|
1724
|
-
if (validator) {
|
|
1797
|
+
if (validator && registrationEmail(ctx)) {
|
|
1725
1798
|
const result = await validator.validate(trimmed);
|
|
1726
1799
|
const policy = result.policy;
|
|
1727
1800
|
if (!policy?.enabled) return;
|
|
@@ -1732,10 +1805,11 @@ function createEmailValidationHook(validator, trackEvent) {
|
|
|
1732
1805
|
const action = policy.action;
|
|
1733
1806
|
if (!result.valid) {
|
|
1734
1807
|
if (action === "allow") return;
|
|
1735
|
-
const
|
|
1808
|
+
const { reason, message } = resolveEmailBlock(result);
|
|
1736
1809
|
if (trackEvent) {
|
|
1737
1810
|
setOutcome(ctx, "blocked", "email_validity", {
|
|
1738
|
-
reason
|
|
1811
|
+
reason,
|
|
1812
|
+
rawReason: result.reason,
|
|
1739
1813
|
confidence: result.confidence,
|
|
1740
1814
|
email: trimmed
|
|
1741
1815
|
});
|
|
@@ -2006,6 +2080,7 @@ function throwChallengeError(challenge, reason, message = "Please complete a sec
|
|
|
2006
2080
|
}
|
|
2007
2081
|
/** Emits the `security_check` event inline before throwing — the after-hook won't run on throw. */
|
|
2008
2082
|
async function handleSecurityVerdict(verdict, hookCtx, checkCtx, trackEvent, securityService) {
|
|
2083
|
+
if (verdict.powVerified) recordCheck(hookCtx, "pow_challenge");
|
|
2009
2084
|
if (verdict.action === "allow") return;
|
|
2010
2085
|
const reason = verdict.reason || "unknown";
|
|
2011
2086
|
if (verdict.action === "challenge" && checkCtx.visitorId) {
|
|
@@ -2040,15 +2115,15 @@ async function handleSecurityVerdict(verdict, hookCtx, checkCtx, trackEvent, sec
|
|
|
2040
2115
|
* Single API call from the plugin; server runs all configured rules and returns
|
|
2041
2116
|
* one verdict — counts as one billable check.
|
|
2042
2117
|
*/
|
|
2043
|
-
async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent,
|
|
2044
|
-
if (powVerified) return;
|
|
2118
|
+
async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent, powSolution) {
|
|
2045
2119
|
recordCheck(hookCtx, "server_security_check");
|
|
2046
2120
|
await handleSecurityVerdict(await securityService.checkSecurity({
|
|
2047
2121
|
visitorId: checkCtx.visitorId,
|
|
2048
2122
|
requestId: checkCtx.identification?.requestId || null,
|
|
2049
|
-
ip: checkCtx.
|
|
2123
|
+
ip: checkCtx.ip,
|
|
2050
2124
|
path: checkCtx.path,
|
|
2051
|
-
identifier: checkCtx.identifier
|
|
2125
|
+
identifier: checkCtx.identifier,
|
|
2126
|
+
powSolution: powSolution ?? void 0
|
|
2052
2127
|
}), hookCtx, checkCtx, trackEvent, securityService);
|
|
2053
2128
|
}
|
|
2054
2129
|
//#endregion
|
|
@@ -2137,7 +2212,9 @@ const sentinel = (options) => {
|
|
|
2137
2212
|
const identification = ctx.context.identification;
|
|
2138
2213
|
if (session.userId && identification?.location && visitorId) {
|
|
2139
2214
|
recordCheck(ctx, "impossible_travel");
|
|
2140
|
-
const
|
|
2215
|
+
const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
|
|
2216
|
+
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution);
|
|
2217
|
+
if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
|
|
2141
2218
|
if (travelCheck?.isImpossible) {
|
|
2142
2219
|
if (travelCheck.action === "block") {
|
|
2143
2220
|
setOutcome(ctx, "blocked", "impossible_travel", {
|
|
@@ -2228,16 +2305,6 @@ const sentinel = (options) => {
|
|
|
2228
2305
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2229
2306
|
handler: createIdentificationMiddleware($kv)
|
|
2230
2307
|
},
|
|
2231
|
-
{
|
|
2232
|
-
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2233
|
-
handler: createAuthMiddleware(async (ctx) => {
|
|
2234
|
-
const visitorId = ctx.context.visitorId;
|
|
2235
|
-
const powSolution = ctx.headers?.get?.("X-PoW-Solution");
|
|
2236
|
-
if (visitorId && powSolution) {
|
|
2237
|
-
if ((await securityService.verifyPoWSolution(visitorId, powSolution)).valid) ctx.context.powVerified = true;
|
|
2238
|
-
}
|
|
2239
|
-
})
|
|
2240
|
-
},
|
|
2241
2308
|
...emailHooks.before,
|
|
2242
2309
|
...phoneValidationHooks.before,
|
|
2243
2310
|
{
|
|
@@ -2245,6 +2312,7 @@ const sentinel = (options) => {
|
|
|
2245
2312
|
handler: createAuthMiddleware(async (ctx) => {
|
|
2246
2313
|
const visitorId = ctx.context.visitorId;
|
|
2247
2314
|
const identification = ctx.context.identification;
|
|
2315
|
+
const ip = ctx.context.ip;
|
|
2248
2316
|
const isSignIn = matchesAnyRoute(ctx.path, [
|
|
2249
2317
|
routes.SIGN_IN_EMAIL,
|
|
2250
2318
|
routes.SIGN_IN_USERNAME,
|
|
@@ -2275,14 +2343,13 @@ const sentinel = (options) => {
|
|
|
2275
2343
|
if (!(isSignIn || isSignUp || isPasswordReset || isTwoFactor || isOtpSend || isMagicLinkVerify || isOrgCreate || isSensitiveAction)) return;
|
|
2276
2344
|
const requestBody = ctx.body;
|
|
2277
2345
|
const identifier = requestBody?.email || requestBody?.phone || requestBody?.username || void 0;
|
|
2278
|
-
const
|
|
2279
|
-
if (visitorId
|
|
2280
|
-
if (visitorId) {
|
|
2346
|
+
const powSolution = ctx.headers?.get?.("X-PoW-Solution");
|
|
2347
|
+
if (visitorId || ip) {
|
|
2281
2348
|
recordCheck(ctx, "credential_stuffing");
|
|
2282
|
-
if (await securityService.isBlocked(visitorId)) {
|
|
2349
|
+
if (await securityService.isBlocked(visitorId ?? "", ip)) {
|
|
2283
2350
|
setOutcome(ctx, "blocked", "credential_stuffing", {
|
|
2284
|
-
|
|
2285
|
-
visitorId,
|
|
2351
|
+
phase: "cooldown_active",
|
|
2352
|
+
visitorId: ctx.context.untrustedVisitorId ?? void 0,
|
|
2286
2353
|
confidence: 1
|
|
2287
2354
|
});
|
|
2288
2355
|
emitEvaluation(ctx, trackEvent, {
|
|
@@ -2297,8 +2364,9 @@ const sentinel = (options) => {
|
|
|
2297
2364
|
identifier,
|
|
2298
2365
|
visitorId,
|
|
2299
2366
|
identification,
|
|
2367
|
+
ip: ctx.context.ip,
|
|
2300
2368
|
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2301
|
-
}, securityService, trackEvent,
|
|
2369
|
+
}, securityService, trackEvent, powSolution);
|
|
2302
2370
|
if (matchesAnyRoute(ctx.path, [
|
|
2303
2371
|
routes.SIGN_UP_EMAIL,
|
|
2304
2372
|
routes.CHANGE_PASSWORD,
|
|
@@ -2331,22 +2399,21 @@ const sentinel = (options) => {
|
|
|
2331
2399
|
after: [{
|
|
2332
2400
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2333
2401
|
handler: createAuthMiddleware(async (ctx) => {
|
|
2334
|
-
const
|
|
2335
|
-
const
|
|
2402
|
+
const untrustedVisitorId = ctx.context.untrustedVisitorId;
|
|
2403
|
+
const ip = ctx.context.ip;
|
|
2336
2404
|
const body = ctx.body;
|
|
2405
|
+
const loginId = matchesAnyRoute(ctx.path, [routes.SIGN_IN_USERNAME]) ? body?.username : body?.email;
|
|
2406
|
+
const isPasswordSignInRoute = matchesAnyRoute(ctx.path, [
|
|
2407
|
+
routes.SIGN_IN_EMAIL,
|
|
2408
|
+
routes.SIGN_IN_USERNAME,
|
|
2409
|
+
routes.SIGN_IN_EMAIL_OTP
|
|
2410
|
+
]);
|
|
2337
2411
|
emitEvaluation(ctx, trackEvent, {
|
|
2338
|
-
identifier:
|
|
2412
|
+
identifier: loginId,
|
|
2339
2413
|
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2340
2414
|
});
|
|
2341
|
-
if (
|
|
2342
|
-
|
|
2343
|
-
const ip = identification?.ip || null;
|
|
2344
|
-
await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(email, visitorId, password, ip));
|
|
2345
|
-
}
|
|
2346
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && !(ctx.context.returned instanceof Error) && body?.email) {
|
|
2347
|
-
const email = body.email;
|
|
2348
|
-
await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(email));
|
|
2349
|
-
}
|
|
2415
|
+
if (isPasswordSignInRoute && ctx.context.returned instanceof Error && loginId && body?.password && untrustedVisitorId) await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(loginId, untrustedVisitorId, body.password, ip));
|
|
2416
|
+
if (isPasswordSignInRoute && !(ctx.context.returned instanceof Error) && loginId) await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(loginId));
|
|
2350
2417
|
})
|
|
2351
2418
|
}]
|
|
2352
2419
|
}
|
|
@@ -2356,7 +2423,7 @@ const sentinel = (options) => {
|
|
|
2356
2423
|
//#region src/events/organization/events-invitation.ts
|
|
2357
2424
|
const initInvitationEvents = (tracker) => {
|
|
2358
2425
|
const { trackEvent } = tracker;
|
|
2359
|
-
const trackOrganizationMemberInvited = (organization, invitation, inviter, trigger) => {
|
|
2426
|
+
const trackOrganizationMemberInvited = (organization, invitation, inviter, trigger, location) => {
|
|
2360
2427
|
trackEvent({
|
|
2361
2428
|
eventKey: organization.id,
|
|
2362
2429
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITED,
|
|
@@ -2374,10 +2441,14 @@ const initInvitationEvents = (tracker) => {
|
|
|
2374
2441
|
inviterEmail: inviter.email ?? "unknown",
|
|
2375
2442
|
triggeredBy: trigger.triggeredBy,
|
|
2376
2443
|
triggerContext: trigger.triggerContext
|
|
2377
|
-
}
|
|
2444
|
+
},
|
|
2445
|
+
ipAddress: location?.ipAddress,
|
|
2446
|
+
city: location?.city,
|
|
2447
|
+
country: location?.country,
|
|
2448
|
+
countryCode: location?.countryCode
|
|
2378
2449
|
});
|
|
2379
2450
|
};
|
|
2380
|
-
const trackOrganizationMemberInviteAccepted = (organization, invitation, member, acceptedBy, trigger) => {
|
|
2451
|
+
const trackOrganizationMemberInviteAccepted = (organization, invitation, member, acceptedBy, trigger, location) => {
|
|
2381
2452
|
trackEvent({
|
|
2382
2453
|
eventKey: organization.id,
|
|
2383
2454
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_ACCEPTED,
|
|
@@ -2397,10 +2468,14 @@ const initInvitationEvents = (tracker) => {
|
|
|
2397
2468
|
memberRole: member.role,
|
|
2398
2469
|
triggeredBy: trigger.triggeredBy,
|
|
2399
2470
|
triggerContext: trigger.triggerContext
|
|
2400
|
-
}
|
|
2471
|
+
},
|
|
2472
|
+
ipAddress: location?.ipAddress,
|
|
2473
|
+
city: location?.city,
|
|
2474
|
+
country: location?.country,
|
|
2475
|
+
countryCode: location?.countryCode
|
|
2401
2476
|
});
|
|
2402
2477
|
};
|
|
2403
|
-
const trackOrganizationMemberInviteRejected = (organization, invitation, rejectedBy, trigger) => {
|
|
2478
|
+
const trackOrganizationMemberInviteRejected = (organization, invitation, rejectedBy, trigger, location) => {
|
|
2404
2479
|
trackEvent({
|
|
2405
2480
|
eventKey: organization.id,
|
|
2406
2481
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_REJECTED,
|
|
@@ -2418,10 +2493,14 @@ const initInvitationEvents = (tracker) => {
|
|
|
2418
2493
|
rejectedByName: rejectedBy.name,
|
|
2419
2494
|
triggeredBy: trigger.triggeredBy,
|
|
2420
2495
|
triggerContext: trigger.triggerContext
|
|
2421
|
-
}
|
|
2496
|
+
},
|
|
2497
|
+
ipAddress: location?.ipAddress,
|
|
2498
|
+
city: location?.city,
|
|
2499
|
+
country: location?.country,
|
|
2500
|
+
countryCode: location?.countryCode
|
|
2422
2501
|
});
|
|
2423
2502
|
};
|
|
2424
|
-
const trackOrganizationMemberInviteCanceled = (organization, invitation, cancelledBy, trigger) => {
|
|
2503
|
+
const trackOrganizationMemberInviteCanceled = (organization, invitation, cancelledBy, trigger, location) => {
|
|
2425
2504
|
trackEvent({
|
|
2426
2505
|
eventKey: organization.id,
|
|
2427
2506
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_CANCELED,
|
|
@@ -2439,7 +2518,11 @@ const initInvitationEvents = (tracker) => {
|
|
|
2439
2518
|
cancelledByEmail: cancelledBy.email ?? "unknown",
|
|
2440
2519
|
triggeredBy: trigger.triggeredBy,
|
|
2441
2520
|
triggerContext: trigger.triggerContext
|
|
2442
|
-
}
|
|
2521
|
+
},
|
|
2522
|
+
ipAddress: location?.ipAddress,
|
|
2523
|
+
city: location?.city,
|
|
2524
|
+
country: location?.country,
|
|
2525
|
+
countryCode: location?.countryCode
|
|
2443
2526
|
});
|
|
2444
2527
|
};
|
|
2445
2528
|
return {
|
|
@@ -2453,7 +2536,7 @@ const initInvitationEvents = (tracker) => {
|
|
|
2453
2536
|
//#region src/events/organization/events-member.ts
|
|
2454
2537
|
const initMemberEvents = (tracker) => {
|
|
2455
2538
|
const { trackEvent } = tracker;
|
|
2456
|
-
const trackOrganizationMemberAdded = (organization, member, user, trigger) => {
|
|
2539
|
+
const trackOrganizationMemberAdded = (organization, member, user, trigger, location) => {
|
|
2457
2540
|
trackEvent({
|
|
2458
2541
|
eventKey: organization.id,
|
|
2459
2542
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_ADDED,
|
|
@@ -2469,10 +2552,14 @@ const initMemberEvents = (tracker) => {
|
|
|
2469
2552
|
memberEmail: user.email ?? "unknown",
|
|
2470
2553
|
triggeredBy: trigger.triggeredBy,
|
|
2471
2554
|
triggerContext: trigger.triggerContext
|
|
2472
|
-
}
|
|
2555
|
+
},
|
|
2556
|
+
ipAddress: location?.ipAddress,
|
|
2557
|
+
city: location?.city,
|
|
2558
|
+
country: location?.country,
|
|
2559
|
+
countryCode: location?.countryCode
|
|
2473
2560
|
});
|
|
2474
2561
|
};
|
|
2475
|
-
const trackOrganizationMemberRemoved = (organization, member, user, trigger) => {
|
|
2562
|
+
const trackOrganizationMemberRemoved = (organization, member, user, trigger, location) => {
|
|
2476
2563
|
trackEvent({
|
|
2477
2564
|
eventKey: organization.id,
|
|
2478
2565
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_REMOVED,
|
|
@@ -2488,10 +2575,14 @@ const initMemberEvents = (tracker) => {
|
|
|
2488
2575
|
memberEmail: user.email ?? "unknown",
|
|
2489
2576
|
triggeredBy: trigger.triggeredBy,
|
|
2490
2577
|
triggerContext: trigger.triggerContext
|
|
2491
|
-
}
|
|
2578
|
+
},
|
|
2579
|
+
ipAddress: location?.ipAddress,
|
|
2580
|
+
city: location?.city,
|
|
2581
|
+
country: location?.country,
|
|
2582
|
+
countryCode: location?.countryCode
|
|
2492
2583
|
});
|
|
2493
2584
|
};
|
|
2494
|
-
const trackOrganizationMemberRoleUpdated = (organization, member, user, previousRole, trigger) => {
|
|
2585
|
+
const trackOrganizationMemberRoleUpdated = (organization, member, user, previousRole, trigger, location) => {
|
|
2495
2586
|
trackEvent({
|
|
2496
2587
|
eventKey: organization.id,
|
|
2497
2588
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_ROLE_UPDATED,
|
|
@@ -2508,7 +2599,11 @@ const initMemberEvents = (tracker) => {
|
|
|
2508
2599
|
memberEmail: user.email ?? "unknown",
|
|
2509
2600
|
triggeredBy: trigger.triggeredBy,
|
|
2510
2601
|
triggerContext: trigger.triggerContext
|
|
2511
|
-
}
|
|
2602
|
+
},
|
|
2603
|
+
ipAddress: location?.ipAddress,
|
|
2604
|
+
city: location?.city,
|
|
2605
|
+
country: location?.country,
|
|
2606
|
+
countryCode: location?.countryCode
|
|
2512
2607
|
});
|
|
2513
2608
|
};
|
|
2514
2609
|
return {
|
|
@@ -2521,7 +2616,7 @@ const initMemberEvents = (tracker) => {
|
|
|
2521
2616
|
//#region src/events/organization/events-organization.ts
|
|
2522
2617
|
const initOrganizationEvents = (tracker) => {
|
|
2523
2618
|
const { trackEvent } = tracker;
|
|
2524
|
-
const trackOrganizationCreated = (organization, trigger) => {
|
|
2619
|
+
const trackOrganizationCreated = (organization, trigger, location) => {
|
|
2525
2620
|
trackEvent({
|
|
2526
2621
|
eventKey: organization.id,
|
|
2527
2622
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_CREATED,
|
|
@@ -2532,10 +2627,14 @@ const initOrganizationEvents = (tracker) => {
|
|
|
2532
2627
|
organizationName: organization.name,
|
|
2533
2628
|
triggeredBy: trigger.triggeredBy,
|
|
2534
2629
|
triggerContext: trigger.triggerContext
|
|
2535
|
-
}
|
|
2630
|
+
},
|
|
2631
|
+
ipAddress: location?.ipAddress,
|
|
2632
|
+
city: location?.city,
|
|
2633
|
+
country: location?.country,
|
|
2634
|
+
countryCode: location?.countryCode
|
|
2536
2635
|
});
|
|
2537
2636
|
};
|
|
2538
|
-
const trackOrganizationUpdated = (organization, trigger) => {
|
|
2637
|
+
const trackOrganizationUpdated = (organization, trigger, location) => {
|
|
2539
2638
|
trackEvent({
|
|
2540
2639
|
eventKey: organization.id,
|
|
2541
2640
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_UPDATED,
|
|
@@ -2546,7 +2645,11 @@ const initOrganizationEvents = (tracker) => {
|
|
|
2546
2645
|
organizationName: organization.name,
|
|
2547
2646
|
triggeredBy: trigger.triggeredBy,
|
|
2548
2647
|
triggerContext: trigger.triggerContext
|
|
2549
|
-
}
|
|
2648
|
+
},
|
|
2649
|
+
ipAddress: location?.ipAddress,
|
|
2650
|
+
city: location?.city,
|
|
2651
|
+
country: location?.country,
|
|
2652
|
+
countryCode: location?.countryCode
|
|
2550
2653
|
});
|
|
2551
2654
|
};
|
|
2552
2655
|
return {
|
|
@@ -2558,7 +2661,7 @@ const initOrganizationEvents = (tracker) => {
|
|
|
2558
2661
|
//#region src/events/organization/events-team.ts
|
|
2559
2662
|
const initTeamEvents = (tracker) => {
|
|
2560
2663
|
const { trackEvent } = tracker;
|
|
2561
|
-
const trackOrganizationTeamCreated = (organization, team, trigger) => {
|
|
2664
|
+
const trackOrganizationTeamCreated = (organization, team, trigger, location) => {
|
|
2562
2665
|
trackEvent({
|
|
2563
2666
|
eventKey: organization.id,
|
|
2564
2667
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_CREATED,
|
|
@@ -2571,10 +2674,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2571
2674
|
teamName: team.name,
|
|
2572
2675
|
triggeredBy: trigger.triggeredBy,
|
|
2573
2676
|
triggerContext: trigger.triggerContext
|
|
2574
|
-
}
|
|
2677
|
+
},
|
|
2678
|
+
ipAddress: location?.ipAddress,
|
|
2679
|
+
city: location?.city,
|
|
2680
|
+
country: location?.country,
|
|
2681
|
+
countryCode: location?.countryCode
|
|
2575
2682
|
});
|
|
2576
2683
|
};
|
|
2577
|
-
const trackOrganizationTeamUpdated = (organization, team, trigger) => {
|
|
2684
|
+
const trackOrganizationTeamUpdated = (organization, team, trigger, location) => {
|
|
2578
2685
|
trackEvent({
|
|
2579
2686
|
eventKey: organization.id,
|
|
2580
2687
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_UPDATED,
|
|
@@ -2587,10 +2694,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2587
2694
|
teamName: team.name,
|
|
2588
2695
|
triggeredBy: trigger.triggeredBy,
|
|
2589
2696
|
triggerContext: trigger.triggerContext
|
|
2590
|
-
}
|
|
2697
|
+
},
|
|
2698
|
+
ipAddress: location?.ipAddress,
|
|
2699
|
+
city: location?.city,
|
|
2700
|
+
country: location?.country,
|
|
2701
|
+
countryCode: location?.countryCode
|
|
2591
2702
|
});
|
|
2592
2703
|
};
|
|
2593
|
-
const trackOrganizationTeamDeleted = (organization, team, trigger) => {
|
|
2704
|
+
const trackOrganizationTeamDeleted = (organization, team, trigger, location) => {
|
|
2594
2705
|
trackEvent({
|
|
2595
2706
|
eventKey: organization.id,
|
|
2596
2707
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_DELETED,
|
|
@@ -2603,10 +2714,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2603
2714
|
teamName: team.name,
|
|
2604
2715
|
triggeredBy: trigger.triggeredBy,
|
|
2605
2716
|
triggerContext: trigger.triggerContext
|
|
2606
|
-
}
|
|
2717
|
+
},
|
|
2718
|
+
ipAddress: location?.ipAddress,
|
|
2719
|
+
city: location?.city,
|
|
2720
|
+
country: location?.country,
|
|
2721
|
+
countryCode: location?.countryCode
|
|
2607
2722
|
});
|
|
2608
2723
|
};
|
|
2609
|
-
const trackOrganizationTeamMemberAdded = (organization, team, user, teamMember, trigger) => {
|
|
2724
|
+
const trackOrganizationTeamMemberAdded = (organization, team, user, teamMember, trigger, location) => {
|
|
2610
2725
|
trackEvent({
|
|
2611
2726
|
eventKey: organization.id,
|
|
2612
2727
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_MEMBER_ADDED,
|
|
@@ -2621,10 +2736,14 @@ const initTeamEvents = (tracker) => {
|
|
|
2621
2736
|
memberName: user.name,
|
|
2622
2737
|
triggeredBy: trigger.triggeredBy,
|
|
2623
2738
|
triggerContext: trigger.triggerContext
|
|
2624
|
-
}
|
|
2739
|
+
},
|
|
2740
|
+
ipAddress: location?.ipAddress,
|
|
2741
|
+
city: location?.city,
|
|
2742
|
+
country: location?.country,
|
|
2743
|
+
countryCode: location?.countryCode
|
|
2625
2744
|
});
|
|
2626
2745
|
};
|
|
2627
|
-
const trackOrganizationTeamMemberRemoved = (organization, team, user, teamMember, trigger) => {
|
|
2746
|
+
const trackOrganizationTeamMemberRemoved = (organization, team, user, teamMember, trigger, location) => {
|
|
2628
2747
|
trackEvent({
|
|
2629
2748
|
eventKey: organization.id,
|
|
2630
2749
|
eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_MEMBER_REMOVED,
|
|
@@ -2639,7 +2758,11 @@ const initTeamEvents = (tracker) => {
|
|
|
2639
2758
|
memberName: user.name,
|
|
2640
2759
|
triggeredBy: trigger.triggeredBy,
|
|
2641
2760
|
triggerContext: trigger.triggerContext
|
|
2642
|
-
}
|
|
2761
|
+
},
|
|
2762
|
+
ipAddress: location?.ipAddress,
|
|
2763
|
+
city: location?.city,
|
|
2764
|
+
country: location?.country,
|
|
2765
|
+
countryCode: location?.countryCode
|
|
2643
2766
|
});
|
|
2644
2767
|
};
|
|
2645
2768
|
return {
|
|
@@ -2794,7 +2917,7 @@ const jwtValidateMiddleware = (options) => {
|
|
|
2794
2917
|
};
|
|
2795
2918
|
//#endregion
|
|
2796
2919
|
//#region src/version.ts
|
|
2797
|
-
const PLUGIN_VERSION = "0.2.
|
|
2920
|
+
const PLUGIN_VERSION = "0.2.13";
|
|
2798
2921
|
//#endregion
|
|
2799
2922
|
//#region src/routes/auth/config.ts
|
|
2800
2923
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
@@ -3636,6 +3759,31 @@ const executeAdapter = (options) => {
|
|
|
3636
3759
|
};
|
|
3637
3760
|
//#endregion
|
|
3638
3761
|
//#region src/routes/invitations/index.ts
|
|
3762
|
+
async function verifyPendingInvitation(token, $api, ctx) {
|
|
3763
|
+
const { data: invitation, error } = await $api("/api/internal/invitations/verify", {
|
|
3764
|
+
method: "POST",
|
|
3765
|
+
body: { token }
|
|
3766
|
+
});
|
|
3767
|
+
if (error || !invitation) {
|
|
3768
|
+
ctx.context.logger.warn("[Dash] Failed to verify invitation", error);
|
|
3769
|
+
throw new APIError("BAD_REQUEST", { message: "Invalid or expired invitation." });
|
|
3770
|
+
}
|
|
3771
|
+
if (invitation.status !== "pending") throw new APIError("BAD_REQUEST", { message: `This invitation has already been ${invitation.status}.` });
|
|
3772
|
+
if (invitation.expiresAt) {
|
|
3773
|
+
if (new Date(invitation.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
3774
|
+
const { error: markError } = await $api("/api/internal/invitations/mark-expired", {
|
|
3775
|
+
method: "POST",
|
|
3776
|
+
body: { token }
|
|
3777
|
+
});
|
|
3778
|
+
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as expired", markError);
|
|
3779
|
+
throw new APIError("BAD_REQUEST", { message: "This invitation has expired." });
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
return invitation;
|
|
3783
|
+
}
|
|
3784
|
+
function getFallbackRedirect(ctx) {
|
|
3785
|
+
return typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : "/";
|
|
3786
|
+
}
|
|
3639
3787
|
/**
|
|
3640
3788
|
* Accept invitation endpoint
|
|
3641
3789
|
* This is called when a user clicks the invitation link.
|
|
@@ -3648,25 +3796,7 @@ const acceptInvitation = (options) => {
|
|
|
3648
3796
|
query: z$1.object({ token: z$1.string() })
|
|
3649
3797
|
}, async (ctx) => {
|
|
3650
3798
|
const { token } = ctx.query;
|
|
3651
|
-
const
|
|
3652
|
-
method: "POST",
|
|
3653
|
-
body: { token }
|
|
3654
|
-
});
|
|
3655
|
-
if (error || !invitation) {
|
|
3656
|
-
ctx.context.logger.warn("[Dash] Failed to verify invitation", error);
|
|
3657
|
-
throw new APIError("BAD_REQUEST", { message: "Invalid or expired invitation." });
|
|
3658
|
-
}
|
|
3659
|
-
if (invitation.status !== "pending") throw new APIError("BAD_REQUEST", { message: `This invitation has already been ${invitation.status}.` });
|
|
3660
|
-
if (invitation.expiresAt) {
|
|
3661
|
-
if (new Date(invitation.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
3662
|
-
const { error: markError } = await $api("/api/internal/invitations/mark-expired", {
|
|
3663
|
-
method: "POST",
|
|
3664
|
-
body: { token }
|
|
3665
|
-
});
|
|
3666
|
-
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as expired", markError);
|
|
3667
|
-
throw new APIError("BAD_REQUEST", { message: "This invitation has expired." });
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3799
|
+
const invitation = await verifyPendingInvitation(token, $api, ctx);
|
|
3670
3800
|
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3671
3801
|
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user) => user?.user);
|
|
3672
3802
|
if (existingUser) {
|
|
@@ -3727,24 +3857,12 @@ const completeInvitation = (options) => {
|
|
|
3727
3857
|
method: "POST",
|
|
3728
3858
|
body: z$1.object({
|
|
3729
3859
|
token: z$1.string(),
|
|
3730
|
-
password: z$1.string().optional()
|
|
3731
|
-
providerId: z$1.string().optional(),
|
|
3732
|
-
providerAccountId: z$1.string().optional(),
|
|
3733
|
-
accessToken: z$1.string().optional(),
|
|
3734
|
-
refreshToken: z$1.string().optional()
|
|
3860
|
+
password: z$1.string().optional()
|
|
3735
3861
|
})
|
|
3736
3862
|
}, async (ctx) => {
|
|
3737
|
-
const { token, password
|
|
3738
|
-
const fallbackRedirect =
|
|
3739
|
-
const
|
|
3740
|
-
method: "POST",
|
|
3741
|
-
body: { token }
|
|
3742
|
-
});
|
|
3743
|
-
if (error || !invitation) {
|
|
3744
|
-
ctx.context.logger.warn("[Dash] Failed to verify invitation", error);
|
|
3745
|
-
throw new APIError("BAD_REQUEST", { message: "Invalid or expired invitation." });
|
|
3746
|
-
}
|
|
3747
|
-
if (invitation.status !== "pending") throw new APIError("BAD_REQUEST", { message: `This invitation has already been ${invitation.status}.` });
|
|
3863
|
+
const { token, password } = ctx.body;
|
|
3864
|
+
const fallbackRedirect = getFallbackRedirect(ctx);
|
|
3865
|
+
const invitation = await verifyPendingInvitation(token, $api, ctx);
|
|
3748
3866
|
if (!ctx.context) throw new APIError("BAD_REQUEST", { message: "Context is required" });
|
|
3749
3867
|
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3750
3868
|
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user) => user?.user);
|
|
@@ -3766,6 +3884,7 @@ const completeInvitation = (options) => {
|
|
|
3766
3884
|
redirectUrl: invitation.redirectUrl ?? fallbackRedirect
|
|
3767
3885
|
};
|
|
3768
3886
|
}
|
|
3887
|
+
if (!password) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
|
|
3769
3888
|
const user = await ctx.context.internalAdapter.createUser({
|
|
3770
3889
|
email: invitationEmail,
|
|
3771
3890
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
@@ -3773,19 +3892,12 @@ const completeInvitation = (options) => {
|
|
|
3773
3892
|
createdAt: /* @__PURE__ */ new Date(),
|
|
3774
3893
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3775
3894
|
});
|
|
3776
|
-
|
|
3895
|
+
await ctx.context.internalAdapter.createAccount({
|
|
3777
3896
|
userId: user.id,
|
|
3778
3897
|
providerId: "credential",
|
|
3779
3898
|
accountId: user.id,
|
|
3780
3899
|
password: await ctx.context.password.hash(password)
|
|
3781
3900
|
});
|
|
3782
|
-
else if (providerId && providerAccountId) await ctx.context.internalAdapter.createAccount({
|
|
3783
|
-
userId: user.id,
|
|
3784
|
-
providerId,
|
|
3785
|
-
accountId: providerAccountId,
|
|
3786
|
-
accessToken,
|
|
3787
|
-
refreshToken
|
|
3788
|
-
});
|
|
3789
3901
|
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
3790
3902
|
method: "POST",
|
|
3791
3903
|
body: {
|
|
@@ -3804,6 +3916,30 @@ const completeInvitation = (options) => {
|
|
|
3804
3916
|
};
|
|
3805
3917
|
});
|
|
3806
3918
|
};
|
|
3919
|
+
const completeInvitationSocial = (options) => {
|
|
3920
|
+
const { $api } = options;
|
|
3921
|
+
return createAuthEndpoint("/dash/complete-invitation-social", {
|
|
3922
|
+
method: "GET",
|
|
3923
|
+
query: z$1.object({ token: z$1.string() }),
|
|
3924
|
+
use: [sessionMiddleware]
|
|
3925
|
+
}, async (ctx) => {
|
|
3926
|
+
const sessionUser = ctx.context.session?.user;
|
|
3927
|
+
if (!sessionUser) throw new APIError("UNAUTHORIZED", { message: "Authentication required." });
|
|
3928
|
+
const invitation = await verifyPendingInvitation(ctx.query.token, $api, ctx);
|
|
3929
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3930
|
+
const sessionEmail = sessionUser.email ? normalizeEmail(sessionUser.email, ctx.context) : null;
|
|
3931
|
+
if (!sessionEmail || sessionEmail !== invitationEmail) throw new APIError("BAD_REQUEST", { message: "Signed-in account does not match this invitation." });
|
|
3932
|
+
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
3933
|
+
method: "POST",
|
|
3934
|
+
body: {
|
|
3935
|
+
token: ctx.query.token,
|
|
3936
|
+
userId: sessionUser.id
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3939
|
+
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
3940
|
+
return ctx.redirect((invitation.redirectUrl || getFallbackRedirect(ctx)).toString());
|
|
3941
|
+
});
|
|
3942
|
+
};
|
|
3807
3943
|
/**
|
|
3808
3944
|
* Check if a user exists by email
|
|
3809
3945
|
* Used by the platform to verify before sending invitation
|
|
@@ -5661,7 +5797,7 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
|
|
|
5661
5797
|
}, async (ctx) => {
|
|
5662
5798
|
const { userId } = ctx.body;
|
|
5663
5799
|
if (!await ctx.context.internalAdapter.findUserById(userId)) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
5664
|
-
await ctx.context.internalAdapter.
|
|
5800
|
+
await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
5665
5801
|
return ctx.json({ success: true });
|
|
5666
5802
|
});
|
|
5667
5803
|
const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
|
|
@@ -5670,7 +5806,7 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
|
|
|
5670
5806
|
}, async (ctx) => {
|
|
5671
5807
|
const { userIds } = ctx.context.payload;
|
|
5672
5808
|
await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
|
|
5673
|
-
for (const userId of chunk) await ctx.context.internalAdapter.
|
|
5809
|
+
for (const userId of chunk) await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
5674
5810
|
}, { concurrency: 3 });
|
|
5675
5811
|
return ctx.json({
|
|
5676
5812
|
success: true,
|
|
@@ -5694,8 +5830,14 @@ function isPrivateIPv4(a, b) {
|
|
|
5694
5830
|
if (a === 169 && b === 254) return true;
|
|
5695
5831
|
if (a === 0) return true;
|
|
5696
5832
|
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
5833
|
+
if (a >= 224) return true;
|
|
5697
5834
|
return false;
|
|
5698
5835
|
}
|
|
5836
|
+
function parseIPv4Quad(quad) {
|
|
5837
|
+
const v4parts = quad.split(".").map(Number);
|
|
5838
|
+
if (v4parts.length === 4 && v4parts.every((p) => !isNaN(p) && p >= 0 && p <= 255)) return [v4parts[0], v4parts[1]];
|
|
5839
|
+
return null;
|
|
5840
|
+
}
|
|
5699
5841
|
/**
|
|
5700
5842
|
* Loopback, RFC1918, link-local, CGNAT, ULA, documentation, NAT64 well-known,
|
|
5701
5843
|
* IPv4-mapped private, unspecified, and multicast (RFC 4291 section 2.7).
|
|
@@ -5732,12 +5874,23 @@ function parseIPv6(addr) {
|
|
|
5732
5874
|
if (left.length !== 8) return null;
|
|
5733
5875
|
return left;
|
|
5734
5876
|
}
|
|
5877
|
+
/** True when the hostname is an IPv4 or IPv6 literal (not a DNS name). */
|
|
5878
|
+
function isIpLiteralHostname(hostname) {
|
|
5879
|
+
const bare = hostname.replace(/^\[|\]$/g, "");
|
|
5880
|
+
if (parseIPv4Quad(bare)) return true;
|
|
5881
|
+
return parseIPv6(bare)?.length === 8;
|
|
5882
|
+
}
|
|
5735
5883
|
/** Returns true if the hostname resolves to a private/reserved IP or is a local hostname. */
|
|
5736
5884
|
function isPrivateHost(hostname) {
|
|
5737
|
-
if (hostname === "localhost" || hostname.endsWith(".local") || hostname.endsWith(".internal")) return true;
|
|
5885
|
+
if (hostname === "localhost" || hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) return true;
|
|
5738
5886
|
const bare = hostname.replace(/^\[|\]$/g, "");
|
|
5739
|
-
const
|
|
5740
|
-
if (
|
|
5887
|
+
const v4 = parseIPv4Quad(bare);
|
|
5888
|
+
if (v4) return isPrivateIPv4(v4[0], v4[1]);
|
|
5889
|
+
const mappedTail = bare.match(/(?:^|:)([\d.]+)$/);
|
|
5890
|
+
if (mappedTail && mappedTail[1].includes(".")) {
|
|
5891
|
+
const mappedV4 = parseIPv4Quad(mappedTail[1]);
|
|
5892
|
+
if (mappedV4) return isPrivateIPv4(mappedV4[0], mappedV4[1]);
|
|
5893
|
+
}
|
|
5741
5894
|
const groups = parseIPv6(bare);
|
|
5742
5895
|
if (groups && groups.length === 8) return ipv6GroupsAreNonPublic(groups);
|
|
5743
5896
|
return false;
|
|
@@ -5775,39 +5928,41 @@ async function resolveHostnameAddresses(hostname) {
|
|
|
5775
5928
|
return null;
|
|
5776
5929
|
}
|
|
5777
5930
|
}
|
|
5778
|
-
function validateUrlSync(
|
|
5779
|
-
const trimmed = rawUrl.trim();
|
|
5931
|
+
function validateUrlSync(input, options = {}) {
|
|
5780
5932
|
let parsed;
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5933
|
+
if (input instanceof URL) parsed = input;
|
|
5934
|
+
else {
|
|
5935
|
+
const trimmed = input.trim();
|
|
5936
|
+
try {
|
|
5937
|
+
parsed = new URL(trimmed);
|
|
5938
|
+
} catch {
|
|
5939
|
+
return {
|
|
5940
|
+
ok: false,
|
|
5941
|
+
reason: "invalid_url"
|
|
5942
|
+
};
|
|
5943
|
+
}
|
|
5789
5944
|
}
|
|
5790
5945
|
if (!isAllowedProtocol(parsed.protocol, options.allowedProtocols)) return {
|
|
5791
5946
|
ok: false,
|
|
5792
5947
|
reason: "disallowed_protocol",
|
|
5793
|
-
url:
|
|
5948
|
+
url: parsed,
|
|
5794
5949
|
protocol: parsed.protocol
|
|
5795
5950
|
};
|
|
5796
5951
|
if (parsed.username !== "" || parsed.password !== "") return {
|
|
5797
5952
|
ok: false,
|
|
5798
5953
|
reason: "embedded_credentials",
|
|
5799
|
-
url:
|
|
5954
|
+
url: parsed
|
|
5800
5955
|
};
|
|
5801
5956
|
if (isPrivateHost(parsed.hostname)) return {
|
|
5802
5957
|
ok: false,
|
|
5803
5958
|
reason: "private_host",
|
|
5804
|
-
url:
|
|
5959
|
+
url: parsed
|
|
5805
5960
|
};
|
|
5806
5961
|
if (options.allowedOrigins?.length) {
|
|
5807
5962
|
if (!isAllowedOrigin(parsed.origin, options.allowedOrigins)) return {
|
|
5808
5963
|
ok: false,
|
|
5809
5964
|
reason: "disallowed_origin",
|
|
5810
|
-
url:
|
|
5965
|
+
url: parsed,
|
|
5811
5966
|
hostname: parsed.hostname,
|
|
5812
5967
|
origin: parsed.origin
|
|
5813
5968
|
};
|
|
@@ -5818,21 +5973,32 @@ function validateUrlSync(rawUrl, options = {}) {
|
|
|
5818
5973
|
};
|
|
5819
5974
|
}
|
|
5820
5975
|
/** SSRF checks with DNS resolution when available. */
|
|
5821
|
-
async function validateUrl(
|
|
5822
|
-
const sync = validateUrlSync(
|
|
5976
|
+
async function validateUrl(input, options = {}) {
|
|
5977
|
+
const sync = validateUrlSync(input, options);
|
|
5823
5978
|
if (!sync.ok) return sync;
|
|
5824
5979
|
if (options.dns === false) return sync;
|
|
5825
|
-
const
|
|
5980
|
+
const hostname = sync.url.hostname;
|
|
5981
|
+
const addresses = await resolveHostnameAddresses(hostname);
|
|
5982
|
+
if (!isIpLiteralHostname(hostname) && !addresses?.length) return {
|
|
5983
|
+
ok: false,
|
|
5984
|
+
reason: "unresolved_host",
|
|
5985
|
+
url: sync.url,
|
|
5986
|
+
hostname
|
|
5987
|
+
};
|
|
5826
5988
|
if (addresses) {
|
|
5827
5989
|
for (const addr of addresses) if (isPrivateHost(addr)) return {
|
|
5828
5990
|
ok: false,
|
|
5829
5991
|
reason: "private_dns",
|
|
5830
|
-
url: sync.url
|
|
5831
|
-
hostname
|
|
5992
|
+
url: sync.url,
|
|
5993
|
+
hostname,
|
|
5832
5994
|
address: addr
|
|
5833
5995
|
};
|
|
5834
5996
|
}
|
|
5835
|
-
return
|
|
5997
|
+
return {
|
|
5998
|
+
ok: true,
|
|
5999
|
+
url: sync.url,
|
|
6000
|
+
...addresses ? { addresses } : {}
|
|
6001
|
+
};
|
|
5836
6002
|
}
|
|
5837
6003
|
function safeResolveUrl(raw, baseURL) {
|
|
5838
6004
|
const trim = raw.trim();
|
|
@@ -5848,12 +6014,6 @@ function safeResolveUrl(raw, baseURL) {
|
|
|
5848
6014
|
}
|
|
5849
6015
|
//#endregion
|
|
5850
6016
|
//#region src/validation/ssrf.ts
|
|
5851
|
-
/**
|
|
5852
|
-
* SSRF protection for dash plugin outbound fetches.
|
|
5853
|
-
*
|
|
5854
|
-
* Uses @infra/utils at build time; tsdown bundles it into published dist so
|
|
5855
|
-
* consumers do not install the private workspace package.
|
|
5856
|
-
*/
|
|
5857
6017
|
const REDIRECT_STATUSES = new Set([
|
|
5858
6018
|
301,
|
|
5859
6019
|
302,
|
|
@@ -5868,11 +6028,6 @@ var SsrfBlockedError = class extends Error {
|
|
|
5868
6028
|
function isSsrfBlockedError(error) {
|
|
5869
6029
|
return error instanceof SsrfBlockedError;
|
|
5870
6030
|
}
|
|
5871
|
-
/** Validates URL hostname literals and resolved DNS addresses. */
|
|
5872
|
-
async function parseAndValidateUrl(url) {
|
|
5873
|
-
const result = await validateUrl(url);
|
|
5874
|
-
return result.ok ? result.url : null;
|
|
5875
|
-
}
|
|
5876
6031
|
/**
|
|
5877
6032
|
* Fetch with SSRF checks on the initial URL and every redirect target.
|
|
5878
6033
|
*/
|
|
@@ -5880,8 +6035,9 @@ async function $outbound(url, options = {}) {
|
|
|
5880
6035
|
const { timeout, signal: callerSignal, ...fetchInit } = options;
|
|
5881
6036
|
let currentUrl = url;
|
|
5882
6037
|
for (let hop = 0;; hop++) {
|
|
5883
|
-
const
|
|
5884
|
-
if (!
|
|
6038
|
+
const result = await validateUrl(currentUrl);
|
|
6039
|
+
if (!result.ok) throw new SsrfBlockedError("Invalid or blocked URL");
|
|
6040
|
+
const validated = result.url;
|
|
5885
6041
|
const signals = [];
|
|
5886
6042
|
if (callerSignal) signals.push(callerSignal);
|
|
5887
6043
|
if (timeout !== void 0 && timeout > 0) signals.push(AbortSignal.timeout(timeout));
|
|
@@ -5896,7 +6052,7 @@ async function $outbound(url, options = {}) {
|
|
|
5896
6052
|
const location = response.headers.get("location");
|
|
5897
6053
|
const next = location ? safeResolveUrl(location, validated.href) : null;
|
|
5898
6054
|
if (!next) throw new SsrfBlockedError("Invalid redirect location");
|
|
5899
|
-
currentUrl = next
|
|
6055
|
+
currentUrl = next;
|
|
5900
6056
|
}
|
|
5901
6057
|
}
|
|
5902
6058
|
//#endregion
|
|
@@ -6011,23 +6167,19 @@ const oidcConfigSchema = z$1.object({
|
|
|
6011
6167
|
});
|
|
6012
6168
|
async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
6013
6169
|
let idpMetadataXml = samlConfig.idpMetadata?.metadata;
|
|
6014
|
-
if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) {
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
if (e instanceof APIError$1) throw e;
|
|
6028
|
-
ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
|
|
6029
|
-
throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
|
|
6030
|
-
}
|
|
6170
|
+
if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) try {
|
|
6171
|
+
const metadataResponse = await $outbound(samlConfig.idpMetadata.metadataUrl, {
|
|
6172
|
+
method: "GET",
|
|
6173
|
+
headers: { Accept: "application/xml, text/xml" },
|
|
6174
|
+
timeout: 15e3
|
|
6175
|
+
});
|
|
6176
|
+
if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
|
|
6177
|
+
idpMetadataXml = await metadataResponse.text();
|
|
6178
|
+
} catch (e) {
|
|
6179
|
+
if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
|
|
6180
|
+
if (e instanceof APIError$1) throw e;
|
|
6181
|
+
ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
|
|
6182
|
+
throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
|
|
6031
6183
|
}
|
|
6032
6184
|
const metadataAlgorithmWarnings = [];
|
|
6033
6185
|
if (idpMetadataXml) {
|
|
@@ -6571,41 +6723,13 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
|
|
|
6571
6723
|
} catch {}
|
|
6572
6724
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6573
6725
|
const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
|
|
6574
|
-
return {
|
|
6575
|
-
totpURI: `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(user?.email || userId)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`,
|
|
6576
|
-
secret
|
|
6577
|
-
};
|
|
6726
|
+
return { totpURI: `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(user?.email || userId)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30` };
|
|
6578
6727
|
});
|
|
6579
6728
|
const viewBackupCodes = (options) => createAuthEndpoint("/dash/view-backup-codes", {
|
|
6580
6729
|
method: "POST",
|
|
6581
6730
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6582
|
-
}, async (
|
|
6583
|
-
|
|
6584
|
-
const twoFactorRecord = await ctx.context.adapter.findOne({
|
|
6585
|
-
model: "twoFactor",
|
|
6586
|
-
where: [{
|
|
6587
|
-
field: "userId",
|
|
6588
|
-
value: userId
|
|
6589
|
-
}]
|
|
6590
|
-
});
|
|
6591
|
-
if (!twoFactorRecord) throw new APIError("NOT_FOUND", { message: "Two-factor authentication not set up for this user" });
|
|
6592
|
-
let backupCodes;
|
|
6593
|
-
try {
|
|
6594
|
-
backupCodes = JSON.parse(twoFactorRecord.backupCodes);
|
|
6595
|
-
} catch {
|
|
6596
|
-
try {
|
|
6597
|
-
const { symmetricDecrypt } = await import("better-auth/crypto");
|
|
6598
|
-
const decrypted = await symmetricDecrypt({
|
|
6599
|
-
key: ctx.context.secret,
|
|
6600
|
-
data: twoFactorRecord.backupCodes
|
|
6601
|
-
});
|
|
6602
|
-
backupCodes = JSON.parse(decrypted);
|
|
6603
|
-
} catch (decryptError) {
|
|
6604
|
-
ctx.context.logger.debug("[Dash] Failed to decrypt backup codes:", decryptError);
|
|
6605
|
-
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to decrypt backup codes" });
|
|
6606
|
-
}
|
|
6607
|
-
}
|
|
6608
|
-
return { backupCodes };
|
|
6731
|
+
}, async () => {
|
|
6732
|
+
throw new APIError("FORBIDDEN", { message: "Backup codes cannot be viewed after initial setup. Generate new codes instead." });
|
|
6609
6733
|
});
|
|
6610
6734
|
const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-factor", {
|
|
6611
6735
|
method: "POST",
|
|
@@ -7599,7 +7723,7 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
|
|
|
7599
7723
|
banExpires: banExpires ? new Date(banExpires) : null,
|
|
7600
7724
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7601
7725
|
});
|
|
7602
|
-
await ctx.context.internalAdapter.
|
|
7726
|
+
await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
7603
7727
|
return { success: true };
|
|
7604
7728
|
});
|
|
7605
7729
|
const banManyUsers = (options) => {
|
|
@@ -7929,6 +8053,13 @@ async function sendSMS(options, config) {
|
|
|
7929
8053
|
}
|
|
7930
8054
|
//#endregion
|
|
7931
8055
|
//#region src/index.ts
|
|
8056
|
+
async function getRequestLocation() {
|
|
8057
|
+
try {
|
|
8058
|
+
return (await getCurrentAuthContext()).context.location;
|
|
8059
|
+
} catch {
|
|
8060
|
+
return;
|
|
8061
|
+
}
|
|
8062
|
+
}
|
|
7932
8063
|
const dash = (options) => {
|
|
7933
8064
|
const processedBulkOperationContexts = /* @__PURE__ */ new WeakSet();
|
|
7934
8065
|
const opts = resolveDashOptions(options);
|
|
@@ -7960,85 +8091,85 @@ const dash = (options) => {
|
|
|
7960
8091
|
const afterCreateOrganization = organizationHooks.afterCreateOrganization;
|
|
7961
8092
|
organizationHooks.afterCreateOrganization = async (...args) => {
|
|
7962
8093
|
const [{ organization, user }] = args;
|
|
7963
|
-
trackOrganizationCreated(organization, getOrganizationTriggerInfo(user));
|
|
8094
|
+
trackOrganizationCreated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
7964
8095
|
if (afterCreateOrganization) return afterCreateOrganization(...args);
|
|
7965
8096
|
};
|
|
7966
8097
|
const afterUpdateOrganization = organizationHooks.afterUpdateOrganization;
|
|
7967
8098
|
organizationHooks.afterUpdateOrganization = async (...args) => {
|
|
7968
8099
|
const [{ organization, user }] = args;
|
|
7969
|
-
if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user));
|
|
8100
|
+
if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
7970
8101
|
if (afterUpdateOrganization) return afterUpdateOrganization(...args);
|
|
7971
8102
|
};
|
|
7972
8103
|
const afterAddMember = organizationHooks.afterAddMember;
|
|
7973
8104
|
organizationHooks.afterAddMember = async (...args) => {
|
|
7974
8105
|
const [{ organization, member, user }] = args;
|
|
7975
|
-
trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user));
|
|
8106
|
+
trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
7976
8107
|
if (afterAddMember) return afterAddMember(...args);
|
|
7977
8108
|
};
|
|
7978
8109
|
const afterRemoveMember = organizationHooks.afterRemoveMember;
|
|
7979
8110
|
organizationHooks.afterRemoveMember = async (...args) => {
|
|
7980
8111
|
const [{ organization, member, user }] = args;
|
|
7981
|
-
trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user));
|
|
8112
|
+
trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
7982
8113
|
if (afterRemoveMember) return afterRemoveMember(...args);
|
|
7983
8114
|
};
|
|
7984
8115
|
const afterUpdateMemberRole = organizationHooks.afterUpdateMemberRole;
|
|
7985
8116
|
organizationHooks.afterUpdateMemberRole = async (...args) => {
|
|
7986
8117
|
const [{ organization, member, user, previousRole }] = args;
|
|
7987
|
-
trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user));
|
|
8118
|
+
trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
7988
8119
|
if (afterUpdateMemberRole) return afterUpdateMemberRole(...args);
|
|
7989
8120
|
};
|
|
7990
8121
|
const afterCreateInvitation = organizationHooks.afterCreateInvitation;
|
|
7991
8122
|
organizationHooks.afterCreateInvitation = async (...args) => {
|
|
7992
8123
|
const [{ organization, invitation, inviter }] = args;
|
|
7993
|
-
trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter));
|
|
8124
|
+
trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter), await getRequestLocation());
|
|
7994
8125
|
if (afterCreateInvitation) return afterCreateInvitation(...args);
|
|
7995
8126
|
};
|
|
7996
8127
|
const afterAcceptInvitation = organizationHooks.afterAcceptInvitation;
|
|
7997
8128
|
organizationHooks.afterAcceptInvitation = async (...args) => {
|
|
7998
8129
|
const [{ organization, invitation, member, user }] = args;
|
|
7999
|
-
trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user));
|
|
8130
|
+
trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8000
8131
|
if (afterAcceptInvitation) return afterAcceptInvitation(...args);
|
|
8001
8132
|
};
|
|
8002
8133
|
const afterRejectInvitation = organizationHooks.afterRejectInvitation;
|
|
8003
8134
|
organizationHooks.afterRejectInvitation = async (...args) => {
|
|
8004
8135
|
const [{ organization, invitation, user }] = args;
|
|
8005
|
-
trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user));
|
|
8136
|
+
trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8006
8137
|
if (afterRejectInvitation) return afterRejectInvitation(...args);
|
|
8007
8138
|
};
|
|
8008
8139
|
const afterCancelInvitation = organizationHooks.afterCancelInvitation;
|
|
8009
8140
|
organizationHooks.afterCancelInvitation = async (...args) => {
|
|
8010
8141
|
const [{ organization, invitation, cancelledBy }] = args;
|
|
8011
|
-
trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy));
|
|
8142
|
+
trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy), await getRequestLocation());
|
|
8012
8143
|
if (afterCancelInvitation) return afterCancelInvitation(...args);
|
|
8013
8144
|
};
|
|
8014
8145
|
const afterCreateTeam = organizationHooks.afterCreateTeam;
|
|
8015
8146
|
organizationHooks.afterCreateTeam = async (...args) => {
|
|
8016
8147
|
const [{ organization, team, user }] = args;
|
|
8017
|
-
trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user));
|
|
8148
|
+
trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8018
8149
|
if (afterCreateTeam) return afterCreateTeam(...args);
|
|
8019
8150
|
};
|
|
8020
8151
|
const afterUpdateTeam = organizationHooks.afterUpdateTeam;
|
|
8021
8152
|
organizationHooks.afterUpdateTeam = async (...args) => {
|
|
8022
8153
|
const [{ organization, team, user }] = args;
|
|
8023
|
-
if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user));
|
|
8154
|
+
if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8024
8155
|
if (afterUpdateTeam) return afterUpdateTeam(...args);
|
|
8025
8156
|
};
|
|
8026
8157
|
const afterDeleteTeam = organizationHooks.afterDeleteTeam;
|
|
8027
8158
|
organizationHooks.afterDeleteTeam = async (...args) => {
|
|
8028
8159
|
const [{ organization, team, user }] = args;
|
|
8029
|
-
trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user));
|
|
8160
|
+
trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8030
8161
|
if (afterDeleteTeam) return afterDeleteTeam(...args);
|
|
8031
8162
|
};
|
|
8032
8163
|
const afterAddTeamMember = organizationHooks.afterAddTeamMember;
|
|
8033
8164
|
organizationHooks.afterAddTeamMember = async (...args) => {
|
|
8034
8165
|
const [{ organization, team, user, teamMember }] = args;
|
|
8035
|
-
trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user));
|
|
8166
|
+
trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8036
8167
|
if (afterAddTeamMember) return afterAddTeamMember(...args);
|
|
8037
8168
|
};
|
|
8038
8169
|
const afterRemoveTeamMember = organizationHooks.afterRemoveTeamMember;
|
|
8039
8170
|
organizationHooks.afterRemoveTeamMember = async (...args) => {
|
|
8040
8171
|
const [{ organization, team, user, teamMember }] = args;
|
|
8041
|
-
trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user));
|
|
8172
|
+
trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
|
|
8042
8173
|
if (afterRemoveTeamMember) return afterRemoveTeamMember(...args);
|
|
8043
8174
|
};
|
|
8044
8175
|
};
|
|
@@ -8219,7 +8350,11 @@ const dash = (options) => {
|
|
|
8219
8350
|
matcher: (ctx) => {
|
|
8220
8351
|
if (ctx.request?.method !== "GET") return true;
|
|
8221
8352
|
const path = new URL(ctx.request.url).pathname;
|
|
8222
|
-
return matchesAnyRoute(path, [
|
|
8353
|
+
return matchesAnyRoute(path, [
|
|
8354
|
+
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8355
|
+
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
8356
|
+
routes.DASH_IMPERSONATE_USER
|
|
8357
|
+
]);
|
|
8223
8358
|
},
|
|
8224
8359
|
handler: createIdentificationMiddleware($kv)
|
|
8225
8360
|
}],
|
|
@@ -8227,16 +8362,20 @@ const dash = (options) => {
|
|
|
8227
8362
|
matcher: (ctx) => {
|
|
8228
8363
|
if (ctx.request?.method !== "GET") return true;
|
|
8229
8364
|
const path = new URL(ctx.request.url).pathname;
|
|
8230
|
-
return matchesAnyRoute(path, [
|
|
8365
|
+
return matchesAnyRoute(path, [
|
|
8366
|
+
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8367
|
+
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
8368
|
+
routes.DASH_IMPERSONATE_USER
|
|
8369
|
+
]);
|
|
8231
8370
|
},
|
|
8232
8371
|
handler: createAuthMiddleware(async (_ctx) => {
|
|
8233
8372
|
const ctx = _ctx;
|
|
8234
8373
|
const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
|
|
8235
|
-
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);
|
|
8374
|
+
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);
|
|
8236
8375
|
const body = ctx.body;
|
|
8237
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
|
|
8238
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
|
|
8239
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
8376
|
+
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);
|
|
8377
|
+
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);
|
|
8378
|
+
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);
|
|
8240
8379
|
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
8241
8380
|
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
8242
8381
|
maxAge: 600,
|
|
@@ -8341,6 +8480,7 @@ const dash = (options) => {
|
|
|
8341
8480
|
getEventTypes: getEventTypes(settings),
|
|
8342
8481
|
dashAcceptInvitation: acceptInvitation(settings),
|
|
8343
8482
|
dashCompleteInvitation: completeInvitation(settings),
|
|
8483
|
+
dashCompleteInvitationSocial: completeInvitationSocial(settings),
|
|
8344
8484
|
dashCheckUserExists: checkUserExists(settings),
|
|
8345
8485
|
listDashOrganizationDirectories: listOrganizationDirectories(settings),
|
|
8346
8486
|
createDashOrganizationDirectory: createOrganizationDirectory(settings),
|