@better-auth/infra 0.1.10 → 0.1.11

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.
Files changed (3) hide show
  1. package/dist/index.d.mts +270 -450
  2. package/dist/index.mjs +273 -316
  3. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1523,6 +1523,7 @@ const paths = [
1523
1523
  "/email-otp/verify-email",
1524
1524
  "/sign-in/email-otp",
1525
1525
  "/sign-in/magic-link",
1526
+ "/sign-in/email",
1526
1527
  "/forget-password/email-otp",
1527
1528
  "/email-otp/reset-password",
1528
1529
  "/email-otp/create-verification-otp",
@@ -1555,25 +1556,6 @@ const signIn = new Set(paths.slice(1, 12));
1555
1556
  * @returns boolean
1556
1557
  */
1557
1558
  const allEmail = ({ path }) => !!path && all.has(path);
1558
- /**
1559
- * Path is one of `[
1560
- * '/email-otp/verify-email',
1561
- * '/sign-in/email-otp',
1562
- * '/sign-in/magic-link',
1563
- * '/sign-in/email',
1564
- * '/forget-password/email-otp',
1565
- * '/email-otp/reset-password',
1566
- * '/email-otp/create-verification-otp',
1567
- * '/email-otp/get-verification-otp',
1568
- * '/email-otp/send-verification-otp',
1569
- * '/forget-password',
1570
- * '/send-verification-email'
1571
- * ]`.
1572
- * @param context Request context
1573
- * @param context.path Request path
1574
- * @returns boolean
1575
- */
1576
- const allEmailSignIn = ({ path }) => !!path && signIn.has(path);
1577
1559
 
1578
1560
  //#endregion
1579
1561
  //#region src/validation/email.ts
@@ -1605,9 +1587,13 @@ const PLUS_ADDRESSING_DOMAINS = new Set([
1605
1587
  * - Remove dots from Gmail-like providers (they ignore dots)
1606
1588
  * - Remove plus addressing (user+tag@domain → user@domain)
1607
1589
  * - Normalize googlemail.com to gmail.com
1590
+ *
1591
+ * @param email - Raw email to normalize
1592
+ * @param context - Auth context with getPlugin (for sentinel policy). Pass undefined when context unavailable (e.g. server, hooks).
1608
1593
  */
1609
- function normalizeEmail(email) {
1594
+ function normalizeEmail(email, context) {
1610
1595
  if (!email || typeof email !== "string") return email;
1596
+ if ((context.getPlugin?.("sentinel"))?.options?.emailValidation?.enabled === false) return email;
1611
1597
  const trimmed = email.trim().toLowerCase();
1612
1598
  const atIndex = trimmed.lastIndexOf("@");
1613
1599
  if (atIndex === -1) return trimmed;
@@ -1700,45 +1686,42 @@ function isValidEmailFormatLocal(email) {
1700
1686
  if (domain.length > 253) return false;
1701
1687
  return true;
1702
1688
  }
1703
- const getEmail = (ctx) => ({
1704
- email: ctx.body?.email ?? ctx.query?.email,
1705
- container: ctx.body ? "body" : "query"
1706
- });
1689
+ const getEmail = (ctx) => {
1690
+ if (ctx.path === "/change-email") return {
1691
+ email: ctx.body?.newEmail,
1692
+ container: "body",
1693
+ field: "newEmail"
1694
+ };
1695
+ const body = ctx.body;
1696
+ const query = ctx.query;
1697
+ return {
1698
+ email: body?.email ?? query?.email,
1699
+ container: body ? "body" : "query",
1700
+ field: "email"
1701
+ };
1702
+ };
1707
1703
  /**
1708
1704
  * Create email normalization hook (shared between all configurations)
1709
1705
  */
1710
1706
  function createEmailNormalizationHook() {
1711
1707
  return {
1712
- matcher: allEmailSignIn,
1708
+ matcher: allEmail,
1713
1709
  handler: createAuthMiddleware(async (ctx) => {
1714
- const { email, container } = getEmail(ctx);
1710
+ const { email, container, field } = getEmail(ctx);
1715
1711
  if (typeof email !== "string") return;
1716
- const normalizedEmail = normalizeEmail(email);
1717
- if (normalizedEmail !== email) {
1718
- const user = await ctx.context.adapter.findOne({
1719
- model: "user",
1720
- where: [{
1721
- field: "normalizedEmail",
1722
- value: normalizedEmail
1723
- }]
1724
- });
1725
- if (!user) return;
1726
- return container === "query" ? { context: {
1727
- ...ctx,
1728
- query: {
1729
- ...ctx.query,
1730
- email: user.email,
1731
- normalizedEmail
1732
- }
1733
- } } : { context: {
1734
- ...ctx,
1735
- body: {
1736
- ...ctx.body,
1737
- email: user.email,
1738
- normalizedEmail
1739
- }
1740
- } };
1741
- }
1712
+ const normalized = normalizeEmail(email, ctx.context);
1713
+ if (normalized === email) return;
1714
+ const data = container === "query" ? {
1715
+ ...ctx.query,
1716
+ [field]: normalized
1717
+ } : {
1718
+ ...ctx.body,
1719
+ [field]: normalized
1720
+ };
1721
+ return { context: {
1722
+ ...ctx,
1723
+ [container]: data
1724
+ } };
1742
1725
  })
1743
1726
  };
1744
1727
  }
@@ -1749,7 +1732,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
1749
1732
  return {
1750
1733
  matcher: allEmail,
1751
1734
  handler: createAuthMiddleware(async (ctx) => {
1752
- const email = ctx.path === "/change-email" ? ctx.body?.newEmail : getEmail(ctx).email;
1735
+ const { email } = getEmail(ctx);
1753
1736
  if (typeof email !== "string") return;
1754
1737
  if (!isValidEmailFormatLocal(email)) throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
1755
1738
  if (validator) {
@@ -1758,7 +1741,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
1758
1741
  if (!policy?.enabled) return;
1759
1742
  const action = policy.action;
1760
1743
  if (!result.valid) {
1761
- if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist") && onDisposableEmail) {
1744
+ if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist" || result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short") && onDisposableEmail) {
1762
1745
  const ip = ctx.request?.headers?.get("x-forwarded-for")?.split(",")[0] || ctx.request?.headers?.get("cf-connecting-ip") || void 0;
1763
1746
  onDisposableEmail({
1764
1747
  email,
@@ -1770,7 +1753,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
1770
1753
  });
1771
1754
  }
1772
1755
  if (action === "allow") return;
1773
- throw new APIError$1("BAD_REQUEST", { 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 === "fake_domain" || result.reason === "fake_pattern" ? "This email address appears to be invalid" : "Invalid email" });
1756
+ throw new APIError$1("BAD_REQUEST", { 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" });
1774
1757
  }
1775
1758
  }
1776
1759
  })
@@ -1809,12 +1792,13 @@ function createEmailHooks(options = {}) {
1809
1792
  ...defaultConfig
1810
1793
  };
1811
1794
  if (!emailConfig.enabled) return { before: [] };
1812
- return { before: [createEmailValidationHook(useApi ? createEmailValidator({
1795
+ const validator = useApi ? createEmailValidator({
1813
1796
  apiUrl,
1814
1797
  kvUrl,
1815
1798
  apiKey,
1816
1799
  defaultConfig: emailConfig
1817
- }) : void 0, onDisposableEmail), createEmailNormalizationHook()] };
1800
+ }) : void 0;
1801
+ return { before: [createEmailNormalizationHook(), createEmailValidationHook(validator, onDisposableEmail)] };
1818
1802
  }
1819
1803
  /**
1820
1804
  * Default email hooks using local validation only
@@ -2084,100 +2068,107 @@ const sentinel = (options) => {
2084
2068
  return {
2085
2069
  id: "sentinel",
2086
2070
  init() {
2087
- return { options: { databaseHooks: {
2088
- user: { create: {
2089
- async before(_user, ctx) {
2090
- if (!ctx) return;
2091
- const visitorId = ctx.context.visitorId;
2092
- if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
2093
- const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
2094
- if (abuseCheck.isAbuse && abuseCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2095
- }
2096
- },
2097
- async after(user, ctx) {
2098
- if (!ctx) return;
2099
- const visitorId = ctx.context.visitorId;
2100
- if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
2101
- }
2102
- } },
2103
- session: { create: {
2104
- async before(session, ctx) {
2105
- if (!ctx) return;
2106
- const visitorId = ctx.context.visitorId;
2107
- const identification = ctx.context.identification;
2108
- if (session.userId && identification?.location && visitorId) {
2109
- const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
2110
- if (travelCheck?.isImpossible) {
2111
- if (travelCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
2112
- if (travelCheck.action === "challenge" && travelCheck.challenge) throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
2071
+ return { options: {
2072
+ emailValidation: opts.security?.emailValidation,
2073
+ databaseHooks: {
2074
+ user: { create: {
2075
+ async before(user, ctx) {
2076
+ if (!ctx) return;
2077
+ const visitorId = ctx.context.visitorId;
2078
+ if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
2079
+ const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
2080
+ if (abuseCheck.isAbuse && abuseCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2113
2081
  }
2082
+ if (user.email && typeof user.email === "string") return { data: {
2083
+ ...user,
2084
+ email: normalizeEmail(user.email, ctx.context)
2085
+ } };
2086
+ },
2087
+ async after(user, ctx) {
2088
+ if (!ctx) return;
2089
+ const visitorId = ctx.context.visitorId;
2090
+ if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
2114
2091
  }
2115
- },
2116
- async after(session, ctx) {
2117
- if (!ctx || !session.userId) return;
2118
- const visitorId = ctx.context.visitorId;
2119
- const identification = ctx.context.identification;
2120
- let user = null;
2121
- try {
2122
- user = await ctx.context.adapter.findOne({
2123
- model: "user",
2124
- select: [
2125
- "email",
2126
- "name",
2127
- "lastActiveAt"
2128
- ],
2129
- where: [{
2130
- field: "id",
2131
- value: session.userId
2132
- }]
2133
- });
2134
- } catch (error) {
2135
- logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2136
- }
2137
- if (visitorId) {
2138
- if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
2139
- }
2140
- if (opts.security?.staleUsers?.enabled && user) {
2141
- const staleCheck = await securityService.checkStaleUser(session.userId, user.lastActiveAt || null);
2142
- if (staleCheck.isStale) {
2143
- const staleOpts = opts.security.staleUsers;
2144
- const notificationPromises = [];
2145
- if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2146
- if (staleCheck.notifyAdmin && staleOpts.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, session.userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2147
- if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
2148
- logger.error("[Sentinel] Failed to send stale account notifications:", error);
2149
- });
2150
- trackEvent({
2151
- eventKey: session.userId,
2152
- eventType: "security_stale_account",
2153
- eventDisplayName: "Security: stale account reactivated",
2154
- eventData: {
2155
- action: staleCheck.action === "block" ? "blocked" : staleCheck.action === "challenge" ? "challenged" : "logged",
2156
- reason: "stale_account_reactivation",
2157
- userId: session.userId,
2158
- daysSinceLastActive: staleCheck.daysSinceLastActive,
2159
- staleDays: staleCheck.staleDays,
2160
- lastActiveAt: staleCheck.lastActiveAt,
2161
- notifyUser: staleCheck.notifyUser,
2162
- notifyAdmin: staleCheck.notifyAdmin,
2163
- detectionLabel: "Stale Account Reactivation",
2164
- description: `Dormant account (inactive for ${staleCheck.daysSinceLastActive} days) became active`
2165
- },
2166
- ipAddress: identification?.ip || void 0,
2167
- city: identification?.location?.city || void 0,
2168
- country: identification?.location?.country?.name || void 0,
2169
- countryCode: identification?.location?.country?.code || void 0
2170
- });
2171
- if (staleCheck.action === "block") throw new APIError("FORBIDDEN", {
2172
- message: "This account has been inactive for an extended period. Please contact support to reactivate.",
2173
- code: "STALE_ACCOUNT"
2092
+ } },
2093
+ session: { create: {
2094
+ async before(session, ctx) {
2095
+ if (!ctx) return;
2096
+ const visitorId = ctx.context.visitorId;
2097
+ const identification = ctx.context.identification;
2098
+ if (session.userId && identification?.location && visitorId) {
2099
+ const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
2100
+ if (travelCheck?.isImpossible) {
2101
+ if (travelCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
2102
+ if (travelCheck.action === "challenge" && travelCheck.challenge) throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
2103
+ }
2104
+ }
2105
+ },
2106
+ async after(session, ctx) {
2107
+ if (!ctx || !session.userId) return;
2108
+ const visitorId = ctx.context.visitorId;
2109
+ const identification = ctx.context.identification;
2110
+ let user = null;
2111
+ try {
2112
+ user = await ctx.context.adapter.findOne({
2113
+ model: "user",
2114
+ select: [
2115
+ "email",
2116
+ "name",
2117
+ "lastActiveAt"
2118
+ ],
2119
+ where: [{
2120
+ field: "id",
2121
+ value: session.userId
2122
+ }]
2174
2123
  });
2124
+ } catch (error) {
2125
+ logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2126
+ }
2127
+ if (visitorId) {
2128
+ if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
2129
+ }
2130
+ if (opts.security?.staleUsers?.enabled && user) {
2131
+ const staleCheck = await securityService.checkStaleUser(session.userId, user.lastActiveAt || null);
2132
+ if (staleCheck.isStale) {
2133
+ const staleOpts = opts.security.staleUsers;
2134
+ const notificationPromises = [];
2135
+ if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2136
+ if (staleCheck.notifyAdmin && staleOpts.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, session.userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2137
+ if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
2138
+ logger.error("[Sentinel] Failed to send stale account notifications:", error);
2139
+ });
2140
+ trackEvent({
2141
+ eventKey: session.userId,
2142
+ eventType: "security_stale_account",
2143
+ eventDisplayName: "Security: stale account reactivated",
2144
+ eventData: {
2145
+ action: staleCheck.action === "block" ? "blocked" : staleCheck.action === "challenge" ? "challenged" : "logged",
2146
+ reason: "stale_account_reactivation",
2147
+ userId: session.userId,
2148
+ daysSinceLastActive: staleCheck.daysSinceLastActive,
2149
+ staleDays: staleCheck.staleDays,
2150
+ lastActiveAt: staleCheck.lastActiveAt,
2151
+ notifyUser: staleCheck.notifyUser,
2152
+ notifyAdmin: staleCheck.notifyAdmin,
2153
+ detectionLabel: "Stale Account Reactivation",
2154
+ description: `Dormant account (inactive for ${staleCheck.daysSinceLastActive} days) became active`
2155
+ },
2156
+ ipAddress: identification?.ip || void 0,
2157
+ city: identification?.location?.city || void 0,
2158
+ country: identification?.location?.country?.name || void 0,
2159
+ countryCode: identification?.location?.country?.code || void 0
2160
+ });
2161
+ if (staleCheck.action === "block") throw new APIError("FORBIDDEN", {
2162
+ message: "This account has been inactive for an extended period. Please contact support to reactivate.",
2163
+ code: "STALE_ACCOUNT"
2164
+ });
2165
+ }
2175
2166
  }
2167
+ if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
2176
2168
  }
2177
- if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
2178
- }
2179
- } }
2180
- } } };
2169
+ } }
2170
+ }
2171
+ } };
2181
2172
  },
2182
2173
  hooks: {
2183
2174
  before: [
@@ -2633,7 +2624,7 @@ const initTeamEvents = (tracker) => {
2633
2624
  //#region src/jwt.ts
2634
2625
  /**
2635
2626
  * Hash the given value
2636
- * Note: Must match @infra/crypto hash()
2627
+ * Note: Must match @infra/utils/crypto hash()
2637
2628
  * @param value - The value to hash
2638
2629
  */
2639
2630
  async function hash(value) {
@@ -2765,7 +2756,7 @@ const getConfig = (options) => {
2765
2756
  schema: plugin.schema,
2766
2757
  options: sanitizePluginOptions(plugin.id, plugin.options)
2767
2758
  };
2768
- }),
2759
+ }) ?? [],
2769
2760
  organization: {
2770
2761
  sendInvitationEmailEnabled: !!organizationPlugin?.options?.sendInvitationEmail,
2771
2762
  additionalFields: (() => {
@@ -2894,7 +2885,8 @@ async function getScimProviderOwner(ctx, organizationId, providerId) {
2894
2885
  });
2895
2886
  return (await ctx.context.adapter.findOne({
2896
2887
  model: "scimProvider",
2897
- where
2888
+ where,
2889
+ select: ["userId"]
2898
2890
  }))?.userId ?? null;
2899
2891
  }
2900
2892
  function getScimEndpoint(baseUrl) {
@@ -3367,7 +3359,8 @@ const acceptInvitation = (options) => {
3367
3359
  throw new APIError("BAD_REQUEST", { message: "This invitation has expired." });
3368
3360
  }
3369
3361
  }
3370
- const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitation.email).then((user$1) => user$1?.user);
3362
+ const invitationEmail = normalizeEmail(invitation.email, ctx.context);
3363
+ const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user$1) => user$1?.user);
3371
3364
  if (existingUser) {
3372
3365
  await $api("/api/internal/invitations/mark-accepted", {
3373
3366
  method: "POST",
@@ -3381,7 +3374,7 @@ const acceptInvitation = (options) => {
3381
3374
  user: existingUser
3382
3375
  });
3383
3376
  const redirectUrl$1 = invitation.redirectUrl || ctx.context.options.baseURL || "/";
3384
- return ctx.redirect(redirectUrl$1);
3377
+ return ctx.redirect(redirectUrl$1.toString());
3385
3378
  }
3386
3379
  if (invitation.authMode === "auth") {
3387
3380
  const platformUrl = options.apiUrl || INFRA_API_URL;
@@ -3392,7 +3385,7 @@ const acceptInvitation = (options) => {
3392
3385
  return ctx.redirect(acceptPageUrl.toString());
3393
3386
  }
3394
3387
  const user = await ctx.context.internalAdapter.createUser({
3395
- email: invitation.email,
3388
+ email: invitationEmail,
3396
3389
  name: invitation.name || invitation.email.split("@")[0] || "",
3397
3390
  emailVerified: true,
3398
3391
  createdAt: /* @__PURE__ */ new Date(),
@@ -3410,7 +3403,7 @@ const acceptInvitation = (options) => {
3410
3403
  user
3411
3404
  });
3412
3405
  const redirectUrl = invitation.redirectUrl || ctx.context.options.baseURL || "/";
3413
- return ctx.redirect(redirectUrl);
3406
+ return ctx.redirect(redirectUrl.toString());
3414
3407
  });
3415
3408
  };
3416
3409
  /**
@@ -3442,7 +3435,8 @@ const completeInvitation = (options) => {
3442
3435
  if (error || !invitation) throw new APIError("BAD_REQUEST", { message: "Invalid or expired invitation." });
3443
3436
  if (invitation.status !== "pending") throw new APIError("BAD_REQUEST", { message: `This invitation has already been ${invitation.status}.` });
3444
3437
  if (!ctx.context) throw new APIError("BAD_REQUEST", { message: "Context is required" });
3445
- const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitation.email).then((user$1) => user$1?.user);
3438
+ const invitationEmail = normalizeEmail(invitation.email, ctx.context);
3439
+ const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user$1) => user$1?.user);
3446
3440
  if (existingUser) {
3447
3441
  await $api("/api/internal/invitations/mark-accepted", {
3448
3442
  method: "POST",
@@ -3461,7 +3455,7 @@ const completeInvitation = (options) => {
3461
3455
  };
3462
3456
  }
3463
3457
  const user = await ctx.context.internalAdapter.createUser({
3464
- email: invitation.email,
3458
+ email: invitationEmail,
3465
3459
  name: invitation.name || invitation.email.split("@")[0] || "",
3466
3460
  emailVerified: true,
3467
3461
  createdAt: /* @__PURE__ */ new Date(),
@@ -3509,7 +3503,8 @@ const checkUserExists = (_options) => {
3509
3503
  }, async (ctx) => {
3510
3504
  const { email } = ctx.body;
3511
3505
  if (!ctx.request?.headers.get("Authorization")) throw new APIError("UNAUTHORIZED", { message: "Authorization required" });
3512
- const existingUser = await ctx.context.internalAdapter.findUserByEmail(email.toLowerCase()).then((user) => user?.user);
3506
+ const normalizedEmail = normalizeEmail(email, ctx.context);
3507
+ const existingUser = await ctx.context.internalAdapter.findUserByEmail(normalizedEmail).then((user) => user?.user);
3513
3508
  return {
3514
3509
  exists: !!existingUser,
3515
3510
  userId: existingUser?.id || null
@@ -3719,7 +3714,7 @@ const deleteOrganizationLogDrain = (options) => {
3719
3714
  body: z$1.object({ logDrainId: z$1.string() })
3720
3715
  }, async (ctx) => {
3721
3716
  const { organizationId } = ctx.context.payload;
3722
- if (!await ctx.context.adapter.findOne({
3717
+ if (await ctx.context.adapter.count({
3723
3718
  model: "orgLogDrain",
3724
3719
  where: [{
3725
3720
  field: "id",
@@ -3728,7 +3723,7 @@ const deleteOrganizationLogDrain = (options) => {
3728
3723
  field: "organizationId",
3729
3724
  value: organizationId
3730
3725
  }]
3731
- })) throw ctx.error("NOT_FOUND", { message: "Log drain not found" });
3726
+ }) === 0) throw ctx.error("NOT_FOUND", { message: "Log drain not found" });
3732
3727
  await ctx.context.adapter.delete({
3733
3728
  model: "orgLogDrain",
3734
3729
  where: [{
@@ -3917,6 +3912,15 @@ const exportFactory = (input, options) => async (ctx) => {
3917
3912
 
3918
3913
  //#endregion
3919
3914
  //#region src/helper.ts
3915
+ /**
3916
+ * Checks whether a plugin is registered by its ID.
3917
+ * Prefers the native `hasPlugin` when available, otherwise
3918
+ * falls back to scanning `options.plugins`.
3919
+ */
3920
+ function hasPlugin(context, pluginId) {
3921
+ if (typeof context.hasPlugin === "function") return context.hasPlugin(pluginId);
3922
+ return context.options.plugins?.some((p) => p.id === pluginId) ?? false;
3923
+ }
3920
3924
  function* chunkArray(arr, options) {
3921
3925
  const batchSize = options?.batchSize || 200;
3922
3926
  for (let i = 0; i < arr.length; i += batchSize) yield arr.slice(i, i + batchSize);
@@ -4129,32 +4133,26 @@ const listOrganizations = (options) => {
4129
4133
  sortBy: {
4130
4134
  field: dbSortBy,
4131
4135
  direction: sortOrder
4132
- }
4136
+ },
4137
+ join: { member: { limit: 5 } }
4133
4138
  }), needsInMemoryProcessing ? Promise.resolve(0) : ctx.context.adapter.count({
4134
4139
  model: "organization",
4135
4140
  where
4136
4141
  })]);
4137
4142
  const orgIds = organizations.map((o) => o.id);
4138
- const allMembers = orgIds.length > 0 ? await ctx.context.adapter.findMany({
4143
+ const memberCounts = await withConcurrency(orgIds, (orgId) => ctx.context.adapter.count({
4139
4144
  model: "member",
4140
4145
  where: [{
4141
4146
  field: "organizationId",
4142
- value: orgIds,
4143
- operator: "in"
4147
+ value: orgId
4144
4148
  }]
4145
- }) : [];
4146
- const membersByOrg = /* @__PURE__ */ new Map();
4147
- for (const m of allMembers) {
4148
- const list = membersByOrg.get(m.organizationId) || [];
4149
- list.push(m);
4150
- membersByOrg.set(m.organizationId, list);
4151
- }
4149
+ }), { concurrency: 10 });
4150
+ const memberCountByOrg = new Map(orgIds.map((orgId, i) => [orgId, memberCounts[i]]));
4152
4151
  let withCounts = organizations.map((organization) => {
4153
- const orgMembers = membersByOrg.get(organization.id) || [];
4152
+ const memberCount = memberCountByOrg.get(organization.id) ?? 0;
4154
4153
  return {
4155
4154
  ...organization,
4156
- _members: orgMembers,
4157
- memberCount: orgMembers.length
4155
+ memberCount
4158
4156
  };
4159
4157
  });
4160
4158
  if (filterMembers) {
@@ -4174,25 +4172,26 @@ const listOrganizations = (options) => {
4174
4172
  const total = needsInMemoryProcessing ? withCounts.length : initialTotal;
4175
4173
  if (needsInMemoryProcessing) withCounts = withCounts.slice(offset, offset + limit);
4176
4174
  const allUserIds = /* @__PURE__ */ new Set();
4177
- for (const organization of withCounts) for (const member of organization._members.slice(0, 5)) allUserIds.add(member.userId);
4175
+ for (const organization of withCounts) for (const member of organization.member) allUserIds.add(member.userId);
4178
4176
  const users = allUserIds.size > 0 ? await ctx.context.adapter.findMany({
4179
4177
  model: "user",
4180
4178
  where: [{
4181
4179
  field: "id",
4182
4180
  value: Array.from(allUserIds),
4183
4181
  operator: "in"
4184
- }]
4182
+ }],
4183
+ limit: allUserIds.size
4185
4184
  }) : [];
4186
4185
  const userMap = new Map(users.map((u) => [u.id, u]));
4187
4186
  return {
4188
4187
  organizations: withCounts.map((organization) => {
4189
- const members = organization._members.slice(0, 5).map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
4188
+ const members = organization.member.map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
4190
4189
  id: u.id,
4191
4190
  name: u.name,
4192
4191
  email: u.email,
4193
4192
  image: u.image
4194
4193
  }));
4195
- const { _members, ...org } = organization;
4194
+ const { member: _members, ...org } = organization;
4196
4195
  return {
4197
4196
  ...org,
4198
4197
  members
@@ -4284,17 +4283,9 @@ const listOrganizationMembers = (options) => {
4284
4283
  where: [{
4285
4284
  field: "organizationId",
4286
4285
  value: ctx.params.id
4287
- }]
4286
+ }],
4287
+ join: { user: true }
4288
4288
  });
4289
- const userIds = members.map((m) => m.userId);
4290
- const users = userIds.length ? await ctx.context.adapter.findMany({
4291
- model: "user",
4292
- where: [{
4293
- field: "id",
4294
- value: userIds,
4295
- operator: "in"
4296
- }]
4297
- }) : [];
4298
4289
  const invitations = await ctx.context.adapter.findMany({
4299
4290
  model: "invitation",
4300
4291
  where: [{
@@ -4303,22 +4294,17 @@ const listOrganizationMembers = (options) => {
4303
4294
  }, {
4304
4295
  field: "status",
4305
4296
  value: "accepted"
4306
- }]
4297
+ }],
4298
+ join: { user: true }
4307
4299
  });
4308
- const inviterIds = [...new Set(invitations.map((i) => i.inviterId).filter(Boolean))];
4309
- const inviters = inviterIds.length ? await ctx.context.adapter.findMany({
4310
- model: "user",
4311
- where: [{
4312
- field: "id",
4313
- value: inviterIds,
4314
- operator: "in"
4315
- }]
4316
- }) : [];
4317
- const inviterById = new Map(inviters.map((u) => [u.id, u]));
4318
- const userById = new Map(users.map((u) => [u.id, u]));
4300
+ const inviterById = /* @__PURE__ */ new Map();
4301
+ for (const inv of invitations) {
4302
+ const inviter = Array.isArray(inv.user) ? inv.user[0] : inv.user;
4303
+ if (inviter && inv.inviterId) inviterById.set(inv.inviterId, inviter);
4304
+ }
4319
4305
  const invitationByEmail = new Map(invitations.map((i) => [i.email.toLowerCase(), i]));
4320
4306
  return members.map((m) => {
4321
- const user = userById.get(m.userId);
4307
+ const user = (Array.isArray(m.user) ? m.user[0] : m.user) ?? null;
4322
4308
  const invitation = user ? invitationByEmail.get(user.email.toLowerCase()) : null;
4323
4309
  const inviter = invitation ? inviterById.get(invitation.inviterId) : null;
4324
4310
  return {
@@ -4351,7 +4337,7 @@ const listOrganizationInvitations = (options) => {
4351
4337
  value: ctx.params.id
4352
4338
  }]
4353
4339
  });
4354
- const emails = [...new Set(invitations.map((i) => i.email.toLowerCase()))];
4340
+ const emails = [...new Set(invitations.map((i) => normalizeEmail(i.email, ctx.context)))];
4355
4341
  const users = emails.length ? await ctx.context.adapter.findMany({
4356
4342
  model: "user",
4357
4343
  where: [{
@@ -4360,9 +4346,10 @@ const listOrganizationInvitations = (options) => {
4360
4346
  operator: "in"
4361
4347
  }]
4362
4348
  }) : [];
4363
- const userByEmail = new Map(users.map((u) => [u.email.toLowerCase(), u]));
4349
+ const userByEmail = new Map(users.map((u) => [normalizeEmail(u.email, ctx.context), u]));
4364
4350
  return invitations.map((invitation) => {
4365
- const user = userByEmail.get(invitation.email.toLowerCase());
4351
+ const invitationEmail = normalizeEmail(invitation.email, ctx.context);
4352
+ const user = userByEmail.get(invitationEmail);
4366
4353
  return {
4367
4354
  ...invitation,
4368
4355
  user: user ? {
@@ -4398,17 +4385,12 @@ const deleteOrganization = (options) => {
4398
4385
  field: "createdAt",
4399
4386
  direction: "asc"
4400
4387
  },
4401
- limit: 1
4388
+ limit: 1,
4389
+ join: { user: true }
4402
4390
  });
4403
4391
  if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4404
4392
  const owner = owners[0];
4405
- const deletedByUser = await ctx.context.adapter.findOne({
4406
- model: "user",
4407
- where: [{
4408
- field: "id",
4409
- value: owner.userId
4410
- }]
4411
- });
4393
+ const deletedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
4412
4394
  if (!deletedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4413
4395
  const organization = await ctx.context.adapter.findOne({
4414
4396
  model: "organization",
@@ -4555,17 +4537,12 @@ const updateTeam = (options) => {
4555
4537
  field: "createdAt",
4556
4538
  direction: "asc"
4557
4539
  },
4558
- limit: 1
4540
+ limit: 1,
4541
+ join: { user: true }
4559
4542
  });
4560
4543
  if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
4561
4544
  const owner = owners[0];
4562
- const user = await ctx.context.adapter.findOne({
4563
- model: "user",
4564
- where: [{
4565
- field: "id",
4566
- value: owner.userId
4567
- }]
4568
- });
4545
+ const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
4569
4546
  if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4570
4547
  let updateData = { updatedAt: /* @__PURE__ */ new Date() };
4571
4548
  if (ctx.body.name) updateData.name = ctx.body.name;
@@ -4649,17 +4626,12 @@ const deleteTeam = (options) => {
4649
4626
  field: "createdAt",
4650
4627
  direction: "asc"
4651
4628
  },
4652
- limit: 1
4629
+ limit: 1,
4630
+ join: { user: true }
4653
4631
  });
4654
4632
  if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
4655
4633
  const owner = owners[0];
4656
- const user = await ctx.context.adapter.findOne({
4657
- model: "user",
4658
- where: [{
4659
- field: "id",
4660
- value: owner.userId
4661
- }]
4662
- });
4634
+ const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
4663
4635
  if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4664
4636
  if (orgOptions?.organizationHooks?.beforeDeleteTeam) await orgOptions.organizationHooks.beforeDeleteTeam({
4665
4637
  team,
@@ -4727,17 +4699,12 @@ const createTeam = (options) => {
4727
4699
  field: "createdAt",
4728
4700
  direction: "asc"
4729
4701
  },
4730
- limit: 1
4702
+ limit: 1,
4703
+ join: { user: true }
4731
4704
  });
4732
4705
  if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
4733
4706
  const owner = owners[0];
4734
- const user = await ctx.context.adapter.findOne({
4735
- model: "user",
4736
- where: [{
4737
- field: "id",
4738
- value: owner.userId
4739
- }]
4740
- });
4707
+ const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
4741
4708
  if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4742
4709
  let teamData = {
4743
4710
  name: ctx.body.name,
@@ -4773,7 +4740,7 @@ const listTeamMembers = (options) => {
4773
4740
  use: [jwtMiddleware(options)]
4774
4741
  }, async (ctx) => {
4775
4742
  try {
4776
- if (!await ctx.context.adapter.findOne({
4743
+ if (await ctx.context.adapter.count({
4777
4744
  model: "team",
4778
4745
  where: [{
4779
4746
  field: "id",
@@ -4782,22 +4749,16 @@ const listTeamMembers = (options) => {
4782
4749
  field: "organizationId",
4783
4750
  value: ctx.params.orgId
4784
4751
  }]
4785
- })) throw ctx.error("NOT_FOUND", { message: "Team not found" });
4786
- const teamMembers = await ctx.context.adapter.findMany({
4752
+ }) === 0) throw ctx.error("NOT_FOUND", { message: "Team not found" });
4753
+ return (await ctx.context.adapter.findMany({
4787
4754
  model: "teamMember",
4788
4755
  where: [{
4789
4756
  field: "teamId",
4790
4757
  value: ctx.params.teamId
4791
- }]
4792
- });
4793
- return await Promise.all(teamMembers.map(async (tm) => {
4794
- const user = await ctx.context.adapter.findOne({
4795
- model: "user",
4796
- where: [{
4797
- field: "id",
4798
- value: tm.userId
4799
- }]
4800
- });
4758
+ }],
4759
+ join: { user: true }
4760
+ })).map((tm) => {
4761
+ const user = Array.isArray(tm.user) ? tm.user[0] : tm.user;
4801
4762
  return {
4802
4763
  ...tm,
4803
4764
  user: user ? {
@@ -4807,7 +4768,7 @@ const listTeamMembers = (options) => {
4807
4768
  image: user.image
4808
4769
  } : null
4809
4770
  };
4810
- }));
4771
+ });
4811
4772
  } catch (e) {
4812
4773
  ctx.context.logger.warn("[Dash] Failed to list team members:", e);
4813
4774
  return [];
@@ -4865,7 +4826,7 @@ const addTeamMember = (options) => {
4865
4826
  value: organizationId
4866
4827
  }]
4867
4828
  })) throw ctx.error("BAD_REQUEST", { message: "User is not a member of this organization" });
4868
- if (await ctx.context.adapter.findOne({
4829
+ if (await ctx.context.adapter.count({
4869
4830
  model: "teamMember",
4870
4831
  where: [{
4871
4832
  field: "teamId",
@@ -4874,7 +4835,7 @@ const addTeamMember = (options) => {
4874
4835
  field: "userId",
4875
4836
  value: ctx.body.userId
4876
4837
  }]
4877
- })) throw ctx.error("BAD_REQUEST", { message: "User is already a member of this team" });
4838
+ }) > 0) throw ctx.error("BAD_REQUEST", { message: "User is already a member of this team" });
4878
4839
  if (orgOptions?.teams?.maximumMembersPerTeam) {
4879
4840
  const teamMemberCount = await ctx.context.adapter.count({
4880
4841
  model: "teamMember",
@@ -5023,13 +4984,13 @@ const createOrganization = (options) => {
5023
4984
  });
5024
4985
  if (!user) throw ctx.error("BAD_REQUEST", { message: "User not found" });
5025
4986
  const orgOptions = organizationPlugin.options || {};
5026
- if (await ctx.context.adapter.findOne({
4987
+ if (await ctx.context.adapter.count({
5027
4988
  model: "organization",
5028
4989
  where: [{
5029
4990
  field: "slug",
5030
4991
  value: ctx.body.slug
5031
4992
  }]
5032
- })) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
4993
+ }) > 0) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
5033
4994
  let orgData = {
5034
4995
  ...ctx.body,
5035
4996
  defaultTeamName: void 0
@@ -5178,14 +5139,15 @@ const updateOrganization = (options) => {
5178
5139
  const { organizationId } = ctx.context.payload;
5179
5140
  const orgOptions = ctx.context.getPlugin("organization")?.options || {};
5180
5141
  if (ctx.body.slug) {
5181
- const existingOrg = await ctx.context.adapter.findOne({
5142
+ const orgWithSlug = await ctx.context.adapter.findOne({
5182
5143
  model: "organization",
5183
5144
  where: [{
5184
5145
  field: "slug",
5185
5146
  value: ctx.body.slug
5186
- }]
5147
+ }],
5148
+ select: ["id"]
5187
5149
  });
5188
- if (existingOrg && existingOrg.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
5150
+ if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
5189
5151
  }
5190
5152
  const owners = await ctx.context.adapter.findMany({
5191
5153
  model: "member",
@@ -5200,17 +5162,12 @@ const updateOrganization = (options) => {
5200
5162
  field: "createdAt",
5201
5163
  direction: "asc"
5202
5164
  },
5203
- limit: 1
5165
+ limit: 1,
5166
+ join: { user: true }
5204
5167
  });
5205
5168
  if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5206
5169
  const owner = owners[0];
5207
- const updatedByUser = await ctx.context.adapter.findOne({
5208
- model: "user",
5209
- where: [{
5210
- field: "id",
5211
- value: owner.userId
5212
- }]
5213
- });
5170
+ const updatedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
5214
5171
  if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5215
5172
  let updateData = { ...ctx.body };
5216
5173
  if (typeof updateData.metadata === "string") try {
@@ -5472,10 +5429,11 @@ const inviteMember = (options) => {
5472
5429
  }]
5473
5430
  });
5474
5431
  if (!invitedBy) throw ctx.error("BAD_REQUEST", { message: "Invited by user not found" });
5432
+ const invitationEmail = normalizeEmail(ctx.body.email, ctx.context);
5475
5433
  return await organizationPlugin.endpoints.createInvitation({
5476
5434
  headers: ctx.request?.headers ?? new Headers(),
5477
5435
  body: {
5478
- email: ctx.body.email,
5436
+ email: invitationEmail,
5479
5437
  role: ctx.body.role,
5480
5438
  organizationId
5481
5439
  },
@@ -5497,11 +5455,12 @@ const checkUserByEmail = (options) => {
5497
5455
  use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
5498
5456
  }, async (ctx) => {
5499
5457
  const { organizationId } = ctx.context.payload;
5458
+ const email = normalizeEmail(ctx.body.email, ctx.context);
5500
5459
  const user = await ctx.context.adapter.findOne({
5501
5460
  model: "user",
5502
5461
  where: [{
5503
5462
  field: "email",
5504
- value: ctx.body.email
5463
+ value: email
5505
5464
  }]
5506
5465
  });
5507
5466
  if (!user) return {
@@ -6095,10 +6054,11 @@ const resendInvitation = (options) => {
6095
6054
  });
6096
6055
  if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
6097
6056
  if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
6057
+ const invitationEmail = normalizeEmail(invitation.email, ctx.context);
6098
6058
  await organizationPlugin.endpoints.createInvitation({
6099
6059
  headers: ctx.request?.headers ?? new Headers(),
6100
6060
  body: {
6101
- email: invitation.email,
6061
+ email: invitationEmail,
6102
6062
  role: invitation.role,
6103
6063
  organizationId,
6104
6064
  resend: true
@@ -6137,28 +6097,31 @@ const listAllSessions = (options) => {
6137
6097
  }).optional()
6138
6098
  }, async (ctx) => {
6139
6099
  const sessionsCount = await ctx.context.adapter.count({ model: "session" });
6100
+ const limit = ctx.query?.limit || sessionsCount;
6101
+ const offset = ctx.query?.offset || 0;
6140
6102
  const sessions = await ctx.context.adapter.findMany({
6141
6103
  model: "session",
6142
- limit: ctx.query?.limit || sessionsCount,
6143
- offset: ctx.query?.offset || 0,
6104
+ limit,
6105
+ offset,
6144
6106
  sortBy: {
6145
6107
  field: "createdAt",
6146
6108
  direction: "desc"
6147
- }
6148
- });
6149
- return (await ctx.context.adapter.findMany({
6150
- model: "user",
6151
- where: [{
6152
- field: "id",
6153
- value: sessions.map((s) => s.userId),
6154
- operator: "in"
6155
- }]
6156
- })).map((u) => {
6157
- return {
6158
- ...u,
6159
- sessions: sessions.filter((s) => s.userId === u.id)
6160
- };
6161
- });
6109
+ },
6110
+ join: { user: true }
6111
+ });
6112
+ const userMap = /* @__PURE__ */ new Map();
6113
+ for (const s of sessions) {
6114
+ const user = Array.isArray(s.user) ? s.user[0] : s.user;
6115
+ if (!user) continue;
6116
+ const { user: _u, ...sessionData } = s;
6117
+ const session = sessionData;
6118
+ if (!userMap.has(user.id)) userMap.set(user.id, {
6119
+ ...user,
6120
+ sessions: []
6121
+ });
6122
+ userMap.get(user.id).sessions.push(session);
6123
+ }
6124
+ return Array.from(userMap.values());
6162
6125
  });
6163
6126
  };
6164
6127
  const revokeSession = (options) => createAuthEndpoint("/dash/sessions/revoke", {
@@ -6267,7 +6230,7 @@ const getUsers = (options) => {
6267
6230
  totalQuery,
6268
6231
  onlineUsersQuery
6269
6232
  ]);
6270
- const hasAdminPlugin = ctx.context.hasPlugin("admin");
6233
+ const hasAdminPlugin = hasPlugin(ctx.context, "admin");
6271
6234
  return {
6272
6235
  users: users.map((user) => {
6273
6236
  const u = user;
@@ -6292,7 +6255,7 @@ const exportUsers = (options) => {
6292
6255
  use: [jwtMiddleware(options)],
6293
6256
  query: getUsersQuerySchema
6294
6257
  }, async (ctx) => {
6295
- const hasAdminPlugin = ctx.context.hasPlugin("admin");
6258
+ const hasAdminPlugin = hasPlugin(ctx.context, "admin");
6296
6259
  return exportFactory({
6297
6260
  model: "user",
6298
6261
  limit: ctx.query?.limit,
@@ -6428,7 +6391,8 @@ const createUser = (options) => {
6428
6391
  }).passthrough()
6429
6392
  }, async (ctx) => {
6430
6393
  const userData = ctx.body;
6431
- if (await ctx.context.internalAdapter.findUserByEmail(userData.email)) throw new APIError("BAD_REQUEST", { message: "User with this email already exist" });
6394
+ const email = normalizeEmail(userData.email, ctx.context);
6395
+ if (await ctx.context.internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", { message: "User with this email already exist" });
6432
6396
  let password = null;
6433
6397
  if (userData.generatePassword && !userData.password) password = generateId(12);
6434
6398
  else if (userData.password && userData.password.trim() !== "") password = userData.password;
@@ -6449,6 +6413,7 @@ const createUser = (options) => {
6449
6413
  }
6450
6414
  const user = await ctx.context.internalAdapter.createUser({
6451
6415
  ...userData,
6416
+ email,
6452
6417
  emailVerified: userData.emailVerified,
6453
6418
  createdAt: /* @__PURE__ */ new Date(),
6454
6419
  updatedAt: /* @__PURE__ */ new Date()
@@ -6562,7 +6527,7 @@ const getUserDetails = (options) => {
6562
6527
  }, async (ctx) => {
6563
6528
  const { userId } = ctx.context.payload;
6564
6529
  const minimal = !!ctx.query?.minimal;
6565
- const hasAdminPlugin = ctx.context.hasPlugin("admin");
6530
+ const hasAdminPlugin = hasPlugin(ctx.context, "admin");
6566
6531
  const user = await ctx.context.adapter.findOne({
6567
6532
  model: "user",
6568
6533
  where: [{
@@ -6632,47 +6597,39 @@ const getUserOrganizations = (options) => {
6632
6597
  const isOrgEnabled = ctx.context.getPlugin("organization");
6633
6598
  if (!isOrgEnabled) return { organizations: [] };
6634
6599
  const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
6635
- const [member, teamMembers] = await Promise.all([ctx.context.adapter.findMany({
6600
+ const [membersWithOrg, teamMembersWithTeam] = await Promise.all([ctx.context.adapter.findMany({
6636
6601
  model: "member",
6637
6602
  where: [{
6638
6603
  field: "userId",
6639
6604
  value: userId
6640
- }]
6605
+ }],
6606
+ join: { organization: true }
6641
6607
  }), isTeamEnabled ? ctx.context.adapter.findMany({
6642
6608
  model: "teamMember",
6643
6609
  where: [{
6644
6610
  field: "userId",
6645
6611
  value: userId
6646
- }]
6612
+ }],
6613
+ join: { team: true }
6647
6614
  }).catch((e) => {
6648
6615
  ctx.context.logger.error("[Dash] Failed to fetch team members:", e);
6649
6616
  return [];
6650
6617
  }) : Promise.resolve([])]);
6651
- if (member.length === 0) return { organizations: [] };
6652
- const [organizations, teams] = await Promise.all([ctx.context.adapter.findMany({
6653
- model: "organization",
6654
- where: [{
6655
- field: "id",
6656
- value: member.map((m) => m.organizationId),
6657
- operator: "in"
6658
- }]
6659
- }), isTeamEnabled && teamMembers.length > 0 ? ctx.context.adapter.findMany({
6660
- model: "team",
6661
- where: [{
6662
- field: "id",
6663
- value: teamMembers.map((tm) => tm.teamId),
6664
- operator: "in"
6665
- }]
6666
- }) : Promise.resolve([])]);
6667
- return { organizations: organizations.map((organization) => ({
6668
- id: organization.id,
6669
- name: organization.name,
6670
- logo: organization.logo,
6671
- createdAt: organization.createdAt,
6672
- slug: organization.slug,
6673
- role: member.find((m) => m.organizationId === organization.id)?.role,
6674
- teams: teams.filter((team) => team.organizationId === organization.id)
6675
- })) };
6618
+ if (membersWithOrg.length === 0) return { organizations: [] };
6619
+ const teams = teamMembersWithTeam.map((tm) => Array.isArray(tm.team) ? tm.team[0] : tm.team).filter((t) => t != null);
6620
+ return { organizations: membersWithOrg.map((m) => {
6621
+ const organization = Array.isArray(m.organization) ? m.organization[0] : m.organization;
6622
+ if (!organization) return null;
6623
+ return {
6624
+ id: organization.id,
6625
+ name: organization.name,
6626
+ logo: organization.logo,
6627
+ createdAt: organization.createdAt,
6628
+ slug: organization.slug,
6629
+ role: m.role,
6630
+ teams: teams.filter((team) => team.organizationId === organization.id)
6631
+ };
6632
+ }).filter(Boolean) };
6676
6633
  });
6677
6634
  };
6678
6635
  const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
@@ -8051,4 +8008,4 @@ const dash = (options) => {
8051
8008
  };
8052
8009
 
8053
8010
  //#endregion
8054
- export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
8011
+ export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, normalizeEmail, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };