@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.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, _location) => {
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, _ctx) => {
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
- if (requestId && identification) {
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
- ctx.context.location = {
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
- return;
973
- }
974
- const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
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
- return;
1022
+ else location = void 0;
982
1023
  }
983
- ctx.context.location = void 0;
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
- /** Call immediately before throwing so the buffer reflects the final outcome. */
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
- return (await $api(`/security/is-blocked?visitorId=${encodeURIComponent(visitorId)}`, { method: "GET" })).blocked ?? false;
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
- const all = new Set([
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
- * Path is one of `[
1508
- * '/sign-up/email',
1509
- * '/email-otp/verify-email',
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: "invalid_format",
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 message = result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short" ? "This email address appears to be invalid" : "Invalid email";
1808
+ const { reason, message } = resolveEmailBlock(result);
1736
1809
  if (trackEvent) {
1737
1810
  setOutcome(ctx, "blocked", "email_validity", {
1738
- reason: result.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, powVerified) {
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.identification?.ip || null,
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 travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip);
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 powVerified = ctx.context.powVerified === true;
2279
- if (visitorId && powVerified) recordCheck(ctx, "pow_challenge");
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
- reason: "cooldown_active",
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, powVerified);
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 visitorId = ctx.context.visitorId;
2335
- const identification = ctx.context.identification;
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: body?.email,
2412
+ identifier: loginId,
2339
2413
  userAgent: ctx.headers?.get?.("user-agent") || ""
2340
2414
  });
2341
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email && body?.password && visitorId) {
2342
- const { email, password } = body;
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.11";
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 { data: invitation, error } = await $api("/api/internal/invitations/verify", {
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, providerId, providerAccountId, accessToken, refreshToken } = ctx.body;
3738
- const fallbackRedirect = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : "/";
3739
- const { data: invitation, error } = await $api("/api/internal/invitations/verify", {
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
- if (password) await ctx.context.internalAdapter.createAccount({
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.deleteSessions(userId);
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.deleteSessions(userId);
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 v4parts = bare.split(".").map(Number);
5740
- if (v4parts.length === 4 && v4parts.every((p) => !isNaN(p) && p >= 0 && p <= 255)) return isPrivateIPv4(v4parts[0], v4parts[1]);
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(rawUrl, options = {}) {
5779
- const trimmed = rawUrl.trim();
5931
+ function validateUrlSync(input, options = {}) {
5780
5932
  let parsed;
5781
- try {
5782
- parsed = new URL(trimmed);
5783
- } catch {
5784
- return {
5785
- ok: false,
5786
- reason: "invalid_url",
5787
- url: trimmed
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: trimmed,
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: trimmed
5954
+ url: parsed
5800
5955
  };
5801
5956
  if (isPrivateHost(parsed.hostname)) return {
5802
5957
  ok: false,
5803
5958
  reason: "private_host",
5804
- url: trimmed
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: trimmed,
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(rawUrl, options = {}) {
5822
- const sync = validateUrlSync(rawUrl, options);
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 addresses = await resolveHostnameAddresses(sync.url.hostname);
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.href,
5831
- hostname: sync.url.hostname,
5992
+ url: sync.url,
5993
+ hostname,
5832
5994
  address: addr
5833
5995
  };
5834
5996
  }
5835
- return sync;
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 validated = await parseAndValidateUrl(currentUrl);
5884
- if (!validated) throw new SsrfBlockedError("Invalid or blocked URL");
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.href;
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 validatedMetadataUrl = await parseAndValidateUrl(samlConfig.idpMetadata.metadataUrl);
6016
- if (!validatedMetadataUrl) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
6017
- try {
6018
- const metadataResponse = await $outbound(validatedMetadataUrl.href, {
6019
- method: "GET",
6020
- headers: { Accept: "application/xml, text/xml" },
6021
- timeout: 15e3
6022
- });
6023
- if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
6024
- idpMetadataXml = await metadataResponse.text();
6025
- } catch (e) {
6026
- if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
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 (ctx) => {
6583
- const { userId } = ctx.context.payload;
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.deleteSessions(userId);
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, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
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, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
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),