@better-auth/infra 0.3.3 → 0.3.5

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,5 +1,5 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-z_1dN2O_.mjs";
2
- import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-yhK7ZLPn.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-COVOGExF.mjs";
2
+ import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-DvC9lT5e.mjs";
3
3
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
4
  import { getCurrentAuthContext } from "@better-auth/core/context";
5
5
  import { APIError, generateId, getAuthTables, logger } from "better-auth";
@@ -148,6 +148,15 @@ const USER_EVENT_TYPES = {
148
148
  };
149
149
  //#endregion
150
150
  //#region src/events/core/adapter.ts
151
+ function resolveUserFromContext(userId, ctx) {
152
+ for (const candidate of [ctx.context.session?.user, ctx.context.newSession?.user]) if (candidate?.id === userId) return {
153
+ id: candidate.id,
154
+ name: candidate.name,
155
+ email: candidate.email ?? null,
156
+ ..."lastActiveAt" in candidate && candidate.lastActiveAt !== void 0 ? { lastActiveAt: candidate.lastActiveAt } : {}
157
+ };
158
+ return null;
159
+ }
151
160
  const getUserByEmail = async (email, ctx) => {
152
161
  let user = null;
153
162
  try {
@@ -168,25 +177,26 @@ const getUserByEmail = async (email, ctx) => {
168
177
  }
169
178
  return user;
170
179
  };
171
- async function getUserById(userId, ctx) {
172
- let user = null;
180
+ async function getUserById(userId, ctx, options) {
181
+ const select = [
182
+ "id",
183
+ "name",
184
+ "email"
185
+ ];
186
+ if (options?.includeLastActiveAt) select.push("lastActiveAt");
173
187
  try {
174
- user = await ctx.context.adapter.findOne({
188
+ return await ctx.context.adapter.findOne({
175
189
  model: "user",
176
- select: [
177
- "id",
178
- "name",
179
- "email"
180
- ],
190
+ select,
181
191
  where: [{
182
192
  field: "id",
183
193
  value: userId
184
194
  }]
185
- });
195
+ }) ?? null;
186
196
  } catch (error) {
187
197
  logger.debug("[Dash] Failed to fetch user info:", error);
198
+ return null;
188
199
  }
189
- return user;
190
200
  }
191
201
  //#endregion
192
202
  //#region src/events/core/events-account.ts
@@ -293,6 +303,8 @@ const matchesAnyRoute = (path, routes) => {
293
303
  const cleanPath = stripQuery(path);
294
304
  return routes.some((route) => routeToRegex(route).test(cleanPath));
295
305
  };
306
+ /** True for `/dash` and any `/dash/*` handler (dashboard admin API). */
307
+ const isDashRoute = (path) => matchesAnyRoute(path, ["/dash"]);
296
308
  //#endregion
297
309
  //#region src/events/oauth-callback-provider.ts
298
310
  /** Provider id from social (`/callback/:id`) or generic OAuth (`/oauth2/callback/:providerId`). */
@@ -379,10 +391,10 @@ async function resolveOAuthUser(providerId, ctx) {
379
391
  //#region src/events/core/events-session.ts
380
392
  const initSessionEvents = (tracker) => {
381
393
  const { trackEvent } = tracker;
382
- const trackUserSignedIn = (session, trigger, ctx, location) => {
394
+ const trackUserSignedIn = (session, trigger, ctx, location, knownUser) => {
383
395
  const track = async () => {
384
396
  const loginMethod = session.loginMethod ?? "unknown";
385
- const user = await getUserById(session.userId, ctx);
397
+ const user = knownUser ?? await getUserById(session.userId, ctx);
386
398
  trackEvent({
387
399
  eventKey: session.userId,
388
400
  eventType: EVENT_TYPES.USER_SIGNED_IN,
@@ -405,10 +417,10 @@ const initSessionEvents = (tracker) => {
405
417
  };
406
418
  ctx.context.runInBackground(track());
407
419
  };
408
- const trackUserSignedOut = (session, trigger, ctx, location) => {
420
+ const trackUserSignedOut = (session, trigger, ctx, location, knownUser) => {
409
421
  const track = async () => {
410
422
  const loginMethod = session.loginMethod ?? "unknown";
411
- const user = await getUserById(session.userId, ctx);
423
+ const user = knownUser ?? await getUserById(session.userId, ctx);
412
424
  trackEvent({
413
425
  eventKey: session.userId,
414
426
  eventType: EVENT_TYPES.USER_SIGNED_OUT,
@@ -431,10 +443,10 @@ const initSessionEvents = (tracker) => {
431
443
  };
432
444
  ctx.context.runInBackground(track());
433
445
  };
434
- const trackSessionRevoked = (session, trigger, ctx, location) => {
446
+ const trackSessionRevoked = (session, trigger, ctx, location, knownUser) => {
435
447
  const track = async () => {
436
448
  const loginMethod = session.loginMethod ?? "unknown";
437
- const user = await getUserById(session.userId, ctx);
449
+ const user = knownUser ?? await getUserById(session.userId, ctx);
438
450
  trackEvent({
439
451
  eventKey: session.userId,
440
452
  eventType: EVENT_TYPES.SESSION_REVOKED,
@@ -457,9 +469,9 @@ const initSessionEvents = (tracker) => {
457
469
  };
458
470
  ctx.context.runInBackground(track());
459
471
  };
460
- const trackSessionRevokedAll = (session, trigger, ctx, location) => {
472
+ const trackSessionRevokedAll = (session, trigger, ctx, location, knownUser) => {
461
473
  const track = async () => {
462
- const user = await getUserById(session.userId, ctx);
474
+ const user = knownUser ?? await getUserById(session.userId, ctx);
463
475
  trackEvent({
464
476
  eventKey: session.userId,
465
477
  eventType: EVENT_TYPES.ALL_SESSIONS_REVOKED,
@@ -479,11 +491,11 @@ const initSessionEvents = (tracker) => {
479
491
  };
480
492
  ctx.context.runInBackground(track());
481
493
  };
482
- const trackUserImpersonated = (session, trigger, ctx, location) => {
494
+ const trackUserImpersonated = (session, trigger, ctx, location, knownUser, knownImpersonator) => {
483
495
  const track = async () => {
484
496
  const loginMethod = session.loginMethod ?? "unknown";
485
- const user = await getUserById(session.userId, ctx);
486
- const impersonator = session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null;
497
+ const user = knownUser ?? await getUserById(session.userId, ctx);
498
+ const impersonator = knownImpersonator ?? (session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null);
487
499
  trackEvent({
488
500
  eventKey: session.userId,
489
501
  eventType: EVENT_TYPES.USER_IMPERSONATED,
@@ -508,11 +520,11 @@ const initSessionEvents = (tracker) => {
508
520
  };
509
521
  ctx.context.runInBackground(track());
510
522
  };
511
- const trackUserImpersonationStop = (session, trigger, ctx, location) => {
523
+ const trackUserImpersonationStop = (session, trigger, ctx, location, knownUser, knownImpersonator) => {
512
524
  const track = async () => {
513
525
  const loginMethod = session.loginMethod ?? "unknown";
514
- const user = await getUserById(session.userId, ctx);
515
- const impersonator = session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null;
526
+ const user = knownUser ?? await getUserById(session.userId, ctx);
527
+ const impersonator = knownImpersonator ?? (session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null);
516
528
  trackEvent({
517
529
  eventKey: session.userId,
518
530
  eventType: EVENT_TYPES.USER_IMPERSONATED_STOPPED,
@@ -537,10 +549,10 @@ const initSessionEvents = (tracker) => {
537
549
  };
538
550
  ctx.context.runInBackground(track());
539
551
  };
540
- const trackSessionCreated = (session, trigger, ctx, location) => {
552
+ const trackSessionCreated = (session, trigger, ctx, location, knownUser) => {
541
553
  const track = async () => {
542
554
  const loginMethod = session.loginMethod ?? "unknown";
543
- const user = await getUserById(session.userId, ctx);
555
+ const user = knownUser ?? await getUserById(session.userId, ctx);
544
556
  trackEvent({
545
557
  eventKey: session.userId,
546
558
  eventType: EVENT_TYPES.SESSION_CREATED,
@@ -1033,12 +1045,19 @@ function resolveUntrustedVisitorId(visitorId, ip, headerVisitorId) {
1033
1045
  *
1034
1046
  * @param $kv — KV client from {@link createKV} for the same plugin options (one instance per plugin).
1035
1047
  */
1036
- function createIdentificationMiddleware($kv) {
1048
+ function createIdentificationMiddleware($kv, options) {
1037
1049
  return createAuthMiddleware(async (ctx) => {
1038
- const { visitorId: headerVisitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
1039
- const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
1050
+ const skipIdentification = options?.skipIdentification?.(ctx) ?? false;
1051
+ let headerVisitorId = null;
1052
+ let requestId = null;
1053
+ if (!skipIdentification) {
1054
+ const extracted = extractIdentificationHeaders(ctx.request);
1055
+ headerVisitorId = extracted.visitorId;
1056
+ requestId = extracted.requestId ?? ctx.getCookie("__infra-rid") ?? null;
1057
+ }
1040
1058
  ctx.context.requestId = requestId;
1041
- if (requestId) {
1059
+ if (skipIdentification) ctx.context.identification = null;
1060
+ else if (requestId) {
1042
1061
  if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv) ?? null;
1043
1062
  } else ctx.context.identification = null;
1044
1063
  const identification = ctx.context.identification;
@@ -1064,7 +1083,7 @@ function createIdentificationMiddleware($kv) {
1064
1083
  else location = void 0;
1065
1084
  ctx.context.location = location;
1066
1085
  ctx.context.ip = resolveSecurityIp(identification, location);
1067
- ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
1086
+ ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, skipIdentification ? null : headerVisitorId);
1068
1087
  });
1069
1088
  }
1070
1089
  /**
@@ -2210,6 +2229,13 @@ const sentinel = (options) => {
2210
2229
  create: {
2211
2230
  async before(user, ctx) {
2212
2231
  if (!ctx) return;
2232
+ if (isDashRoute(ctx.path)) {
2233
+ if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2234
+ ...user,
2235
+ email: normalizeEmail(user.email, ctx.context)
2236
+ } };
2237
+ return;
2238
+ }
2213
2239
  const visitorId = ctx.context.visitorId;
2214
2240
  if (opts.security?.freeTrialAbuse?.enabled) {
2215
2241
  if (!visitorId) {
@@ -2237,7 +2263,7 @@ const sentinel = (options) => {
2237
2263
  } };
2238
2264
  },
2239
2265
  async after(user, ctx) {
2240
- if (!ctx) return;
2266
+ if (!ctx || isDashRoute(ctx.path)) return;
2241
2267
  const visitorId = ctx.context.visitorId;
2242
2268
  const reservationId = ctx.context.freeTrialReservationId;
2243
2269
  if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
@@ -2253,7 +2279,7 @@ const sentinel = (options) => {
2253
2279
  },
2254
2280
  session: { create: {
2255
2281
  async before(session, ctx) {
2256
- if (!ctx) return;
2282
+ if (!ctx || isDashRoute(ctx.path)) return;
2257
2283
  const visitorId = ctx.context.visitorId;
2258
2284
  const identification = ctx.context.identification;
2259
2285
  if (session.userId && identification?.location && visitorId) {
@@ -2286,24 +2312,12 @@ const sentinel = (options) => {
2286
2312
  }
2287
2313
  },
2288
2314
  async after(session, ctx) {
2289
- if (!ctx || !session.userId) return;
2315
+ if (!ctx || !session.userId || isDashRoute(ctx.path)) return;
2290
2316
  const visitorId = ctx.context.visitorId;
2291
2317
  const identification = ctx.context.identification;
2292
2318
  let user = null;
2293
- const userSelect = [
2294
- "email",
2295
- "name",
2296
- ...activityTrackingEnabled ? ["lastActiveAt"] : []
2297
- ];
2298
2319
  try {
2299
- user = await ctx.context.adapter.findOne({
2300
- model: "user",
2301
- select: [...userSelect],
2302
- where: [{
2303
- field: "id",
2304
- value: session.userId
2305
- }]
2306
- });
2320
+ user = await getUserById(session.userId, ctx, { includeLastActiveAt: activityTrackingEnabled });
2307
2321
  } catch (error) {
2308
2322
  logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2309
2323
  }
@@ -2349,12 +2363,12 @@ const sentinel = (options) => {
2349
2363
  before: [
2350
2364
  {
2351
2365
  matcher: (ctx) => ctx.request?.method !== "GET",
2352
- handler: createIdentificationMiddleware($kv)
2366
+ handler: createIdentificationMiddleware($kv, { skipIdentification: (ctx) => isDashRoute(ctx.path) })
2353
2367
  },
2354
2368
  ...emailHooks.before,
2355
2369
  ...phoneValidationHooks.before,
2356
2370
  {
2357
- matcher: (ctx) => ctx.request?.method !== "GET",
2371
+ matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
2358
2372
  handler: createAuthMiddleware(async (ctx) => {
2359
2373
  const visitorId = ctx.context.visitorId;
2360
2374
  const identification = ctx.context.identification;
@@ -2448,7 +2462,7 @@ const sentinel = (options) => {
2448
2462
  }
2449
2463
  ],
2450
2464
  after: [{
2451
- matcher: (ctx) => ctx.request?.method !== "GET",
2465
+ matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
2452
2466
  handler: createAuthMiddleware(async (ctx) => {
2453
2467
  const untrustedVisitorId = ctx.context.untrustedVisitorId;
2454
2468
  const ip = ctx.context.ip;
@@ -3332,7 +3346,7 @@ async function buildSyntheticSession(ctx, ownerUserId) {
3332
3346
  }
3333
3347
  //#endregion
3334
3348
  //#region src/routes/directory-sync/index.ts
3335
- async function getScimProviderOwner(ctx, organizationId, providerId) {
3349
+ async function getLegacyScimProviderOwnerUserId(ctx, organizationId, providerId) {
3336
3350
  const where = [{
3337
3351
  field: "organizationId",
3338
3352
  value: organizationId
@@ -3347,6 +3361,44 @@ async function getScimProviderOwner(ctx, organizationId, providerId) {
3347
3361
  select: ["userId"]
3348
3362
  }))?.userId ?? null;
3349
3363
  }
3364
+ function parseMemberRoles(role) {
3365
+ return role.split(",").map((entry) => entry.trim()).filter(Boolean);
3366
+ }
3367
+ function getScimManagementRoles(ctx) {
3368
+ const creatorRole = ctx.context.getPlugin("organization")?.options?.creatorRole ?? "owner";
3369
+ return Array.from(new Set(["admin", creatorRole]));
3370
+ }
3371
+ async function findOrganizationScimManagerUserId(ctx, organizationId) {
3372
+ const requiredRoles = getScimManagementRoles(ctx);
3373
+ const members = await ctx.context.adapter.findMany({
3374
+ model: "member",
3375
+ where: [{
3376
+ field: "organizationId",
3377
+ value: organizationId
3378
+ }],
3379
+ select: ["userId", "role"]
3380
+ });
3381
+ for (const member of members) if (parseMemberRoles(member.role).some((role) => requiredRoles.includes(role))) return member.userId;
3382
+ return null;
3383
+ }
3384
+ async function organizationHasScimProviders(ctx, organizationId) {
3385
+ return await ctx.context.adapter.findOne({
3386
+ model: "scimProvider",
3387
+ where: [{
3388
+ field: "organizationId",
3389
+ value: organizationId
3390
+ }],
3391
+ select: ["id"]
3392
+ }) != null;
3393
+ }
3394
+ /** User id for synthetic SCIM management sessions (list/delete/regenerate). */
3395
+ async function resolveScimManagementUserId(ctx, organizationId, providerId) {
3396
+ if (providerId) {
3397
+ if (!await scimProviderExists(ctx, organizationId, providerId)) return null;
3398
+ } else if (!await organizationHasScimProviders(ctx, organizationId)) return null;
3399
+ if (isVersionAtLeast(ctx.context.version, "1.7.0")) return findOrganizationScimManagerUserId(ctx, organizationId);
3400
+ return getLegacyScimProviderOwnerUserId(ctx, organizationId, providerId);
3401
+ }
3350
3402
  async function scimProviderExists(ctx, organizationId, providerId) {
3351
3403
  return await ctx.context.adapter.findOne({
3352
3404
  model: "scimProvider",
@@ -3391,14 +3443,14 @@ const listOrganizationDirectories = (options) => {
3391
3443
  ctx.context.logger.warn("[Dash] SCIM plugin not available or provider ownership disabled, returning empty directories list", { organizationId });
3392
3444
  return [];
3393
3445
  }
3394
- const ownerUserId = await getScimProviderOwner(ctx, organizationId);
3395
- if (!ownerUserId) {
3396
- ctx.context.logger.warn("[Dash] No SCIM provider owner found for organization, returning empty directories list", { organizationId });
3446
+ const managerUserId = await resolveScimManagementUserId(ctx, organizationId);
3447
+ if (!managerUserId) {
3448
+ ctx.context.logger.warn("[Dash] No SCIM provider or organization manager found, returning empty directories list", { organizationId });
3397
3449
  return [];
3398
3450
  }
3399
3451
  try {
3400
3452
  const { providers } = await scimPlugin.endpoints.listSCIMProviderConnections({
3401
- context: await buildSyntheticSession(ctx, ownerUserId),
3453
+ context: await buildSyntheticSession(ctx, managerUserId),
3402
3454
  headers: ctx.request?.headers ?? new Headers()
3403
3455
  });
3404
3456
  return providers.filter((p) => p.organizationId === organizationId).map((provider) => ({
@@ -3463,11 +3515,14 @@ const deleteOrganizationDirectory = (options) => {
3463
3515
  if (!scimPlugin?.endpoints.deleteSCIMProviderConnection || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
3464
3516
  const { organizationId } = ctx.context.payload;
3465
3517
  const { providerId } = ctx.body;
3466
- const ownerUserId = await getScimProviderOwner(ctx, organizationId, providerId);
3467
- if (!ownerUserId) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found or missing owner" });
3518
+ const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
3519
+ if (!managerUserId) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found or missing owner" });
3468
3520
  await scimPlugin.endpoints.deleteSCIMProviderConnection({
3469
- body: { providerId },
3470
- context: await buildSyntheticSession(ctx, ownerUserId),
3521
+ body: {
3522
+ providerId,
3523
+ organizationId
3524
+ },
3525
+ context: await buildSyntheticSession(ctx, managerUserId),
3471
3526
  headers: ctx.request?.headers ?? new Headers()
3472
3527
  });
3473
3528
  return { success: true };
@@ -3484,14 +3539,14 @@ const regenerateDirectoryToken = (options) => {
3484
3539
  if (!scimPlugin?.endpoints.generateSCIMToken || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
3485
3540
  const { organizationId } = ctx.context.payload;
3486
3541
  const { providerId } = ctx.body;
3487
- const ownerUserId = await getScimProviderOwner(ctx, organizationId, providerId);
3488
- if (!ownerUserId) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found or missing owner" });
3542
+ const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
3543
+ if (!managerUserId) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found or missing owner" });
3489
3544
  const { scimToken } = await scimPlugin.endpoints.generateSCIMToken({
3490
3545
  body: {
3491
3546
  providerId,
3492
3547
  organizationId
3493
3548
  },
3494
- context: await buildSyntheticSession(ctx, ownerUserId),
3549
+ context: await buildSyntheticSession(ctx, managerUserId),
3495
3550
  headers: ctx.request?.headers ?? new Headers()
3496
3551
  });
3497
3552
  if (!scimToken) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to regenerate directory sync token" });
@@ -4251,6 +4306,13 @@ const checkUserExists = (options) => {
4251
4306
  });
4252
4307
  };
4253
4308
  //#endregion
4309
+ //#region src/db-types.ts
4310
+ /** Unwrap a joined relation that may be a single item or an array. */
4311
+ function joinedRelation(value) {
4312
+ if (value == null) return null;
4313
+ return (Array.isArray(value) ? value[0] : value) ?? null;
4314
+ }
4315
+ //#endregion
4254
4316
  //#region src/routes/organizations/roles.ts
4255
4317
  const DEFAULT_ORG_MEMBER_ROLES = [
4256
4318
  "member",
@@ -4268,6 +4330,14 @@ function validateOrganizationMemberRole(ctx, role, orgOptions) {
4268
4330
  if (!allowedRoles.includes(role)) throw ctx.error("BAD_REQUEST", { message: `Invalid role. Allowed roles: ${allowedRoles.join(", ")}` });
4269
4331
  }
4270
4332
  //#endregion
4333
+ //#region src/routes/organizations/types.ts
4334
+ const ORGANIZATION_USER_PREVIEW_SELECT = [
4335
+ "id",
4336
+ "name",
4337
+ "email",
4338
+ "image"
4339
+ ];
4340
+ //#endregion
4271
4341
  //#region src/routes/organizations/invitations.ts
4272
4342
  const listOrganizationInvitations = (options) => {
4273
4343
  return createAuthEndpoint("/dash/organization/:id/invitations", {
@@ -4293,7 +4363,9 @@ const listOrganizationInvitations = (options) => {
4293
4363
  field: "email",
4294
4364
  value: emails,
4295
4365
  operator: "in"
4296
- }]
4366
+ }],
4367
+ select: [...ORGANIZATION_USER_PREVIEW_SELECT],
4368
+ limit: emails.length
4297
4369
  }) : [];
4298
4370
  const userByEmail = new Map(users.filter((u) => u.email != null).map((u) => [normalizeEmail(u.email, ctx.context), u]));
4299
4371
  return invitations.map((invitation) => {
@@ -4305,7 +4377,7 @@ const listOrganizationInvitations = (options) => {
4305
4377
  id: user.id,
4306
4378
  name: user.name,
4307
4379
  email: user.email ?? void 0,
4308
- image: user.image || null
4380
+ image: user.image ?? null
4309
4381
  } : null
4310
4382
  };
4311
4383
  });
@@ -4369,7 +4441,8 @@ const checkUserByEmail = (options) => {
4369
4441
  where: [{
4370
4442
  field: "email",
4371
4443
  value: email
4372
- }]
4444
+ }],
4445
+ select: [...ORGANIZATION_USER_PREVIEW_SELECT]
4373
4446
  });
4374
4447
  if (!user) return {
4375
4448
  exists: false,
@@ -4410,7 +4483,7 @@ const cancelInvitation = (options) => {
4410
4483
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4411
4484
  const { invitationId, organizationId } = ctx.context.payload;
4412
4485
  if (ctx.body.invitationId !== invitationId) throw ctx.error("BAD_REQUEST", { message: "Invitation ID mismatch" });
4413
- const invitation = await ctx.context.adapter.findOne({
4486
+ const invitationRow = await ctx.context.adapter.findOne({
4414
4487
  model: "invitation",
4415
4488
  where: [{
4416
4489
  field: "id",
@@ -4418,24 +4491,17 @@ const cancelInvitation = (options) => {
4418
4491
  }, {
4419
4492
  field: "organizationId",
4420
4493
  value: organizationId
4421
- }]
4494
+ }],
4495
+ join: {
4496
+ user: true,
4497
+ organization: true
4498
+ }
4422
4499
  });
4423
- if (!invitation) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
4424
- const [user, organization] = await Promise.all([ctx.context.adapter.findOne({
4425
- model: "user",
4426
- where: [{
4427
- field: "id",
4428
- value: invitation.inviterId
4429
- }]
4430
- }), ctx.context.adapter.findOne({
4431
- model: "organization",
4432
- where: [{
4433
- field: "id",
4434
- value: organizationId
4435
- }]
4436
- })]);
4437
- if (!user) throw ctx.error("NOT_FOUND", { message: "Inviter user not found" });
4438
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
4500
+ if (!invitationRow) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
4501
+ const user = joinedRelation(invitationRow.user);
4502
+ const organization = joinedRelation(invitationRow.organization);
4503
+ if (!user || !organization) throw ctx.error("NOT_FOUND", { message: "Inviter not found or is not associated with this invitation" });
4504
+ const invitation = invitationRow;
4439
4505
  if (orgOptions?.organizationHooks?.beforeCancelInvitation) await orgOptions.organizationHooks.beforeCancelInvitation({
4440
4506
  invitation,
4441
4507
  organization,
@@ -4475,7 +4541,7 @@ const resendInvitation = (options) => {
4475
4541
  const organizationPlugin = requireOrganizationPlugin(ctx);
4476
4542
  const { invitationId, organizationId } = ctx.context.payload;
4477
4543
  if (ctx.body.invitationId !== invitationId) throw ctx.error("BAD_REQUEST", { message: "Invitation ID mismatch" });
4478
- const invitation = await ctx.context.adapter.findOne({
4544
+ const invitationRow = await ctx.context.adapter.findOne({
4479
4545
  model: "invitation",
4480
4546
  where: [{
4481
4547
  field: "id",
@@ -4483,18 +4549,14 @@ const resendInvitation = (options) => {
4483
4549
  }, {
4484
4550
  field: "organizationId",
4485
4551
  value: organizationId
4486
- }]
4552
+ }],
4553
+ join: { user: true }
4487
4554
  });
4488
- if (!invitation) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
4555
+ if (!invitationRow) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
4556
+ const invitation = invitationRow;
4489
4557
  if (invitation.status !== "pending") throw ctx.error("BAD_REQUEST", { message: "Only pending invitations can be resent" });
4490
- const invitedByUser = await ctx.context.adapter.findOne({
4491
- model: "user",
4492
- where: [{
4493
- field: "id",
4494
- value: invitation.inviterId
4495
- }]
4496
- });
4497
- if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
4558
+ const invitedByUser = joinedRelation(invitationRow.user);
4559
+ if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter not found or is not associated with this invitation" });
4498
4560
  if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
4499
4561
  const invitationEmail = normalizeEmail(invitation.email, ctx.context);
4500
4562
  await organizationPlugin.endpoints.createInvitation({
@@ -4529,15 +4591,14 @@ const listOrganizationMembers = (options) => {
4529
4591
  return [];
4530
4592
  }
4531
4593
  const organizationId = tryDecode(ctx.params.id);
4532
- const members = await ctx.context.adapter.findMany({
4594
+ const [members, invitations] = await Promise.all([ctx.context.adapter.findMany({
4533
4595
  model: "member",
4534
4596
  where: [{
4535
4597
  field: "organizationId",
4536
4598
  value: organizationId
4537
4599
  }],
4538
4600
  join: { user: true }
4539
- });
4540
- const invitations = await ctx.context.adapter.findMany({
4601
+ }), ctx.context.adapter.findMany({
4541
4602
  model: "invitation",
4542
4603
  where: [{
4543
4604
  field: "organizationId",
@@ -4547,16 +4608,16 @@ const listOrganizationMembers = (options) => {
4547
4608
  value: "accepted"
4548
4609
  }],
4549
4610
  join: { user: true }
4550
- });
4611
+ })]);
4551
4612
  const inviterById = /* @__PURE__ */ new Map();
4552
4613
  for (const inv of invitations) {
4553
- const inviter = Array.isArray(inv.user) ? inv.user[0] : inv.user;
4614
+ const inviter = joinedRelation(inv.user);
4554
4615
  if (inviter && inv.inviterId) inviterById.set(inv.inviterId, inviter);
4555
4616
  }
4556
4617
  const invitationByEmail = new Map(invitations.map((i) => [i.email.toLowerCase(), i]));
4557
4618
  const result = [];
4558
4619
  for (const m of members) {
4559
- const joinedUser = Array.isArray(m.user) ? m.user[0] : m.user;
4620
+ const joinedUser = joinedRelation(m.user);
4560
4621
  if (!joinedUser) continue;
4561
4622
  const invitation = joinedUser.email ? invitationByEmail.get(joinedUser.email.toLowerCase()) : void 0;
4562
4623
  const inviter = invitation ? inviterById.get(invitation.inviterId) : void 0;
@@ -4646,7 +4707,7 @@ const removeMember = (options) => {
4646
4707
  }, async (ctx) => {
4647
4708
  const { organizationId } = ctx.context.payload;
4648
4709
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4649
- const member = await ctx.context.adapter.findOne({
4710
+ const memberRow = await ctx.context.adapter.findOne({
4650
4711
  model: "member",
4651
4712
  where: [{
4652
4713
  field: "id",
@@ -4654,24 +4715,17 @@ const removeMember = (options) => {
4654
4715
  }, {
4655
4716
  field: "organizationId",
4656
4717
  value: organizationId
4657
- }]
4718
+ }],
4719
+ join: {
4720
+ user: true,
4721
+ organization: true
4722
+ }
4658
4723
  });
4659
- if (!member) throw ctx.error("NOT_FOUND", { message: "Member not found" });
4660
- const [user, organization] = await Promise.all([ctx.context.adapter.findOne({
4661
- model: "user",
4662
- where: [{
4663
- field: "id",
4664
- value: member.userId
4665
- }]
4666
- }), ctx.context.adapter.findOne({
4667
- model: "organization",
4668
- where: [{
4669
- field: "id",
4670
- value: organizationId
4671
- }]
4672
- })]);
4673
- if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
4674
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
4724
+ if (!memberRow) throw ctx.error("NOT_FOUND", { message: "Member not found" });
4725
+ const user = joinedRelation(memberRow.user);
4726
+ const organization = joinedRelation(memberRow.organization);
4727
+ if (!user || !organization) throw ctx.error("NOT_FOUND", { message: "User not found or is not associated with this member" });
4728
+ const member = memberRow;
4675
4729
  if (orgOptions?.organizationHooks?.beforeRemoveMember) await orgOptions.organizationHooks.beforeRemoveMember({
4676
4730
  member,
4677
4731
  user,
@@ -4727,7 +4781,7 @@ const updateMemberRole = (options) => {
4727
4781
  const { organizationId } = ctx.context.payload;
4728
4782
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4729
4783
  validateOrganizationMemberRole(ctx, ctx.body.role, orgOptions);
4730
- const existingMember = await ctx.context.adapter.findOne({
4784
+ const memberRow = await ctx.context.adapter.findOne({
4731
4785
  model: "member",
4732
4786
  where: [{
4733
4787
  field: "id",
@@ -4735,24 +4789,17 @@ const updateMemberRole = (options) => {
4735
4789
  }, {
4736
4790
  field: "organizationId",
4737
4791
  value: organizationId
4738
- }]
4792
+ }],
4793
+ join: {
4794
+ user: true,
4795
+ organization: true
4796
+ }
4739
4797
  });
4740
- if (!existingMember) throw ctx.error("NOT_FOUND", { message: "Member not found" });
4741
- const [user, organization] = await Promise.all([ctx.context.adapter.findOne({
4742
- model: "user",
4743
- where: [{
4744
- field: "id",
4745
- value: existingMember.userId
4746
- }]
4747
- }), ctx.context.adapter.findOne({
4748
- model: "organization",
4749
- where: [{
4750
- field: "id",
4751
- value: organizationId
4752
- }]
4753
- })]);
4754
- if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
4755
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
4798
+ if (!memberRow) throw ctx.error("NOT_FOUND", { message: "Member not found" });
4799
+ const user = joinedRelation(memberRow.user);
4800
+ const organization = joinedRelation(memberRow.organization);
4801
+ if (!user || !organization) throw ctx.error("NOT_FOUND", { message: "User not found or is not associated with this member" });
4802
+ const existingMember = memberRow;
4756
4803
  const previousRole = existingMember.role;
4757
4804
  let newRole = ctx.body.role;
4758
4805
  if (orgOptions?.organizationHooks?.beforeUpdateMemberRole) {
@@ -5001,6 +5048,39 @@ function validateWritableOrganizationUpdateFields(data, orgOptions) {
5001
5048
  }
5002
5049
  //#endregion
5003
5050
  //#region src/routes/organizations/organization.ts
5051
+ const MEMBER_COUNT_BATCH_SIZE = 1e4;
5052
+ const MEMBER_COUNT_ORG_CHUNK_SIZE = 200;
5053
+ async function countMembersForOrganizationIdChunk(ctx, orgIdChunk) {
5054
+ const counts = /* @__PURE__ */ new Map();
5055
+ for (const orgId of orgIdChunk) counts.set(orgId, 0);
5056
+ let offset = 0;
5057
+ while (true) {
5058
+ const batch = await ctx.context.adapter.findMany({
5059
+ model: "member",
5060
+ where: [{
5061
+ field: "organizationId",
5062
+ value: orgIdChunk,
5063
+ operator: "in"
5064
+ }],
5065
+ select: ["organizationId"],
5066
+ limit: MEMBER_COUNT_BATCH_SIZE,
5067
+ offset
5068
+ });
5069
+ for (const row of batch) counts.set(row.organizationId, (counts.get(row.organizationId) ?? 0) + 1);
5070
+ if (batch.length < MEMBER_COUNT_BATCH_SIZE) break;
5071
+ offset += MEMBER_COUNT_BATCH_SIZE;
5072
+ }
5073
+ return counts;
5074
+ }
5075
+ /** Count members per org in batched reads instead of one count query per org. */
5076
+ async function countMembersByOrganizationIds(ctx, orgIds) {
5077
+ const counts = /* @__PURE__ */ new Map();
5078
+ for (const orgId of orgIds) counts.set(orgId, 0);
5079
+ if (orgIds.length === 0) return counts;
5080
+ const chunkMaps = await withConcurrency(chunkArray(orgIds, { batchSize: MEMBER_COUNT_ORG_CHUNK_SIZE }), (orgIdChunk) => countMembersForOrganizationIdChunk(ctx, orgIdChunk), { concurrency: 5 });
5081
+ for (const chunkMap of chunkMaps) for (const [orgId, count] of chunkMap) counts.set(orgId, count);
5082
+ return counts;
5083
+ }
5004
5084
  const listOrganizations = (options) => {
5005
5085
  return createAuthEndpoint("/dash/list-organizations", {
5006
5086
  method: "GET",
@@ -5082,15 +5162,7 @@ const listOrganizations = (options) => {
5082
5162
  model: "organization",
5083
5163
  where
5084
5164
  })]);
5085
- const orgIds = organizations.map((o) => o.id);
5086
- const memberCounts = await withConcurrency(orgIds, (orgId) => ctx.context.adapter.count({
5087
- model: "member",
5088
- where: [{
5089
- field: "organizationId",
5090
- value: orgId
5091
- }]
5092
- }), { concurrency: 10 });
5093
- const memberCountByOrg = new Map(orgIds.map((orgId, i) => [orgId, memberCounts[i]]));
5165
+ const memberCountByOrg = await countMembersByOrganizationIds(ctx, organizations.map((o) => o.id));
5094
5166
  let withCounts = organizations.map((organization) => {
5095
5167
  const memberCount = memberCountByOrg.get(organization.id) ?? 0;
5096
5168
  return {
@@ -5123,6 +5195,7 @@ const listOrganizations = (options) => {
5123
5195
  value: Array.from(allUserIds),
5124
5196
  operator: "in"
5125
5197
  }],
5198
+ select: [...ORGANIZATION_USER_PREVIEW_SELECT],
5126
5199
  limit: allUserIds.size
5127
5200
  }) : [];
5128
5201
  const userMap = new Map(users.map((u) => [u.id, u]));
@@ -5229,7 +5302,7 @@ const deleteOrganization = (options) => {
5229
5302
  const { organizationId: bodyOrganizationId } = ctx.body;
5230
5303
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
5231
5304
  if (organizationId !== bodyOrganizationId) throw ctx.error("BAD_REQUEST", { message: "Organization ID mismatch" });
5232
- const owners = await ctx.context.adapter.findMany({
5305
+ const owner = (await ctx.context.adapter.findMany({
5233
5306
  model: "member",
5234
5307
  where: [{
5235
5308
  field: "organizationId",
@@ -5243,20 +5316,14 @@ const deleteOrganization = (options) => {
5243
5316
  direction: "asc"
5244
5317
  },
5245
5318
  limit: 1,
5246
- join: { user: true }
5247
- });
5248
- if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5249
- const owner = owners[0];
5250
- const deletedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
5251
- if (!deletedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5252
- const organization = await ctx.context.adapter.findOne({
5253
- model: "organization",
5254
- where: [{
5255
- field: "id",
5256
- value: organizationId
5257
- }]
5258
- });
5259
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
5319
+ join: {
5320
+ user: true,
5321
+ organization: true
5322
+ }
5323
+ }))[0];
5324
+ const deletedByUser = owner ? joinedRelation(owner.user) : null;
5325
+ const organization = owner ? joinedRelation(owner.organization) : null;
5326
+ if (!owner || !deletedByUser || !organization) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
5260
5327
  if (orgOptions?.organizationHooks?.beforeDeleteOrganization) await orgOptions.organizationHooks.beforeDeleteOrganization({
5261
5328
  organization,
5262
5329
  user: deletedByUser
@@ -5297,7 +5364,8 @@ const deleteManyOrganizations = (options) => {
5297
5364
  });
5298
5365
  const remainingOrgs = await ctx.context.adapter.findMany({
5299
5366
  model: "organization",
5300
- where
5367
+ where,
5368
+ select: ["id"]
5301
5369
  });
5302
5370
  for (const id of chunk) if (!remainingOrgs.some((u) => u.id === id)) deletedOrgIds.add(id);
5303
5371
  else skippedOrgIds.add(id);
@@ -5483,18 +5551,7 @@ const updateOrganization = (options) => {
5483
5551
  }, async (ctx) => {
5484
5552
  const { organizationId } = ctx.context.payload;
5485
5553
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
5486
- if (ctx.body.slug) {
5487
- const orgWithSlug = await ctx.context.adapter.findOne({
5488
- model: "organization",
5489
- where: [{
5490
- field: "slug",
5491
- value: ctx.body.slug
5492
- }],
5493
- select: ["id"]
5494
- });
5495
- if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
5496
- }
5497
- const owners = await ctx.context.adapter.findMany({
5554
+ const ownersPromise = ctx.context.adapter.findMany({
5498
5555
  model: "member",
5499
5556
  where: [{
5500
5557
  field: "organizationId",
@@ -5508,12 +5565,23 @@ const updateOrganization = (options) => {
5508
5565
  direction: "asc"
5509
5566
  },
5510
5567
  limit: 1,
5511
- join: { user: true }
5568
+ join: {
5569
+ user: true,
5570
+ organization: true
5571
+ }
5512
5572
  });
5513
- if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5573
+ const [orgWithSlug, owners] = await Promise.all([ctx.body.slug ? ctx.context.adapter.findOne({
5574
+ model: "organization",
5575
+ where: [{
5576
+ field: "slug",
5577
+ value: ctx.body.slug
5578
+ }],
5579
+ select: ["id"]
5580
+ }) : Promise.resolve(null), ownersPromise]);
5581
+ if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
5514
5582
  const owner = owners[0];
5515
- const updatedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
5516
- if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5583
+ const updatedByUser = owner ? joinedRelation(owner.user) : null;
5584
+ if (!owner || !updatedByUser || !joinedRelation(owner.organization)) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
5517
5585
  let updateData = pickWritableOrganizationFields(ctx.body, orgOptions, "update");
5518
5586
  try {
5519
5587
  validateWritableOrganizationUpdateFields(updateData, orgOptions);
@@ -5571,14 +5639,13 @@ const listOrganizationTeams = (options) => {
5571
5639
  }
5572
5640
  const organizationId = tryDecode(ctx.params.id);
5573
5641
  try {
5574
- const teams = await ctx.context.adapter.findMany({
5642
+ return await withConcurrency(await ctx.context.adapter.findMany({
5575
5643
  model: "team",
5576
5644
  where: [{
5577
5645
  field: "organizationId",
5578
5646
  value: organizationId
5579
5647
  }]
5580
- });
5581
- return await Promise.all(teams.map(async (team) => {
5648
+ }), async (team) => {
5582
5649
  let memberCount = 0;
5583
5650
  try {
5584
5651
  memberCount = await ctx.context.adapter.count({
@@ -5596,7 +5663,7 @@ const listOrganizationTeams = (options) => {
5596
5663
  ...team,
5597
5664
  memberCount
5598
5665
  };
5599
- }));
5666
+ }, { concurrency: 5 });
5600
5667
  } catch (error) {
5601
5668
  ctx.context.logger.warn("[Dash] Failed to list organization teams:", error);
5602
5669
  return [];
@@ -5614,7 +5681,7 @@ const updateTeam = (options) => {
5614
5681
  }, async (ctx) => {
5615
5682
  const { organizationId } = ctx.context.payload;
5616
5683
  const orgOptions = requireTeamsEnabled(ctx);
5617
- const existingTeam = await ctx.context.adapter.findOne({
5684
+ const teamRow = await ctx.context.adapter.findOne({
5618
5685
  model: "team",
5619
5686
  where: [{
5620
5687
  field: "id",
@@ -5622,18 +5689,13 @@ const updateTeam = (options) => {
5622
5689
  }, {
5623
5690
  field: "organizationId",
5624
5691
  value: organizationId
5625
- }]
5626
- });
5627
- if (!existingTeam) throw ctx.error("NOT_FOUND", { message: "Team not found" });
5628
- const organization = await ctx.context.adapter.findOne({
5629
- model: "organization",
5630
- where: [{
5631
- field: "id",
5632
- value: organizationId
5633
- }]
5692
+ }],
5693
+ join: { organization: true }
5634
5694
  });
5635
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
5636
- const owners = await ctx.context.adapter.findMany({
5695
+ const organization = joinedRelation(teamRow?.organization);
5696
+ if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
5697
+ const existingTeam = teamRow;
5698
+ const owner = (await ctx.context.adapter.findMany({
5637
5699
  model: "member",
5638
5700
  where: [{
5639
5701
  field: "organizationId",
@@ -5648,11 +5710,9 @@ const updateTeam = (options) => {
5648
5710
  },
5649
5711
  limit: 1,
5650
5712
  join: { user: true }
5651
- });
5652
- if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
5653
- const owner = owners[0];
5654
- const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
5655
- if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5713
+ }))[0];
5714
+ const user = joinedRelation(owner?.user);
5715
+ if (!owner || !user) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
5656
5716
  let updateData = { updatedAt: /* @__PURE__ */ new Date() };
5657
5717
  if (ctx.body.name) updateData.name = ctx.body.name;
5658
5718
  if (orgOptions?.organizationHooks?.beforeUpdateTeam) {
@@ -5692,7 +5752,7 @@ const deleteTeam = (options) => {
5692
5752
  }, async (ctx) => {
5693
5753
  const { organizationId } = ctx.context.payload;
5694
5754
  const orgOptions = requireTeamsEnabled(ctx);
5695
- const team = await ctx.context.adapter.findOne({
5755
+ const teamRow = await ctx.context.adapter.findOne({
5696
5756
  model: "team",
5697
5757
  where: [{
5698
5758
  field: "id",
@@ -5700,17 +5760,12 @@ const deleteTeam = (options) => {
5700
5760
  }, {
5701
5761
  field: "organizationId",
5702
5762
  value: organizationId
5703
- }]
5704
- });
5705
- if (!team) throw ctx.error("NOT_FOUND", { message: "Team not found" });
5706
- const organization = await ctx.context.adapter.findOne({
5707
- model: "organization",
5708
- where: [{
5709
- field: "id",
5710
- value: organizationId
5711
- }]
5763
+ }],
5764
+ join: { organization: true }
5712
5765
  });
5713
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
5766
+ const organization = joinedRelation(teamRow?.organization);
5767
+ if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
5768
+ const team = teamRow;
5714
5769
  if (orgOptions?.teams?.allowRemovingAllTeams === false) {
5715
5770
  if (await ctx.context.adapter.count({
5716
5771
  model: "team",
@@ -5720,7 +5775,7 @@ const deleteTeam = (options) => {
5720
5775
  }]
5721
5776
  }) <= 1) throw ctx.error("BAD_REQUEST", { message: "Cannot remove the last team in the organization" });
5722
5777
  }
5723
- const owners = await ctx.context.adapter.findMany({
5778
+ const owner = (await ctx.context.adapter.findMany({
5724
5779
  model: "member",
5725
5780
  where: [{
5726
5781
  field: "organizationId",
@@ -5735,11 +5790,9 @@ const deleteTeam = (options) => {
5735
5790
  },
5736
5791
  limit: 1,
5737
5792
  join: { user: true }
5738
- });
5739
- if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
5740
- const owner = owners[0];
5741
- const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
5742
- if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5793
+ }))[0];
5794
+ const user = joinedRelation(owner?.user);
5795
+ if (!owner || !user) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
5743
5796
  if (orgOptions?.organizationHooks?.beforeDeleteTeam) await orgOptions.organizationHooks.beforeDeleteTeam({
5744
5797
  team,
5745
5798
  user,
@@ -5768,14 +5821,28 @@ const createTeam = (options) => {
5768
5821
  }, async (ctx) => {
5769
5822
  const { organizationId } = ctx.context.payload;
5770
5823
  const orgOptions = requireTeamsEnabled(ctx);
5771
- const organization = await ctx.context.adapter.findOne({
5772
- model: "organization",
5824
+ const owner = (await ctx.context.adapter.findMany({
5825
+ model: "member",
5773
5826
  where: [{
5774
- field: "id",
5827
+ field: "organizationId",
5775
5828
  value: organizationId
5776
- }]
5777
- });
5778
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
5829
+ }, {
5830
+ field: "role",
5831
+ value: "owner"
5832
+ }],
5833
+ sortBy: {
5834
+ field: "createdAt",
5835
+ direction: "asc"
5836
+ },
5837
+ limit: 1,
5838
+ join: {
5839
+ user: true,
5840
+ organization: true
5841
+ }
5842
+ }))[0];
5843
+ const user = joinedRelation(owner?.user);
5844
+ const organization = joinedRelation(owner?.organization);
5845
+ if (!owner || !user || !organization) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
5779
5846
  if (orgOptions?.teams?.maximumTeams) {
5780
5847
  const teamsCount = await ctx.context.adapter.count({
5781
5848
  model: "team",
@@ -5790,26 +5857,6 @@ const createTeam = (options) => {
5790
5857
  }) : orgOptions.teams.maximumTeams;
5791
5858
  if (teamsCount >= maxTeams) throw ctx.error("BAD_REQUEST", { message: `Maximum number of teams (${maxTeams}) reached for this organization` });
5792
5859
  }
5793
- const owners = await ctx.context.adapter.findMany({
5794
- model: "member",
5795
- where: [{
5796
- field: "organizationId",
5797
- value: organizationId
5798
- }, {
5799
- field: "role",
5800
- value: "owner"
5801
- }],
5802
- sortBy: {
5803
- field: "createdAt",
5804
- direction: "asc"
5805
- },
5806
- limit: 1,
5807
- join: { user: true }
5808
- });
5809
- if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
5810
- const owner = owners[0];
5811
- const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
5812
- if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
5813
5860
  let teamData = {
5814
5861
  name: ctx.body.name,
5815
5862
  organizationId,
@@ -5868,7 +5915,7 @@ const listTeamMembers = (options) => {
5868
5915
  }],
5869
5916
  join: { user: true }
5870
5917
  })).map((tm) => {
5871
- const user = Array.isArray(tm.user) ? tm.user[0] : tm.user;
5918
+ const user = joinedRelation(tm.user);
5872
5919
  return {
5873
5920
  ...tm,
5874
5921
  user: user ? {
@@ -5896,7 +5943,7 @@ const addTeamMember = (options) => {
5896
5943
  }, async (ctx) => {
5897
5944
  const { organizationId } = ctx.context.payload;
5898
5945
  const orgOptions = requireTeamsEnabled(ctx);
5899
- const team = await ctx.context.adapter.findOne({
5946
+ const teamRow = await ctx.context.adapter.findOne({
5900
5947
  model: "team",
5901
5948
  where: [{
5902
5949
  field: "id",
@@ -5904,26 +5951,13 @@ const addTeamMember = (options) => {
5904
5951
  }, {
5905
5952
  field: "organizationId",
5906
5953
  value: organizationId
5907
- }]
5908
- });
5909
- if (!team) throw ctx.error("NOT_FOUND", { message: "Team not found" });
5910
- const organization = await ctx.context.adapter.findOne({
5911
- model: "organization",
5912
- where: [{
5913
- field: "id",
5914
- value: organizationId
5915
- }]
5916
- });
5917
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
5918
- const user = await ctx.context.adapter.findOne({
5919
- model: "user",
5920
- where: [{
5921
- field: "id",
5922
- value: ctx.body.userId
5923
- }]
5954
+ }],
5955
+ join: { organization: true }
5924
5956
  });
5925
- if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
5926
- if (!await ctx.context.adapter.findOne({
5957
+ const organization = joinedRelation(teamRow?.organization);
5958
+ if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
5959
+ const team = teamRow;
5960
+ const orgMember = await ctx.context.adapter.findOne({
5927
5961
  model: "member",
5928
5962
  where: [{
5929
5963
  field: "userId",
@@ -5931,8 +5965,11 @@ const addTeamMember = (options) => {
5931
5965
  }, {
5932
5966
  field: "organizationId",
5933
5967
  value: organizationId
5934
- }]
5935
- })) throw ctx.error("BAD_REQUEST", { message: "User is not a member of this organization" });
5968
+ }],
5969
+ join: { user: true }
5970
+ });
5971
+ const user = joinedRelation(orgMember?.user);
5972
+ if (!orgMember || !user) throw ctx.error("BAD_REQUEST", { message: "User not found or is not a member of this organization" });
5936
5973
  if (await ctx.context.adapter.count({
5937
5974
  model: "teamMember",
5938
5975
  where: [{
@@ -5999,7 +6036,7 @@ const removeTeamMember = (options) => {
5999
6036
  }, async (ctx) => {
6000
6037
  const { organizationId } = ctx.context.payload;
6001
6038
  const orgOptions = requireTeamsEnabled(ctx);
6002
- const team = await ctx.context.adapter.findOne({
6039
+ const teamRow = await ctx.context.adapter.findOne({
6003
6040
  model: "team",
6004
6041
  where: [{
6005
6042
  field: "id",
@@ -6007,26 +6044,13 @@ const removeTeamMember = (options) => {
6007
6044
  }, {
6008
6045
  field: "organizationId",
6009
6046
  value: organizationId
6010
- }]
6011
- });
6012
- if (!team) throw ctx.error("NOT_FOUND", { message: "Team not found" });
6013
- const organization = await ctx.context.adapter.findOne({
6014
- model: "organization",
6015
- where: [{
6016
- field: "id",
6017
- value: organizationId
6018
- }]
6019
- });
6020
- if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
6021
- const user = await ctx.context.adapter.findOne({
6022
- model: "user",
6023
- where: [{
6024
- field: "id",
6025
- value: ctx.body.userId
6026
- }]
6047
+ }],
6048
+ join: { organization: true }
6027
6049
  });
6028
- if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
6029
- const teamMember = await ctx.context.adapter.findOne({
6050
+ const organization = joinedRelation(teamRow?.organization);
6051
+ if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
6052
+ const team = teamRow;
6053
+ const teamMemberRow = await ctx.context.adapter.findOne({
6030
6054
  model: "teamMember",
6031
6055
  where: [{
6032
6056
  field: "teamId",
@@ -6034,9 +6058,12 @@ const removeTeamMember = (options) => {
6034
6058
  }, {
6035
6059
  field: "userId",
6036
6060
  value: ctx.body.userId
6037
- }]
6061
+ }],
6062
+ join: { user: true }
6038
6063
  });
6039
- if (!teamMember) throw ctx.error("NOT_FOUND", { message: "User is not a member of this team" });
6064
+ const user = joinedRelation(teamMemberRow?.user);
6065
+ if (!teamMemberRow || !user) throw ctx.error("NOT_FOUND", { message: "User not found or is not a member of this team" });
6066
+ const teamMember = teamMemberRow;
6040
6067
  if (orgOptions?.organizationHooks?.beforeRemoveTeamMember) await orgOptions.organizationHooks.beforeRemoveTeamMember({
6041
6068
  teamMember,
6042
6069
  team,
@@ -6967,7 +6994,8 @@ async function resolveTwoFactorStatus(context, userId, user) {
6967
6994
  where: [{
6968
6995
  field: "userId",
6969
6996
  value: userId
6970
- }]
6997
+ }],
6998
+ select: ["id"]
6971
6999
  }) ? "pending" : "disabled";
6972
7000
  }
6973
7001
  //#endregion
@@ -7280,7 +7308,8 @@ const deleteManyUsers = (options) => {
7280
7308
  });
7281
7309
  const remainingUsers = await ctx.context.adapter.findMany({
7282
7310
  model: "user",
7283
- where
7311
+ where,
7312
+ select: ["id"]
7284
7313
  });
7285
7314
  for (const id of chunk) if (!remainingUsers.some((u) => u.id === id)) deletedUserIds.add(id);
7286
7315
  else skippedUserIds.add(id);
@@ -7470,35 +7499,72 @@ const unlinkAccount = (options) => {
7470
7499
  return { success: true };
7471
7500
  });
7472
7501
  };
7502
+ const getUserDetailsJwtSchema = z$1.object({
7503
+ userId: z$1.string(),
7504
+ sessionOnly: z$1.boolean().optional(),
7505
+ accountOnly: z$1.boolean().optional()
7506
+ });
7473
7507
  const getUserDetails = (options) => {
7474
7508
  return createAuthEndpoint("/dash/user", {
7475
7509
  method: "GET",
7476
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
7510
+ use: [jwtMiddleware(options, getUserDetailsJwtSchema)],
7477
7511
  query: z$1.object({ minimal: z$1.boolean().or(z$1.string().transform((val) => val === "true")).optional() }).optional()
7478
7512
  }, async (ctx) => {
7479
- const { userId } = ctx.context.payload;
7513
+ const { userId, sessionOnly, accountOnly } = ctx.context.payload;
7480
7514
  const minimal = !!ctx.query?.minimal;
7515
+ if (sessionOnly && accountOnly) throw ctx.error("BAD_REQUEST", { message: "Cannot use sessionOnly and accountOnly together" });
7481
7516
  const hasAdminPlugin = hasPlugin(ctx.context, "admin");
7482
7517
  const secondaryStorageOnly = isSessionInSecondaryStorageOnly(ctx.context);
7518
+ const join = (() => {
7519
+ if (minimal) return void 0;
7520
+ if (sessionOnly) return secondaryStorageOnly ? void 0 : { session: true };
7521
+ if (accountOnly) return { account: true };
7522
+ return {
7523
+ account: true,
7524
+ ...secondaryStorageOnly ? {} : { session: true }
7525
+ };
7526
+ })();
7483
7527
  const user = await ctx.context.adapter.findOne({
7484
7528
  model: "user",
7485
7529
  where: [{
7486
7530
  field: "id",
7487
7531
  value: userId
7488
7532
  }],
7489
- ...minimal ? {} : { join: {
7490
- account: true,
7491
- ...secondaryStorageOnly ? {} : { session: true }
7492
- } }
7533
+ ...join ? { join } : {}
7493
7534
  });
7494
7535
  if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
7536
+ const banFields = {
7537
+ banned: hasAdminPlugin ? user.banned ?? false : false,
7538
+ banReason: hasAdminPlugin ? user.banReason ?? null : null,
7539
+ banExpires: hasAdminPlugin ? user.banExpires ?? null : null
7540
+ };
7541
+ const lastActiveAtFromUser = user.lastActiveAt ?? null;
7542
+ if (sessionOnly) {
7543
+ const sessions = secondaryStorageOnly ? normalizeDashSessions(await ctx.context.internalAdapter.listSessions(userId)) : user.session || [];
7544
+ const { session: _session, account: _account, ...userWithoutRelations } = user;
7545
+ return {
7546
+ ...userWithoutRelations,
7547
+ ...banFields,
7548
+ lastActiveAt: lastActiveAtFromUser,
7549
+ session: sanitizeSessions(sessions),
7550
+ account: []
7551
+ };
7552
+ }
7553
+ if (accountOnly) {
7554
+ const { session: _session, account, ...userWithoutRelations } = user;
7555
+ return {
7556
+ ...userWithoutRelations,
7557
+ ...banFields,
7558
+ lastActiveAt: lastActiveAtFromUser,
7559
+ account: account || [],
7560
+ session: []
7561
+ };
7562
+ }
7495
7563
  const twoFactorStatus = await resolveTwoFactorStatus(ctx.context, userId, user);
7496
7564
  if (minimal) return {
7497
7565
  ...user,
7498
- lastActiveAt: user.lastActiveAt ?? null,
7499
- banned: hasAdminPlugin ? user.banned ?? false : false,
7500
- banReason: hasAdminPlugin ? user.banReason ?? null : null,
7501
- banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
7566
+ lastActiveAt: lastActiveAtFromUser,
7567
+ ...banFields,
7502
7568
  account: [],
7503
7569
  session: [],
7504
7570
  twoFactorStatus
@@ -7538,9 +7604,7 @@ const getUserDetails = (options) => {
7538
7604
  ...userWithoutSessions,
7539
7605
  session: sanitizeSessions(sessions),
7540
7606
  lastActiveAt,
7541
- banned: hasAdminPlugin ? user.banned ?? false : false,
7542
- banReason: hasAdminPlugin ? user.banReason ?? null : null,
7543
- banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
7607
+ ...banFields,
7544
7608
  twoFactorStatus
7545
7609
  };
7546
7610
  });
@@ -7645,7 +7709,7 @@ async function countUniqueActiveUsers(ctx, range, options) {
7645
7709
  break;
7646
7710
  }
7647
7711
  }
7648
- }, { concurrency: 10 });
7712
+ }, { concurrency: 5 });
7649
7713
  return activeUserIds.size;
7650
7714
  }
7651
7715
  const sessions = await ctx.context.adapter.findMany({
@@ -7857,8 +7921,8 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
7857
7921
  label
7858
7922
  });
7859
7923
  }
7860
- const allQueries = intervalData.flatMap((interval) => [
7861
- ctx.context.adapter.count({
7924
+ const results = await withConcurrency(intervalData.flatMap((interval) => [
7925
+ () => ctx.context.adapter.count({
7862
7926
  model: "user",
7863
7927
  where: [{
7864
7928
  field: "createdAt",
@@ -7866,7 +7930,7 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
7866
7930
  value: interval.endDate
7867
7931
  }]
7868
7932
  }),
7869
- ctx.context.adapter.count({
7933
+ () => ctx.context.adapter.count({
7870
7934
  model: "user",
7871
7935
  where: [{
7872
7936
  field: "createdAt",
@@ -7878,15 +7942,14 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
7878
7942
  value: interval.endDate
7879
7943
  }]
7880
7944
  }),
7881
- countUniqueActiveUsers(ctx, {
7945
+ () => countUniqueActiveUsers(ctx, {
7882
7946
  from: interval.startDate,
7883
7947
  to: interval.endDate
7884
7948
  }, {
7885
7949
  storeInSecondaryStorageOnly,
7886
7950
  activityTrackingEnabled
7887
7951
  })
7888
- ]);
7889
- const results = await Promise.all(allQueries);
7952
+ ]), (runQuery) => runQuery(), { concurrency: 5 });
7890
7953
  return {
7891
7954
  data: intervalData.map((interval, idx) => ({
7892
7955
  date: typeof interval.endDate === "object" ? interval.endDate : interval.endDate,
@@ -7948,41 +8011,19 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
7948
8011
  const activeStart = period === "daily" ? startOfUtcDay(now) : period === "weekly" ? startOfUtcWeek(now) : startOfUtcMonth(now);
7949
8012
  const activeEnd = period === "daily" ? addUtcDays(activeStart, 1) : period === "weekly" ? addUtcDays(activeStart, 7) : addUtcMonths(activeStart, 1);
7950
8013
  const prefix = period === "daily" ? "D" : period === "weekly" ? "W" : "M";
7951
- const data = [];
7952
- for (let n = 1; n <= horizons; n++) {
8014
+ const activeStartIso = activeStart.toISOString();
8015
+ const activeEndIso = activeEnd.toISOString();
8016
+ const intervalMeta = Array.from({ length: horizons }, (_, index) => {
8017
+ const n = index + 1;
7953
8018
  const cohortStart = period === "daily" ? addUtcDays(activeStart, -n) : period === "weekly" ? addUtcDays(activeStart, -7 * n) : addUtcMonths(activeStart, -n);
7954
- const cohortEnd = period === "daily" ? addUtcDays(cohortStart, 1) : period === "weekly" ? addUtcDays(cohortStart, 7) : addUtcMonths(cohortStart, 1);
7955
- const cohortUsers = await ctx.context.adapter.findMany({
7956
- model: "user",
7957
- select: ["id"],
7958
- where: [{
7959
- field: "createdAt",
7960
- operator: "gte",
7961
- value: cohortStart
7962
- }, {
7963
- field: "createdAt",
7964
- operator: "lt",
7965
- value: cohortEnd
7966
- }]
7967
- });
7968
- const cohortSize = cohortUsers.length;
7969
- if (cohortSize === 0) {
7970
- data.push({
7971
- n,
7972
- label: `${prefix}${n}`,
7973
- cohortStart: cohortStart.toISOString(),
7974
- cohortEnd: cohortEnd.toISOString(),
7975
- activeStart: activeStart.toISOString(),
7976
- activeEnd: activeEnd.toISOString(),
7977
- cohortSize: 0,
7978
- retained: 0,
7979
- retentionRate: 0
7980
- });
7981
- continue;
7982
- }
7983
- const cohortUserIds = cohortUsers.map((u) => u.id);
7984
- let retained;
7985
- if (activityTrackingEnabled) retained = await ctx.context.adapter.count({
8019
+ return {
8020
+ n,
8021
+ cohortStart,
8022
+ cohortEnd: period === "daily" ? addUtcDays(cohortStart, 1) : period === "weekly" ? addUtcDays(cohortStart, 7) : addUtcMonths(cohortStart, 1)
8023
+ };
8024
+ });
8025
+ const countRetainedUsers = async (cohortUserIds) => {
8026
+ if (activityTrackingEnabled) return ctx.context.adapter.count({
7986
8027
  model: "user",
7987
8028
  where: [
7988
8029
  {
@@ -8005,7 +8046,7 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
8005
8046
  ctx.context.logger.warn("[Dash] Failed to count retained users by lastActiveAt:", error);
8006
8047
  return 0;
8007
8048
  });
8008
- else if (secondaryOnly) {
8049
+ if (secondaryOnly) {
8009
8050
  const activeStartMs = activeStart.getTime();
8010
8051
  const activeEndMs = activeEnd.getTime();
8011
8052
  const retainedUsers = /* @__PURE__ */ new Set();
@@ -8018,50 +8059,78 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
8018
8059
  break;
8019
8060
  }
8020
8061
  }
8021
- }, { concurrency: 50 });
8022
- retained = retainedUsers.size;
8023
- } else {
8024
- const sessionsInActiveWindow = await ctx.context.adapter.findMany({
8025
- model: "session",
8026
- select: ["userId"],
8027
- where: [
8028
- {
8029
- field: "userId",
8030
- operator: "in",
8031
- value: cohortUserIds
8032
- },
8033
- {
8034
- field: "updatedAt",
8035
- operator: "gte",
8036
- value: activeStart
8037
- },
8038
- {
8039
- field: "updatedAt",
8040
- operator: "lt",
8041
- value: activeEnd
8042
- }
8043
- ]
8044
- }).catch((error) => {
8045
- ctx.context.logger.warn("[Dash] Failed to fetch cohort sessions:", error);
8046
- return [];
8047
- });
8048
- retained = new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
8062
+ }, { concurrency: 5 });
8063
+ return retainedUsers.size;
8049
8064
  }
8050
- const retentionRate = cohortSize > 0 ? Math.round(retained / cohortSize * 100 * 10) / 10 : 0;
8051
- data.push({
8052
- n,
8053
- label: `${prefix}${n}`,
8054
- cohortStart: cohortStart.toISOString(),
8055
- cohortEnd: cohortEnd.toISOString(),
8056
- activeStart: activeStart.toISOString(),
8057
- activeEnd: activeEnd.toISOString(),
8058
- cohortSize,
8059
- retained,
8060
- retentionRate
8065
+ const sessionsInActiveWindow = await ctx.context.adapter.findMany({
8066
+ model: "session",
8067
+ select: ["userId"],
8068
+ where: [
8069
+ {
8070
+ field: "userId",
8071
+ operator: "in",
8072
+ value: cohortUserIds
8073
+ },
8074
+ {
8075
+ field: "updatedAt",
8076
+ operator: "gte",
8077
+ value: activeStart
8078
+ },
8079
+ {
8080
+ field: "updatedAt",
8081
+ operator: "lt",
8082
+ value: activeEnd
8083
+ }
8084
+ ]
8085
+ }).catch((error) => {
8086
+ ctx.context.logger.warn("[Dash] Failed to fetch cohort sessions:", error);
8087
+ return [];
8061
8088
  });
8062
- }
8089
+ return new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
8090
+ };
8063
8091
  return {
8064
- data,
8092
+ data: await withConcurrency(intervalMeta, async ({ n, cohortStart, cohortEnd }) => {
8093
+ const cohortStartIso = cohortStart.toISOString();
8094
+ const cohortEndIso = cohortEnd.toISOString();
8095
+ const cohortUsers = await ctx.context.adapter.findMany({
8096
+ model: "user",
8097
+ select: ["id"],
8098
+ where: [{
8099
+ field: "createdAt",
8100
+ operator: "gte",
8101
+ value: cohortStart
8102
+ }, {
8103
+ field: "createdAt",
8104
+ operator: "lt",
8105
+ value: cohortEnd
8106
+ }]
8107
+ });
8108
+ const cohortSize = cohortUsers.length;
8109
+ if (cohortSize === 0) return {
8110
+ n,
8111
+ label: `${prefix}${n}`,
8112
+ cohortStart: cohortStartIso,
8113
+ cohortEnd: cohortEndIso,
8114
+ activeStart: activeStartIso,
8115
+ activeEnd: activeEndIso,
8116
+ cohortSize: 0,
8117
+ retained: 0,
8118
+ retentionRate: 0
8119
+ };
8120
+ const retained = await countRetainedUsers(cohortUsers.map((u) => u.id));
8121
+ const retentionRate = Math.round(retained / cohortSize * 100 * 10) / 10;
8122
+ return {
8123
+ n,
8124
+ label: `${prefix}${n}`,
8125
+ cohortStart: cohortStartIso,
8126
+ cohortEnd: cohortEndIso,
8127
+ activeStart: activeStartIso,
8128
+ activeEnd: activeEndIso,
8129
+ cohortSize,
8130
+ retained,
8131
+ retentionRate
8132
+ };
8133
+ }, { concurrency: 5 }),
8065
8134
  period
8066
8135
  };
8067
8136
  });
@@ -8611,6 +8680,7 @@ const dash = (options) => {
8611
8680
  country: location?.country,
8612
8681
  countryCode: location?.countryCode
8613
8682
  };
8683
+ const eventUser = resolveUserFromContext(session.userId, ctx);
8614
8684
  let trigger = null;
8615
8685
  if (matchesAnyRoute(ctx.path, [
8616
8686
  routes.SIGN_IN,
@@ -8619,18 +8689,19 @@ const dash = (options) => {
8619
8689
  routes.SIGN_IN_OAUTH_CALLBACK
8620
8690
  ])) {
8621
8691
  trigger = getTriggerInfo(ctx, session.userId, enrichedSession);
8622
- trackUserSignedIn(enrichedSession, trigger, ctx, location);
8692
+ trackUserSignedIn(enrichedSession, trigger, ctx, location, eventUser);
8623
8693
  } else trigger = getTriggerInfo(ctx, session.userId);
8624
- trackSessionCreated(enrichedSession, trigger, ctx, location);
8694
+ trackSessionCreated(enrichedSession, trigger, ctx, location, eventUser);
8625
8695
  if ("impersonatedBy" in session && session.impersonatedBy) {
8626
8696
  trigger = {
8627
8697
  ...trigger,
8628
8698
  triggeredBy: session.impersonatedBy
8629
8699
  };
8630
- trackUserImpersonated(enrichedSession, trigger, ctx, location);
8700
+ const knownImpersonator = resolveUserFromContext(session.impersonatedBy, ctx);
8701
+ trackUserImpersonated(enrichedSession, trigger, ctx, location, eventUser, knownImpersonator);
8631
8702
  }
8632
8703
  if (opts.activityTracking?.enabled) try {
8633
- await ctx.context.adapter.update({
8704
+ await ctx.context.adapter.updateMany({
8634
8705
  model: "user",
8635
8706
  where: [{
8636
8707
  field: "id",
@@ -8656,6 +8727,7 @@ const dash = (options) => {
8656
8727
  countryCode: location?.countryCode
8657
8728
  };
8658
8729
  const trigger = getTriggerInfo(ctx, session.userId);
8730
+ const eventUser = resolveUserFromContext(session.userId, ctx);
8659
8731
  if (matchesAnyRoute(ctx.path, [
8660
8732
  routes.REVOKE_ALL_SESSIONS,
8661
8733
  routes.ADMIN_REVOKE_USER_SESSIONS,
@@ -8663,12 +8735,12 @@ const dash = (options) => {
8663
8735
  routes.DASH_BAN_USER
8664
8736
  ])) {
8665
8737
  if (!processedBulkOperationContexts.has(ctx)) {
8666
- trackSessionRevokedAll(enrichedSession, trigger, ctx, location);
8738
+ trackSessionRevokedAll(enrichedSession, trigger, ctx, location, eventUser);
8667
8739
  processedBulkOperationContexts.add(ctx);
8668
8740
  }
8669
- } else if (matchesAnyRoute(path, [routes.SIGN_OUT])) trackUserSignedOut(enrichedSession, trigger, ctx, location);
8670
- else trackSessionRevoked(enrichedSession, trigger, ctx, location);
8671
- if ("impersonatedBy" in session && session.impersonatedBy) trackUserImpersonationStop(enrichedSession, trigger, ctx, location);
8741
+ } else if (matchesAnyRoute(path, [routes.SIGN_OUT])) trackUserSignedOut(enrichedSession, trigger, ctx, location, eventUser);
8742
+ else trackSessionRevoked(enrichedSession, trigger, ctx, location, eventUser);
8743
+ if ("impersonatedBy" in session && session.impersonatedBy) trackUserImpersonationStop(enrichedSession, trigger, ctx, location, eventUser, resolveUserFromContext(session.impersonatedBy, ctx));
8672
8744
  } }
8673
8745
  },
8674
8746
  account: {
@@ -8685,13 +8757,14 @@ const dash = (options) => {
8685
8757
  const path = ctx.path;
8686
8758
  const trigger = getTriggerInfo(ctx, account.userId);
8687
8759
  const location = ctx.context.location;
8688
- if (matchesAnyRoute(path, [
8760
+ if (!matchesAnyRoute(path, [
8689
8761
  routes.CHANGE_PASSWORD,
8690
8762
  routes.SET_PASSWORD,
8691
8763
  routes.RESET_PASSWORD,
8692
8764
  routes.ADMIN_SET_PASSWORD,
8693
8765
  routes.DASH_SET_PASSWORD
8694
- ])) trackAccountPasswordChange(account, trigger, ctx, location);
8766
+ ])) return;
8767
+ trackAccountPasswordChange(account, trigger, ctx, location);
8695
8768
  } },
8696
8769
  delete: { async after(account, _ctx) {
8697
8770
  const ctx = _ctx;
@@ -8736,7 +8809,7 @@ const dash = (options) => {
8736
8809
  routes.DASH_COMPLETE_INVITATION_SOCIAL
8737
8810
  ]);
8738
8811
  },
8739
- handler: createIdentificationMiddleware($kv)
8812
+ handler: createIdentificationMiddleware($kv, { skipIdentification: (ctx) => isDashRoute(ctx.path) })
8740
8813
  }],
8741
8814
  after: [{
8742
8815
  matcher: (ctx) => {
@@ -8779,13 +8852,15 @@ const dash = (options) => {
8779
8852
  if (lastUpdate) {
8780
8853
  if (now - new Date(lastUpdate).getTime() < activityUpdateInterval) return;
8781
8854
  }
8782
- ctx.context.adapter.update({
8855
+ ctx.context.adapter.updateMany({
8783
8856
  model: "user",
8784
8857
  where: [{
8785
8858
  field: "id",
8786
8859
  value: userId
8787
8860
  }],
8788
8861
  update: { lastActiveAt: /* @__PURE__ */ new Date() }
8862
+ }).catch((error) => {
8863
+ logger.warn("[Dash] Failed to update user activity", error);
8789
8864
  });
8790
8865
  }),
8791
8866
  matcher: (ctx) => ctx.request?.method !== "GET"