@better-auth/infra 0.1.7 → 0.1.9-beta.1

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,4 +1,5 @@
1
- import { a as INFRA_KV_URL, i as INFRA_API_URL, n as createEmailSender, r as sendEmail, t as EMAIL_TEMPLATES } from "./email-BGxJ96Ky.mjs";
1
+ import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS, t as INFRA_API_URL } from "./constants-DWl1utFw.mjs";
2
+ import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
2
3
  import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
3
4
  import { env } from "@better-auth/core/env";
4
5
  import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
@@ -9,8 +10,6 @@ import z$1, { z } from "zod";
9
10
  import { setSessionCookie } from "better-auth/cookies";
10
11
  import { DEFAULT_MAX_SAML_METADATA_SIZE, DigestAlgorithm, DiscoveryError, SignatureAlgorithm, discoverOIDCConfig } from "@better-auth/sso";
11
12
 
12
- export * from "better-call"
13
-
14
13
  //#region src/options.ts
15
14
  function resolveConnectionOptions(options) {
16
15
  return {
@@ -141,11 +140,11 @@ function backgroundTask(task) {
141
140
  try {
142
141
  result = task();
143
142
  } catch (error) {
144
- logger.debug("Error performing background operation: ", error);
143
+ logger.debug("[Dash] Background operation failed: ", error);
145
144
  return;
146
145
  }
147
146
  Promise.resolve(result).catch((error) => {
148
- logger.debug("Error performing background operation: ", error);
147
+ logger.debug("[Dash] Background operation failed: ", error);
149
148
  });
150
149
  }
151
150
 
@@ -167,7 +166,7 @@ const getUserByEmail = async (email, ctx) => {
167
166
  }]
168
167
  });
169
168
  } catch (error) {
170
- logger.debug("Error fetching user info: ", error);
169
+ logger.debug("[Dash] Failed to fetch user info:", error);
171
170
  }
172
171
  return user;
173
172
  };
@@ -186,7 +185,9 @@ async function getUserById(userId, ctx) {
186
185
  value: userId
187
186
  }]
188
187
  });
189
- } catch {}
188
+ } catch (error) {
189
+ logger.debug("[Dash] Failed to fetch user info:", error);
190
+ }
190
191
  return user;
191
192
  }
192
193
  const getUserByIdToken = async (providerId, idToken, ctx) => {
@@ -195,7 +196,7 @@ const getUserByIdToken = async (providerId, idToken, ctx) => {
195
196
  if (provider) try {
196
197
  user = await provider.getUserInfo(idToken);
197
198
  } catch (error) {
198
- logger.debug("Error fetching user info: ", error);
199
+ logger.debug("[Dash] Failed to fetch user info:", error);
199
200
  }
200
201
  return user;
201
202
  };
@@ -213,7 +214,7 @@ const getUserByAuthorizationCode = async (providerId, ctx) => {
213
214
  });
214
215
  userInfo = await provider.getUserInfo({ ...tokens }).then((res) => res?.user);
215
216
  } catch (error) {
216
- logger.debug("Error fetching user info: ", error);
217
+ logger.debug("[Dash] Failed to fetch user info:", error);
217
218
  }
218
219
  return userInfo;
219
220
  };
@@ -306,7 +307,7 @@ const initAccountEvents = (tracker) => {
306
307
  const stripQuery = (value) => value.split("?")[0] || value;
307
308
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
308
309
  const routeToRegex = (route) => {
309
- const pattern = escapeRegex(stripQuery(route)).replace(/\\\/:([^/]+)/g, "/[^/]+");
310
+ const pattern = escapeRegex(stripQuery(route)).replace(/\/:([^/]+)/g, "/[^/]+");
310
311
  return /* @__PURE__ */ new RegExp(`${pattern}(?:$|[/?])`);
311
312
  };
312
313
  const matchesAnyRoute = (path, routes$1) => {
@@ -805,7 +806,7 @@ const initVerificationEvents = (tracker) => {
805
806
  //#endregion
806
807
  //#region src/events/triggers.ts
807
808
  const getTriggerInfo = (ctx, userId, session) => {
808
- const sessionUserId = session?.userId ?? ctx.context.session?.session.userId ?? UNKNOWN_USER;
809
+ const sessionUserId = session?.userId ?? ctx.context.session?.session.userId ?? userId;
809
810
  return {
810
811
  triggeredBy: sessionUserId,
811
812
  triggerContext: sessionUserId === userId ? "user" : matchesAnyRoute(ctx.path, [routes.ADMIN_ROUTE]) ? "admin" : matchesAnyRoute(ctx.path, [routes.DASH_ROUTE]) ? "dashboard" : sessionUserId === UNKNOWN_USER ? "user" : "unknown"
@@ -863,6 +864,7 @@ const initTrackEvents = (options) => {
863
864
  * Fetches identification data from the durable-kv service
864
865
  * when a request includes an X-Request-Id header.
865
866
  */
867
+ const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
866
868
  const identificationCache = /* @__PURE__ */ new Map();
867
869
  const CACHE_TTL_MS = 6e4;
868
870
  const CACHE_MAX_SIZE = 1e3;
@@ -892,7 +894,8 @@ async function getIdentification(requestId, apiKey, kvUrl) {
892
894
  for (let attempt = 0; attempt <= maxRetries; attempt++) try {
893
895
  const response = await fetch(`${baseUrl}/identify/${requestId}`, {
894
896
  method: "GET",
895
- headers: { "x-api-key": apiKey }
897
+ headers: { "x-api-key": apiKey },
898
+ signal: AbortSignal.timeout(KV_TIMEOUT_MS)
896
899
  });
897
900
  if (response.ok) {
898
901
  const data = await response.json();
@@ -938,7 +941,8 @@ function extractIdentificationHeaders(request) {
938
941
  */
939
942
  function createIdentificationMiddleware(options) {
940
943
  return createAuthMiddleware(async (ctx) => {
941
- const { visitorId, requestId } = extractIdentificationHeaders(ctx.request);
944
+ const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
945
+ const requestId = headerRequestId ?? ctx.getCookie(IDENTIFICATION_COOKIE_NAME) ?? null;
942
946
  ctx.context.visitorId = visitorId;
943
947
  ctx.context.requestId = requestId;
944
948
  if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
@@ -1020,7 +1024,8 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1020
1024
  const resolvedApiUrl = apiUrl || INFRA_API_URL;
1021
1025
  const $api = createFetch({
1022
1026
  baseURL: resolvedApiUrl,
1023
- headers: { "x-api-key": apiKey }
1027
+ headers: { "x-api-key": apiKey },
1028
+ throw: true
1024
1029
  });
1025
1030
  const emailSender = createEmailSender({
1026
1031
  apiUrl: resolvedApiUrl,
@@ -1036,14 +1041,14 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1036
1041
  return {
1037
1042
  async checkSecurity(request) {
1038
1043
  try {
1039
- const { data } = await $api("/security/check", {
1044
+ const data = await $api("/security/check", {
1040
1045
  method: "POST",
1041
1046
  body: {
1042
1047
  ...request,
1043
1048
  config: options
1044
1049
  }
1045
1050
  });
1046
- if (data && data.action !== "allow") logEvent({
1051
+ if (data.action !== "allow") logEvent({
1047
1052
  type: this.mapReasonToEventType(data.reason),
1048
1053
  userId: null,
1049
1054
  visitorId: request.visitorId,
@@ -1052,7 +1057,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1052
1057
  details: data.details || { reason: data.reason },
1053
1058
  action: data.action === "block" ? "blocked" : "challenged"
1054
1059
  });
1055
- return data || { action: "allow" };
1060
+ return data;
1056
1061
  } catch (error) {
1057
1062
  logger.error("[Dash] Security check failed:", error);
1058
1063
  return { action: "allow" };
@@ -1070,7 +1075,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1070
1075
  },
1071
1076
  async trackFailedAttempt(identifier, visitorId, password, ip) {
1072
1077
  try {
1073
- const { data } = await $api("/security/track-failed-login", {
1078
+ const data = await $api("/security/track-failed-login", {
1074
1079
  method: "POST",
1075
1080
  body: {
1076
1081
  identifier,
@@ -1080,7 +1085,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1080
1085
  config: options
1081
1086
  }
1082
1087
  });
1083
- if (data?.blocked || data?.challenged) logEvent({
1088
+ if (data.blocked || data.challenged) logEvent({
1084
1089
  type: "credential_stuffing",
1085
1090
  userId: null,
1086
1091
  visitorId,
@@ -1089,7 +1094,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1089
1094
  details: data.details || { reason: data.reason },
1090
1095
  action: data.blocked ? "blocked" : "challenged"
1091
1096
  });
1092
- return data || { blocked: false };
1097
+ return data;
1093
1098
  } catch (error) {
1094
1099
  logger.error("[Dash] Track failed attempt error:", error);
1095
1100
  return { blocked: false };
@@ -1107,26 +1112,23 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1107
1112
  },
1108
1113
  async isBlocked(visitorId) {
1109
1114
  try {
1110
- const { data } = await $api(`/security/is-blocked?visitorId=${encodeURIComponent(visitorId)}`, { method: "GET" });
1111
- return data?.blocked ?? false;
1112
- } catch {
1115
+ return (await $api(`/security/is-blocked?visitorId=${encodeURIComponent(visitorId)}`, { method: "GET" })).blocked ?? false;
1116
+ } catch (error) {
1117
+ logger.warn("[Dash] Security is-blocked check failed:", error);
1113
1118
  return false;
1114
1119
  }
1115
1120
  },
1116
1121
  async verifyPoWSolution(visitorId, solution) {
1117
1122
  try {
1118
- const { data } = await $api("/security/pow/verify", {
1123
+ return await $api("/security/pow/verify", {
1119
1124
  method: "POST",
1120
1125
  body: {
1121
1126
  visitorId,
1122
1127
  solution
1123
1128
  }
1124
1129
  });
1125
- return data || {
1126
- valid: false,
1127
- reason: "unknown"
1128
- };
1129
- } catch {
1130
+ } catch (error) {
1131
+ logger.warn("[Dash] PoW verify failed:", error);
1130
1132
  return {
1131
1133
  valid: false,
1132
1134
  reason: "error"
@@ -1135,22 +1137,22 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1135
1137
  },
1136
1138
  async generateChallenge(visitorId) {
1137
1139
  try {
1138
- const { data } = await $api("/security/pow/generate", {
1140
+ return (await $api("/security/pow/generate", {
1139
1141
  method: "POST",
1140
1142
  body: {
1141
1143
  visitorId,
1142
1144
  difficulty: options.challengeDifficulty
1143
1145
  }
1144
- });
1145
- return data?.challenge || "";
1146
- } catch {
1146
+ })).challenge || "";
1147
+ } catch (error) {
1148
+ logger.warn("[Dash] PoW generate challenge failed:", error);
1147
1149
  return "";
1148
1150
  }
1149
1151
  },
1150
1152
  async checkImpossibleTravel(userId, currentLocation, visitorId) {
1151
1153
  if (!options.impossibleTravel?.enabled || !currentLocation) return null;
1152
1154
  try {
1153
- const { data } = await $api("/security/impossible-travel", {
1155
+ const data = await $api("/security/impossible-travel", {
1154
1156
  method: "POST",
1155
1157
  body: {
1156
1158
  userId,
@@ -1159,7 +1161,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1159
1161
  config: options
1160
1162
  }
1161
1163
  });
1162
- if (data?.isImpossible) {
1164
+ if (data.isImpossible) {
1163
1165
  const actionTaken = data.action === "block" ? "blocked" : data.action === "challenge" ? "challenged" : "logged";
1164
1166
  logEvent({
1165
1167
  type: "impossible_travel",
@@ -1177,8 +1179,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1177
1179
  action: actionTaken
1178
1180
  });
1179
1181
  }
1180
- return data || null;
1181
- } catch {
1182
+ return data;
1183
+ } catch (error) {
1184
+ logger.warn("[Dash] Impossible travel check failed:", error);
1182
1185
  return null;
1183
1186
  }
1184
1187
  },
@@ -1204,14 +1207,14 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1204
1207
  action: "log"
1205
1208
  };
1206
1209
  try {
1207
- const { data } = await $api("/security/free-trial-abuse/check", {
1210
+ const data = await $api("/security/free-trial-abuse/check", {
1208
1211
  method: "POST",
1209
1212
  body: {
1210
1213
  visitorId,
1211
1214
  config: options
1212
1215
  }
1213
1216
  });
1214
- if (data?.isAbuse) logEvent({
1217
+ if (data.isAbuse) logEvent({
1215
1218
  type: "free_trial_abuse",
1216
1219
  userId: null,
1217
1220
  visitorId,
@@ -1223,13 +1226,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1223
1226
  },
1224
1227
  action: data.action === "block" ? "blocked" : "logged"
1225
1228
  });
1226
- return data || {
1227
- isAbuse: false,
1228
- accountCount: 0,
1229
- maxAccounts: 0,
1230
- action: "log"
1231
- };
1232
- } catch {
1229
+ return data;
1230
+ } catch (error) {
1231
+ logger.warn("[Dash] Free trial abuse check failed:", error);
1233
1232
  return {
1234
1233
  isAbuse: false,
1235
1234
  accountCount: 0,
@@ -1254,17 +1253,17 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1254
1253
  },
1255
1254
  async checkCompromisedPassword(password) {
1256
1255
  try {
1257
- const hash = await sha1Hash(password);
1258
- const prefix = hash.substring(0, 5);
1259
- const suffix = hash.substring(5);
1260
- const { data } = await $api("/security/breached-passwords", {
1256
+ const hash$1 = await sha1Hash(password);
1257
+ const prefix = hash$1.substring(0, 5);
1258
+ const suffix = hash$1.substring(5);
1259
+ const data = await $api("/security/breached-passwords", {
1261
1260
  method: "POST",
1262
1261
  body: {
1263
1262
  passwordPrefix: prefix,
1264
1263
  config: options
1265
1264
  }
1266
1265
  });
1267
- if (!data?.enabled) return { compromised: false };
1266
+ if (!data.enabled) return { compromised: false };
1268
1267
  const breachCount = (data.suffixes || {})[suffix] || 0;
1269
1268
  const minBreachCount = data.minBreachCount ?? 1;
1270
1269
  const action = data.action || "block";
@@ -1291,7 +1290,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1291
1290
  async checkStaleUser(userId, lastActiveAt) {
1292
1291
  if (!options.staleUsers?.enabled) return { isStale: false };
1293
1292
  try {
1294
- const { data } = await $api("/security/stale-user", {
1293
+ const data = await $api("/security/stale-user", {
1295
1294
  method: "POST",
1296
1295
  body: {
1297
1296
  userId,
@@ -1299,7 +1298,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1299
1298
  config: options
1300
1299
  }
1301
1300
  });
1302
- if (data?.isStale) logEvent({
1301
+ if (data.isStale) logEvent({
1303
1302
  type: "stale_account_reactivation",
1304
1303
  userId,
1305
1304
  visitorId: null,
@@ -1314,7 +1313,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1314
1313
  },
1315
1314
  action: data.action === "block" ? "blocked" : data.action === "challenge" ? "challenged" : "logged"
1316
1315
  });
1317
- return data || { isStale: false };
1316
+ return data;
1318
1317
  } catch (error) {
1319
1318
  logger.error("[Dash] Stale user check error:", error);
1320
1319
  return { isStale: false };
@@ -1496,8 +1495,13 @@ async function handleSecurityVerdict(verdict, ctx, trackEvent, securityService)
1496
1495
  const reason = verdict.reason || "unknown";
1497
1496
  const confidence = 1;
1498
1497
  if (verdict.action === "challenge" && ctx.visitorId) {
1498
+ const challenge = verdict.challenge || await securityService.generateChallenge(ctx.visitorId);
1499
+ if (!challenge?.trim()) {
1500
+ logger.warn("[Sentinel] Could not generate PoW challenge (service may be unavailable). Falling back to allow.");
1501
+ return;
1502
+ }
1499
1503
  trackEvent(buildEventData(ctx, "challenged", reason, confidence, verdict.details));
1500
- throwChallengeError(verdict.challenge || await securityService.generateChallenge(ctx.visitorId), reason, "Please complete a security check to continue.");
1504
+ throwChallengeError(challenge, reason, "Please complete a security check to continue.");
1501
1505
  } else if (verdict.action === "block") {
1502
1506
  trackEvent(buildEventData(ctx, "blocked", reason, confidence, verdict.details));
1503
1507
  throw new APIError("FORBIDDEN", { message: ERROR_MESSAGES[reason] || "Access denied." });
@@ -1633,7 +1637,8 @@ function createEmailValidator(options = {}) {
1633
1637
  });
1634
1638
  const $kv = createFetch({
1635
1639
  baseURL: kvUrl,
1636
- headers: { "x-api-key": apiKey }
1640
+ headers: { "x-api-key": apiKey },
1641
+ timeout: KV_TIMEOUT_MS
1637
1642
  });
1638
1643
  /**
1639
1644
  * Fetch and resolve email validity policy from API with caching
@@ -2124,12 +2129,19 @@ const sentinel = (options) => {
2124
2129
  try {
2125
2130
  user = await hookCtx.context.adapter.findOne({
2126
2131
  model: "user",
2132
+ select: [
2133
+ "email",
2134
+ "name",
2135
+ "lastActiveAt"
2136
+ ],
2127
2137
  where: [{
2128
2138
  field: "id",
2129
2139
  value: session.userId
2130
2140
  }]
2131
2141
  });
2132
- } catch {}
2142
+ } catch (error) {
2143
+ logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2144
+ }
2133
2145
  if (visitorId) {
2134
2146
  if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) backgroundTask(() => securityService.notifyUnknownDevice(session.userId, user.email, identification));
2135
2147
  }
@@ -2628,16 +2640,49 @@ const initTeamEvents = (tracker) => {
2628
2640
  //#endregion
2629
2641
  //#region src/jwt.ts
2630
2642
  /**
2643
+ * Hash the given value
2644
+ * Note: Must match @infra/crypto hash()
2645
+ * @param value - The value to hash
2646
+ */
2647
+ async function hash(value) {
2648
+ const data = new TextEncoder().encode(value);
2649
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
2650
+ const hashArray = new Uint8Array(hashBuffer);
2651
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2652
+ }
2653
+ /**
2631
2654
  * Skip JTI check for JWTs issued within this many seconds.
2632
2655
  * This is safe because replay attacks require time for interception.
2633
2656
  * A freshly issued token is almost certainly legitimate.
2634
2657
  */
2635
2658
  const JTI_CHECK_GRACE_PERIOD_SECONDS = 30;
2636
- async function getJWKs(apiUrl) {
2659
+ const JWKS_CACHE_TTL_MS = 900 * 1e3;
2660
+ const jwksCache = /* @__PURE__ */ new Map();
2661
+ const inflightRequests = /* @__PURE__ */ new Map();
2662
+ async function fetchJWKS(apiUrl) {
2637
2663
  const jwks = await betterFetch(`${apiUrl}/api/auth/jwks`);
2638
2664
  if (!jwks.data) throw new APIError$1("UNAUTHORIZED", { message: "Invalid API key" });
2665
+ jwksCache.set(apiUrl, {
2666
+ data: jwks.data,
2667
+ expiresAt: Date.now() + JWKS_CACHE_TTL_MS
2668
+ });
2639
2669
  return createLocalJWKSet(jwks.data);
2640
2670
  }
2671
+ async function prefetchJWKS(apiUrl) {
2672
+ const fetchPromise = fetchJWKS(apiUrl);
2673
+ inflightRequests.set(apiUrl, fetchPromise);
2674
+ fetchPromise.finally(() => inflightRequests.delete(apiUrl));
2675
+ return fetchPromise;
2676
+ }
2677
+ async function getJWKs(apiUrl) {
2678
+ const cached = jwksCache.get(apiUrl);
2679
+ if (cached && Date.now() < cached.expiresAt) return createLocalJWKSet(cached.data);
2680
+ if (cached) {
2681
+ if (!inflightRequests.has(apiUrl)) prefetchJWKS(apiUrl);
2682
+ return createLocalJWKSet(cached.data);
2683
+ }
2684
+ return inflightRequests.get(apiUrl) ?? prefetchJWKS(apiUrl);
2685
+ }
2641
2686
  /**
2642
2687
  * Check if JWT is recently issued and can skip JTI verification.
2643
2688
  * JWTs issued within the grace period are trusted without JTI check
@@ -2651,19 +2696,24 @@ function isRecentlyIssued(payload) {
2651
2696
  const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (ctx) => {
2652
2697
  const jwsFromHeader = getJWT ? await getJWT(ctx) : ctx.headers?.get("Authorization")?.split(" ")[1];
2653
2698
  if (!jwsFromHeader) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2654
- const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(options.apiUrl), { maxTokenAge: "5m" }).catch(() => {
2699
+ const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(options.apiUrl), { maxTokenAge: "5m" }).catch((e) => {
2700
+ ctx.context.logger.error("[Dash] JWT verification failed:", e);
2655
2701
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2656
2702
  });
2657
2703
  if (!isRecentlyIssued(payload)) {
2658
2704
  if (!(await betterFetch("/api/auth/check-jti", {
2659
2705
  baseURL: options.apiUrl,
2660
2706
  method: "POST",
2707
+ headers: { "x-api-key": options.apiKey },
2661
2708
  body: {
2662
2709
  jti: payload.jti,
2663
2710
  expiresAt: payload.exp
2664
2711
  }
2665
2712
  })).data?.valid) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2666
2713
  }
2714
+ const apiKeyHash = payload.apiKeyHash;
2715
+ if (typeof apiKeyHash !== "string" || !options.apiKey) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2716
+ if (apiKeyHash !== await hash(options.apiKey)) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2667
2717
  if (schema) {
2668
2718
  const parsed = schema.safeParse(payload);
2669
2719
  if (!parsed.success) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
@@ -2674,6 +2724,33 @@ const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (c
2674
2724
 
2675
2725
  //#endregion
2676
2726
  //#region src/routes/config.ts
2727
+ const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
2728
+ function isPlainSerializable(value) {
2729
+ if (value === null || typeof value !== "object") return true;
2730
+ if (Array.isArray(value)) return true;
2731
+ if (value instanceof Date) return true;
2732
+ const constructor = Object.getPrototypeOf(value)?.constructor;
2733
+ if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
2734
+ return true;
2735
+ }
2736
+ function sanitizePluginOptions(pluginId, options, seen = /* @__PURE__ */ new WeakSet()) {
2737
+ if (options === null || options === void 0) return options;
2738
+ if (typeof options === "function") return void 0;
2739
+ if (typeof options !== "object") return options;
2740
+ if (seen.has(options)) return void 0;
2741
+ seen.add(options);
2742
+ const excludeKeys = PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId];
2743
+ if (Array.isArray(options)) return options.map((item) => sanitizePluginOptions(pluginId, item, seen)).filter((item) => item !== void 0);
2744
+ const result = {};
2745
+ for (const [key, value] of Object.entries(options)) {
2746
+ if (excludeKeys?.has(key)) continue;
2747
+ if (typeof value === "function") continue;
2748
+ if (value !== null && typeof value === "object" && !isPlainSerializable(value)) continue;
2749
+ const sanitized = sanitizePluginOptions(pluginId, value, seen);
2750
+ if (sanitized !== void 0) result[key] = sanitized;
2751
+ }
2752
+ return result;
2753
+ }
2677
2754
  function estimateEntropy(str) {
2678
2755
  const unique = new Set(str).size;
2679
2756
  if (unique === 0) return 0;
@@ -2685,6 +2762,7 @@ const getConfig = (options) => {
2685
2762
  use: [jwtMiddleware(options)]
2686
2763
  }, async (ctx) => {
2687
2764
  const advancedOptions = ctx.context.options.advanced;
2765
+ const organizationPlugin = ctx.context.getPlugin("organization");
2688
2766
  return {
2689
2767
  version: ctx.context.version || null,
2690
2768
  socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
@@ -2693,13 +2771,13 @@ const getConfig = (options) => {
2693
2771
  return {
2694
2772
  id: plugin.id,
2695
2773
  schema: plugin.schema,
2696
- options: plugin.options
2774
+ options: sanitizePluginOptions(plugin.id, plugin.options)
2697
2775
  };
2698
2776
  }),
2699
2777
  organization: {
2700
- sendInvitationEmailEnabled: !!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")?.options?.sendInvitationEmail,
2778
+ sendInvitationEmailEnabled: !!organizationPlugin?.options?.sendInvitationEmail,
2701
2779
  additionalFields: (() => {
2702
- const orgAdditionalFields = (ctx.context.options.plugins?.find((plugin) => plugin.id === "organization"))?.options?.schema?.organization?.additionalFields || {};
2780
+ const orgAdditionalFields = organizationPlugin?.options?.schema?.organization?.additionalFields || {};
2703
2781
  return Object.keys(orgAdditionalFields).map((field) => {
2704
2782
  const fieldType = orgAdditionalFields[field];
2705
2783
  return {
@@ -2853,7 +2931,8 @@ const listOrganizationDirectories = (options) => {
2853
2931
  providerId: provider.providerId,
2854
2932
  scimEndpoint: getScimEndpoint(ctx.context.baseURL)
2855
2933
  }));
2856
- } catch {
2934
+ } catch (error) {
2935
+ ctx.context.logger.warn("[Dash] Failed to list SCIM provider connections:", error);
2857
2936
  return [];
2858
2937
  }
2859
2938
  });
@@ -3036,7 +3115,10 @@ const getUserEvents = (options) => {
3036
3115
  offset: offset.toString()
3037
3116
  }
3038
3117
  });
3039
- if (error || !data) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch events" });
3118
+ if (error || !data) {
3119
+ ctx.context.logger.error("[Dash] Failed to fetch events:", error);
3120
+ throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch events" });
3121
+ }
3040
3122
  let events = data.events.map(transformEvent);
3041
3123
  if (ctx.query?.eventType) events = events.filter((event) => event.eventType === ctx.query?.eventType);
3042
3124
  return {
@@ -3118,7 +3200,10 @@ const getAuditLogs = (options) => {
3118
3200
  offset: offset.toString()
3119
3201
  }
3120
3202
  });
3121
- if (error || !data) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch organization audit logs" });
3203
+ if (error || !data) {
3204
+ ctx.context.logger.error("[Dash] Failed to fetch organization audit logs:", error);
3205
+ throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch organization audit logs" });
3206
+ }
3122
3207
  rawEvents = data.events;
3123
3208
  total = data.total;
3124
3209
  responseLimit = data.limit;
@@ -3132,7 +3217,10 @@ const getAuditLogs = (options) => {
3132
3217
  offset: offset.toString()
3133
3218
  }
3134
3219
  });
3135
- if (error || !data) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch user audit logs" });
3220
+ if (error || !data) {
3221
+ ctx.context.logger.error("[Dash] Failed to fetch user audit logs:", error);
3222
+ throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch user audit logs" });
3223
+ }
3136
3224
  rawEvents = data.events;
3137
3225
  total = data.total;
3138
3226
  responseLimit = data.limit;
@@ -3503,7 +3591,8 @@ const listOrganizationLogDrains = (options) => {
3503
3591
  ...drain,
3504
3592
  config: maskSensitiveConfig(drain.config, drain.destinationType)
3505
3593
  }));
3506
- } catch {
3594
+ } catch (error) {
3595
+ ctx.context.logger.warn("[Dash] Failed to list log drains:", error);
3507
3596
  return [];
3508
3597
  }
3509
3598
  });
@@ -3765,6 +3854,7 @@ const testOrganizationLogDrain = (options) => {
3765
3854
  }
3766
3855
  return { success: true };
3767
3856
  } catch (error) {
3857
+ ctx.context.logger.warn("[Dash] Log drain test failed:", error);
3768
3858
  return {
3769
3859
  success: false,
3770
3860
  error: error instanceof Error ? error.message : "Unknown error"
@@ -3773,6 +3863,89 @@ const testOrganizationLogDrain = (options) => {
3773
3863
  });
3774
3864
  };
3775
3865
 
3866
+ //#endregion
3867
+ //#region src/export-factory.ts
3868
+ const exportFactory = (input, options) => async (ctx) => {
3869
+ const batchSize = options?.batchSize || 1e4;
3870
+ const staleMs = options?.staleMs || 300 * 1e3;
3871
+ const enabledFields = options?.enabledFields || [];
3872
+ const userLimit = input.limit;
3873
+ const userOffset = input.offset || 0;
3874
+ const abortController = new AbortController();
3875
+ let page = 0;
3876
+ let totalExported = 0;
3877
+ let staleTimeout;
3878
+ const resetTimeout = () => {
3879
+ clearTimeout(staleTimeout);
3880
+ staleTimeout = setTimeout(() => {
3881
+ abortController.abort();
3882
+ }, staleMs);
3883
+ };
3884
+ const stream = new ReadableStream({ async start(controller) {
3885
+ const start = performance.now();
3886
+ resetTimeout();
3887
+ try {
3888
+ while (true) {
3889
+ let effectiveBatchSize = batchSize;
3890
+ if (userLimit !== void 0) {
3891
+ const remaining = userLimit - totalExported;
3892
+ if (remaining <= 0) break;
3893
+ effectiveBatchSize = Math.min(batchSize, remaining);
3894
+ }
3895
+ const batch = await ctx.context.adapter.findMany({
3896
+ ...input,
3897
+ limit: effectiveBatchSize,
3898
+ offset: userOffset + page * batchSize
3899
+ });
3900
+ resetTimeout();
3901
+ if (batch.length === 0) {
3902
+ if (page === 0) throw new APIError("FAILED_DEPENDENCY", { message: "Nothing found to export" });
3903
+ break;
3904
+ }
3905
+ let processedBatch = batch;
3906
+ if (enabledFields.length > 0) processedBatch = batch.map((item) => Object.fromEntries(enabledFields.map((key) => [key, item?.[key] ?? null])));
3907
+ const chunk = processedBatch.map((u) => options?.processRow ? options.processRow(u) : u).filter((v) => v !== void 0).map((u) => JSON.stringify(u)).join("\n") + "\n";
3908
+ controller.enqueue(new TextEncoder().encode(chunk));
3909
+ totalExported += batch.length;
3910
+ page++;
3911
+ if (userLimit !== void 0 && totalExported >= userLimit) break;
3912
+ resetTimeout();
3913
+ }
3914
+ } catch (err) {
3915
+ if (!abortController.signal.aborted) console.error("Export stream failed:", err);
3916
+ } finally {
3917
+ clearTimeout(staleTimeout);
3918
+ controller.close();
3919
+ const end = performance.now();
3920
+ console.log(`Export streamed ${totalExported} records in ${Math.round((end - start) / 1e3)}s` + (abortController.signal.aborted ? " (stale timeout)" : ""));
3921
+ }
3922
+ } });
3923
+ return new Response(stream, { headers: { "Content-Type": "application/x-ndjson" } });
3924
+ };
3925
+
3926
+ //#endregion
3927
+ //#region src/helper.ts
3928
+ function* chunkArray(arr, options) {
3929
+ const batchSize = options?.batchSize || 200;
3930
+ for (let i = 0; i < arr.length; i += batchSize) yield arr.slice(i, i + batchSize);
3931
+ }
3932
+ async function withConcurrency(items, fn, options) {
3933
+ const concurrency = options?.concurrency || 5;
3934
+ const results = [];
3935
+ const executing = [];
3936
+ const run = async () => {
3937
+ const batchResults = await Promise.all(executing);
3938
+ results.push(...batchResults);
3939
+ executing.length = 0;
3940
+ };
3941
+ for (const item of items) {
3942
+ executing.push(fn(item));
3943
+ if (executing.length >= concurrency) await run();
3944
+ }
3945
+ if (executing.length > 0) await run();
3946
+ return results;
3947
+ }
3948
+
3776
3949
  //#endregion
3777
3950
  //#region src/validation/ssrf.ts
3778
3951
  /**
@@ -3865,9 +4038,7 @@ function buildSessionContext(userId, user) {
3865
4038
  } };
3866
4039
  }
3867
4040
  function getSSOPlugin(ctx) {
3868
- const plugin = ctx.context.options.plugins?.find((p) => p.id === "sso");
3869
- if (!plugin || !("endpoints" in plugin)) return null;
3870
- return plugin;
4041
+ return ctx.context.getPlugin("sso");
3871
4042
  }
3872
4043
 
3873
4044
  //#endregion
@@ -3916,12 +4087,19 @@ const listOrganizations = (options) => {
3916
4087
  "members"
3917
4088
  ]).optional(),
3918
4089
  sortOrder: z$1.enum(["asc", "desc"]).optional(),
4090
+ filterMembers: z$1.enum([
4091
+ "abandoned",
4092
+ "eq1",
4093
+ "gt1",
4094
+ "gt5",
4095
+ "gt10"
4096
+ ]).optional(),
3919
4097
  search: z$1.string().optional(),
3920
4098
  startDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional(),
3921
4099
  endDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional()
3922
4100
  }).optional()
3923
4101
  }, async (ctx) => {
3924
- const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search } = ctx.query || {};
4102
+ const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search, filterMembers } = ctx.query || {};
3925
4103
  const where = [];
3926
4104
  if (search && search.trim().length > 0) {
3927
4105
  const searchTerm = search.trim();
@@ -3947,26 +4125,64 @@ const listOrganizations = (options) => {
3947
4125
  value: ctx.query.endDate,
3948
4126
  operator: "lte"
3949
4127
  });
4128
+ const needsInMemoryProcessing = sortBy === "members" || !!filterMembers;
4129
+ const dbSortBy = sortBy === "members" ? "createdAt" : sortBy;
3950
4130
  const [organizations, initialTotal] = await Promise.all([ctx.context.adapter.findMany({
3951
4131
  model: "organization",
3952
4132
  where,
3953
- limit,
3954
- offset,
4133
+ ...needsInMemoryProcessing ? {} : {
4134
+ limit,
4135
+ offset
4136
+ },
3955
4137
  sortBy: {
3956
- field: sortBy,
4138
+ field: dbSortBy,
3957
4139
  direction: sortOrder
3958
- },
3959
- join: { member: true }
3960
- }), ctx.context.adapter.count({
4140
+ }
4141
+ }), needsInMemoryProcessing ? Promise.resolve(0) : ctx.context.adapter.count({
3961
4142
  model: "organization",
3962
4143
  where
3963
4144
  })]);
3964
- const withCounts = organizations.map((organization) => ({
3965
- ...organization,
3966
- memberCount: organization.member.length
3967
- }));
4145
+ const orgIds = organizations.map((o) => o.id);
4146
+ const allMembers = orgIds.length > 0 ? await ctx.context.adapter.findMany({
4147
+ model: "member",
4148
+ where: [{
4149
+ field: "organizationId",
4150
+ value: orgIds,
4151
+ operator: "in"
4152
+ }]
4153
+ }) : [];
4154
+ const membersByOrg = /* @__PURE__ */ new Map();
4155
+ for (const m of allMembers) {
4156
+ const list = membersByOrg.get(m.organizationId) || [];
4157
+ list.push(m);
4158
+ membersByOrg.set(m.organizationId, list);
4159
+ }
4160
+ let withCounts = organizations.map((organization) => {
4161
+ const orgMembers = membersByOrg.get(organization.id) || [];
4162
+ return {
4163
+ ...organization,
4164
+ _members: orgMembers,
4165
+ memberCount: orgMembers.length
4166
+ };
4167
+ });
4168
+ if (filterMembers) {
4169
+ const predicate = {
4170
+ abandoned: (c) => c === 0,
4171
+ eq1: (c) => c === 1,
4172
+ gt1: (c) => c > 1,
4173
+ gt5: (c) => c > 5,
4174
+ gt10: (c) => c > 10
4175
+ }[filterMembers];
4176
+ if (predicate) withCounts = withCounts.filter((o) => predicate(o.memberCount));
4177
+ }
4178
+ if (sortBy === "members") {
4179
+ const dir = sortOrder === "asc" ? 1 : -1;
4180
+ withCounts.sort((a, b) => (a.memberCount - b.memberCount) * dir);
4181
+ }
4182
+ const total = needsInMemoryProcessing ? withCounts.length : initialTotal;
4183
+ if (needsInMemoryProcessing) withCounts = withCounts.slice(offset, offset + limit);
3968
4184
  const allUserIds = /* @__PURE__ */ new Set();
3969
- for (const organization of withCounts) for (const member of organization.member.slice(0, 5)) allUserIds.add(member.userId);
4185
+ for (const organization of withCounts) for (const member of organization._members.slice(0, 5)) allUserIds.add(member.userId);
3970
4186
  const users = allUserIds.size > 0 ? await ctx.context.adapter.findMany({
3971
4187
  model: "user",
3972
4188
  where: [{
@@ -3978,29 +4194,63 @@ const listOrganizations = (options) => {
3978
4194
  const userMap = new Map(users.map((u) => [u.id, u]));
3979
4195
  return {
3980
4196
  organizations: withCounts.map((organization) => {
3981
- const members = organization.member.slice(0, 5).map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
4197
+ const members = organization._members.slice(0, 5).map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
3982
4198
  id: u.id,
3983
4199
  name: u.name,
3984
4200
  email: u.email,
3985
4201
  image: u.image
3986
4202
  }));
4203
+ const { _members, ...org } = organization;
3987
4204
  return {
3988
- ...organization,
4205
+ ...org,
3989
4206
  members
3990
4207
  };
3991
4208
  }),
3992
- total: initialTotal,
4209
+ total,
3993
4210
  offset,
3994
4211
  limit
3995
4212
  };
3996
4213
  });
3997
4214
  };
4215
+ function parseWhereClause$1(val) {
4216
+ if (!val) return [];
4217
+ const parsed = JSON.parse(val);
4218
+ if (!Array.isArray(parsed)) return [];
4219
+ return parsed;
4220
+ }
4221
+ const exportOrganizationsQuerySchema = z$1.object({
4222
+ limit: z$1.number().or(z$1.string().transform(Number)).optional(),
4223
+ offset: z$1.number().or(z$1.string().transform(Number)).optional(),
4224
+ sortBy: z$1.string().optional(),
4225
+ sortOrder: z$1.enum(["asc", "desc"]).optional(),
4226
+ where: z$1.string().transform(parseWhereClause$1).optional()
4227
+ }).optional();
4228
+ const exportOrganizations = (options) => {
4229
+ return createAuthEndpoint("/dash/export-organizations", {
4230
+ method: "GET",
4231
+ use: [jwtMiddleware(options)],
4232
+ query: exportOrganizationsQuerySchema
4233
+ }, async (ctx) => {
4234
+ const sortBy = ctx.query?.sortBy || "createdAt";
4235
+ const sortOrder = ctx.query?.sortOrder || "desc";
4236
+ return exportFactory({
4237
+ model: "organization",
4238
+ limit: ctx.query?.limit,
4239
+ offset: ctx.query?.offset ?? 0,
4240
+ sortBy: {
4241
+ field: sortBy,
4242
+ direction: sortOrder
4243
+ },
4244
+ where: ctx.query?.where
4245
+ })(ctx);
4246
+ });
4247
+ };
3998
4248
  const getOrganizationOptions = (options) => {
3999
4249
  return createAuthEndpoint("/dash/organization/options", {
4000
4250
  method: "GET",
4001
4251
  use: [jwtMiddleware(options)]
4002
4252
  }, async (ctx) => {
4003
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
4253
+ const organizationPlugin = ctx.context.getPlugin("organization");
4004
4254
  if (!organizationPlugin) return { teamsEnabled: false };
4005
4255
  return { teamsEnabled: organizationPlugin.options?.teams?.enabled && organizationPlugin.options.teams.defaultTeam?.enabled !== false };
4006
4256
  });
@@ -4141,7 +4391,7 @@ const deleteOrganization = (options) => {
4141
4391
  }, async (ctx) => {
4142
4392
  const { organizationId } = ctx.context.payload;
4143
4393
  const { organizationId: bodyOrganizationId } = ctx.body;
4144
- const orgOptions = (ctx.context.options.plugins?.find((p) => p.id === "organization"))?.options || {};
4394
+ const orgOptions = ctx.context.getPlugin("organization")?.options || {};
4145
4395
  if (organizationId !== bodyOrganizationId) throw ctx.error("BAD_REQUEST", { message: "Organization ID mismatch" });
4146
4396
  const owners = await ctx.context.adapter.findMany({
4147
4397
  model: "member",
@@ -4194,6 +4444,41 @@ const deleteOrganization = (options) => {
4194
4444
  return { success: true };
4195
4445
  });
4196
4446
  };
4447
+ const deleteManyOrganizations = (options) => {
4448
+ return createAuthEndpoint("/dash/organization/delete-many", {
4449
+ method: "POST",
4450
+ use: [jwtMiddleware(options, z$1.object({ organizationIds: z$1.string().array() }))]
4451
+ }, async (ctx) => {
4452
+ const { organizationIds } = ctx.context.payload;
4453
+ const deletedOrgIds = /* @__PURE__ */ new Set();
4454
+ const skippedOrgIds = /* @__PURE__ */ new Set();
4455
+ const start = performance.now();
4456
+ await withConcurrency(chunkArray(organizationIds), async (chunk) => {
4457
+ const where = [{
4458
+ field: "id",
4459
+ value: chunk,
4460
+ operator: "in"
4461
+ }];
4462
+ await ctx.context.adapter.deleteMany({
4463
+ model: "organization",
4464
+ where
4465
+ });
4466
+ const remainingOrgs = await ctx.context.adapter.findMany({
4467
+ model: "organization",
4468
+ where
4469
+ });
4470
+ for (const id of chunk) if (!remainingOrgs.some((u) => u.id === id)) deletedOrgIds.add(id);
4471
+ else skippedOrgIds.add(id);
4472
+ });
4473
+ const end = performance.now();
4474
+ console.log(`Time taken to bulk delete ${deletedOrgIds.size} organizations: ${Math.round((end - start) / 1e3)}s`, skippedOrgIds.size > 0 ? `Failed: ${skippedOrgIds.size}` : "");
4475
+ return {
4476
+ success: deletedOrgIds.size > 0,
4477
+ deletedOrgIds: Array.from(deletedOrgIds),
4478
+ skippedOrgIds: Array.from(skippedOrgIds)
4479
+ };
4480
+ });
4481
+ };
4197
4482
  const listOrganizationTeams = (options) => {
4198
4483
  return createAuthEndpoint("/dash/organization/:id/teams", {
4199
4484
  method: "GET",
@@ -4217,7 +4502,8 @@ const listOrganizationTeams = (options) => {
4217
4502
  value: team.id
4218
4503
  }]
4219
4504
  });
4220
- } catch {
4505
+ } catch (error) {
4506
+ ctx.context.logger.warn("[Dash] Failed to count team members:", error);
4221
4507
  memberCount = 0;
4222
4508
  }
4223
4509
  return {
@@ -4225,7 +4511,8 @@ const listOrganizationTeams = (options) => {
4225
4511
  memberCount
4226
4512
  };
4227
4513
  }));
4228
- } catch {
4514
+ } catch (error) {
4515
+ ctx.context.logger.warn("[Dash] Failed to list organization teams:", error);
4229
4516
  return [];
4230
4517
  }
4231
4518
  });
@@ -4240,7 +4527,7 @@ const updateTeam = (options) => {
4240
4527
  })
4241
4528
  }, async (ctx) => {
4242
4529
  const { organizationId } = ctx.context.payload;
4243
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
4530
+ const organizationPlugin = ctx.context.getPlugin("organization");
4244
4531
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
4245
4532
  const orgOptions = organizationPlugin.options || {};
4246
4533
  if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
@@ -4325,7 +4612,7 @@ const deleteTeam = (options) => {
4325
4612
  body: z$1.object({ teamId: z$1.string() })
4326
4613
  }, async (ctx) => {
4327
4614
  const { organizationId } = ctx.context.payload;
4328
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
4615
+ const organizationPlugin = ctx.context.getPlugin("organization");
4329
4616
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
4330
4617
  const orgOptions = organizationPlugin.options || {};
4331
4618
  if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
@@ -4409,7 +4696,7 @@ const createTeam = (options) => {
4409
4696
  body: z$1.object({ name: z$1.string() })
4410
4697
  }, async (ctx) => {
4411
4698
  const { organizationId } = ctx.context.payload;
4412
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
4699
+ const organizationPlugin = ctx.context.getPlugin("organization");
4413
4700
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
4414
4701
  const orgOptions = organizationPlugin.options || {};
4415
4702
  if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
@@ -4429,7 +4716,10 @@ const createTeam = (options) => {
4429
4716
  value: organizationId
4430
4717
  }]
4431
4718
  });
4432
- const maxTeams = typeof orgOptions.teams.maximumTeams === "function" ? await orgOptions.teams.maximumTeams({ organizationId }) : orgOptions.teams.maximumTeams;
4719
+ const maxTeams = typeof orgOptions.teams.maximumTeams === "function" ? await orgOptions.teams.maximumTeams({
4720
+ organizationId,
4721
+ session: null
4722
+ }) : orgOptions.teams.maximumTeams;
4433
4723
  if (teamsCount >= maxTeams) throw ctx.error("BAD_REQUEST", { message: `Maximum number of teams (${maxTeams}) reached for this organization` });
4434
4724
  }
4435
4725
  const owners = await ctx.context.adapter.findMany({
@@ -4526,7 +4816,8 @@ const listTeamMembers = (options) => {
4526
4816
  } : null
4527
4817
  };
4528
4818
  }));
4529
- } catch (_e) {
4819
+ } catch (e) {
4820
+ ctx.context.logger.warn("[Dash] Failed to list team members:", e);
4530
4821
  return [];
4531
4822
  }
4532
4823
  });
@@ -4541,7 +4832,7 @@ const addTeamMember = (options) => {
4541
4832
  })
4542
4833
  }, async (ctx) => {
4543
4834
  const { organizationId } = ctx.context.payload;
4544
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
4835
+ const organizationPlugin = ctx.context.getPlugin("organization");
4545
4836
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
4546
4837
  const orgOptions = organizationPlugin.options || {};
4547
4838
  if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
@@ -4602,7 +4893,8 @@ const addTeamMember = (options) => {
4602
4893
  });
4603
4894
  const maxMembers = typeof orgOptions.teams.maximumMembersPerTeam === "function" ? await orgOptions.teams.maximumMembersPerTeam({
4604
4895
  teamId: ctx.body.teamId,
4605
- organizationId
4896
+ organizationId,
4897
+ session: null
4606
4898
  }) : orgOptions.teams.maximumMembersPerTeam;
4607
4899
  if (teamMemberCount >= maxMembers) throw ctx.error("BAD_REQUEST", { message: `Maximum number of team members (${maxMembers}) reached for this team` });
4608
4900
  }
@@ -4646,7 +4938,7 @@ const removeTeamMember = (options) => {
4646
4938
  })
4647
4939
  }, async (ctx) => {
4648
4940
  const { organizationId } = ctx.context.payload;
4649
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
4941
+ const organizationPlugin = ctx.context.getPlugin("organization");
4650
4942
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
4651
4943
  const orgOptions = organizationPlugin.options || {};
4652
4944
  if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
@@ -4727,7 +5019,7 @@ const createOrganization = (options) => {
4727
5019
  defaultTeamName: z$1.string().optional()
4728
5020
  }).passthrough()
4729
5021
  }, async (ctx) => {
4730
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
5022
+ const organizationPlugin = ctx.context.getPlugin("organization");
4731
5023
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
4732
5024
  const { userId } = ctx.context.payload;
4733
5025
  const user = await ctx.context.adapter.findOne({
@@ -4892,7 +5184,7 @@ const updateOrganization = (options) => {
4892
5184
  }).passthrough()
4893
5185
  }, async (ctx) => {
4894
5186
  const { organizationId } = ctx.context.payload;
4895
- const orgOptions = (ctx.context.options.plugins?.find((p) => p.id === "organization"))?.options || {};
5187
+ const orgOptions = ctx.context.getPlugin("organization")?.options || {};
4896
5188
  if (ctx.body.slug) {
4897
5189
  const existingOrg = await ctx.context.adapter.findOne({
4898
5190
  model: "organization",
@@ -4929,10 +5221,16 @@ const updateOrganization = (options) => {
4929
5221
  });
4930
5222
  if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
4931
5223
  let updateData = { ...ctx.body };
5224
+ if (typeof updateData.metadata === "string") try {
5225
+ updateData.metadata = updateData.metadata === "" ? void 0 : JSON.parse(updateData.metadata);
5226
+ } catch {
5227
+ throw ctx.error("BAD_REQUEST", { message: "Invalid metadata: must be valid JSON" });
5228
+ }
4932
5229
  if (orgOptions?.organizationHooks?.beforeUpdateOrganization) {
4933
5230
  const response = await orgOptions.organizationHooks.beforeUpdateOrganization({
4934
5231
  organization: updateData,
4935
- user: updatedByUser
5232
+ user: updatedByUser,
5233
+ member: owner
4936
5234
  });
4937
5235
  if (response && typeof response === "object" && "data" in response) updateData = {
4938
5236
  ...updateData,
@@ -4945,11 +5243,16 @@ const updateOrganization = (options) => {
4945
5243
  field: "id",
4946
5244
  value: organizationId
4947
5245
  }],
4948
- update: updateData
5246
+ update: {
5247
+ ...updateData,
5248
+ metadata: typeof updateData.metadata === "object" ? JSON.stringify(updateData.metadata) : updateData.metadata
5249
+ }
4949
5250
  });
5251
+ if (!organization) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to update organization" });
4950
5252
  if (orgOptions?.organizationHooks?.afterUpdateOrganization) await orgOptions.organizationHooks.afterUpdateOrganization({
4951
5253
  organization,
4952
- user: updatedByUser
5254
+ user: updatedByUser,
5255
+ member: owner
4953
5256
  });
4954
5257
  return organization;
4955
5258
  });
@@ -4964,7 +5267,7 @@ const addMember = (options) => {
4964
5267
  })
4965
5268
  }, async (ctx) => {
4966
5269
  const { organizationId } = ctx.context.payload;
4967
- const orgOptions = (ctx.context.options.plugins?.find((p) => p.id === "organization"))?.options || {};
5270
+ const orgOptions = ctx.context.getPlugin("organization")?.options || {};
4968
5271
  const organization = await ctx.context.adapter.findOne({
4969
5272
  model: "organization",
4970
5273
  where: [{
@@ -5017,7 +5320,7 @@ const removeMember = (options) => {
5017
5320
  body: z$1.object({ memberId: z$1.string() })
5018
5321
  }, async (ctx) => {
5019
5322
  const { organizationId } = ctx.context.payload;
5020
- const orgOptions = (ctx.context.options.plugins?.find((p) => p.id === "organization"))?.options || {};
5323
+ const orgOptions = ctx.context.getPlugin("organization")?.options || {};
5021
5324
  const member = await ctx.context.adapter.findOne({
5022
5325
  model: "member",
5023
5326
  where: [{
@@ -5097,7 +5400,7 @@ const updateMemberRole = (options) => {
5097
5400
  })
5098
5401
  }, async (ctx) => {
5099
5402
  const { organizationId } = ctx.context.payload;
5100
- const orgOptions = (ctx.context.options.plugins?.find((p) => p.id === "organization"))?.options || {};
5403
+ const orgOptions = ctx.context.getPlugin("organization")?.options || {};
5101
5404
  const existingMember = await ctx.context.adapter.findOne({
5102
5405
  model: "member",
5103
5406
  where: [{
@@ -5131,7 +5434,7 @@ const updateMemberRole = (options) => {
5131
5434
  member: existingMember,
5132
5435
  user,
5133
5436
  organization,
5134
- role: newRole
5437
+ newRole
5135
5438
  });
5136
5439
  if (response && typeof response === "object" && "data" in response) newRole = response.data.role || newRole;
5137
5440
  }
@@ -5143,6 +5446,7 @@ const updateMemberRole = (options) => {
5143
5446
  }],
5144
5447
  update: { role: newRole }
5145
5448
  });
5449
+ if (!member) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to update member role" });
5146
5450
  if (orgOptions?.organizationHooks?.afterUpdateMemberRole) await orgOptions.organizationHooks.afterUpdateMemberRole({
5147
5451
  member,
5148
5452
  user,
@@ -5166,8 +5470,8 @@ const inviteMember = (options) => {
5166
5470
  }))]
5167
5471
  }, async (ctx) => {
5168
5472
  const { organizationId } = ctx.context.payload;
5169
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
5170
- if (!organizationPlugin.options?.sendInvitationEmail) throw ctx.error("BAD_REQUEST", { message: "Invitation email is not enabled" });
5473
+ const organizationPlugin = ctx.context.getPlugin("organization");
5474
+ if (!organizationPlugin?.options?.sendInvitationEmail) throw ctx.error("BAD_REQUEST", { message: "Invitation email is not enabled" });
5171
5475
  const invitedBy = await ctx.context.adapter.findOne({
5172
5476
  model: "user",
5173
5477
  where: [{
@@ -5177,7 +5481,7 @@ const inviteMember = (options) => {
5177
5481
  });
5178
5482
  if (!invitedBy) throw ctx.error("BAD_REQUEST", { message: "Invited by user not found" });
5179
5483
  return await organizationPlugin.endpoints.createInvitation({
5180
- headers: ctx.request?.headers,
5484
+ headers: ctx.request?.headers ?? new Headers(),
5181
5485
  body: {
5182
5486
  email: ctx.body.email,
5183
5487
  role: ctx.body.role,
@@ -5244,7 +5548,7 @@ const cancelInvitation = (options) => {
5244
5548
  }))],
5245
5549
  body: z$1.object({ invitationId: z$1.string() })
5246
5550
  }, async (ctx) => {
5247
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
5551
+ const organizationPlugin = ctx.context.getPlugin("organization");
5248
5552
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin is not enabled" });
5249
5553
  const orgOptions = organizationPlugin.options || {};
5250
5554
  const { invitationId, organizationId } = ctx.context.payload;
@@ -5337,7 +5641,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
5337
5641
  if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
5338
5642
  idpMetadataXml = await metadataResponse.text();
5339
5643
  } catch (e) {
5340
- ctx.context.logger.error("Failed to fetch IdP metadata from URL", { error: e });
5644
+ ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
5341
5645
  throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
5342
5646
  }
5343
5647
  }
@@ -5351,10 +5655,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
5351
5655
  const warnings = validateSAMLMetadataAlgorithms(idpMetadataXml);
5352
5656
  if (warnings.length > 0) {
5353
5657
  metadataAlgorithmWarnings.push(...warnings);
5354
- ctx.context.logger.warn("SAML IdP metadata uses deprecated algorithms", {
5355
- providerId,
5356
- warnings
5357
- });
5658
+ ctx.context.logger.warn("[Dash] SAML IdP metadata uses deprecated algorithms:", providerId, warnings);
5358
5659
  }
5359
5660
  }
5360
5661
  const idpMetadata = idpMetadataXml ? { metadata: idpMetadataXml } : {
@@ -5370,8 +5671,8 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
5370
5671
  callbackUrl: `${baseURL}/sso/saml2/sp/acs/${providerId}`,
5371
5672
  idpMetadata,
5372
5673
  spMetadata: {},
5373
- ...samlConfig.entryPoint ? { entryPoint: samlConfig.entryPoint } : {},
5374
- ...samlConfig.cert ? { cert: samlConfig.cert } : {},
5674
+ entryPoint: samlConfig.entryPoint ?? "",
5675
+ cert: samlConfig.cert ?? "",
5375
5676
  ...samlConfig.mapping ? { mapping: samlConfig.mapping } : {}
5376
5677
  },
5377
5678
  ...metadataAlgorithmWarnings.length > 0 ? { warnings: metadataAlgorithmWarnings } : {}
@@ -5417,20 +5718,13 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
5417
5718
  };
5418
5719
  } catch (e) {
5419
5720
  if (e instanceof DiscoveryError) {
5420
- ctx.context.logger.error("OIDC discovery failed", {
5421
- code: e.code,
5422
- message: e.message,
5423
- details: e.details
5424
- });
5721
+ ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
5425
5722
  throw ctx.error("BAD_REQUEST", {
5426
5723
  message: `OIDC discovery failed: ${e.message}`,
5427
5724
  code: e.code
5428
5725
  });
5429
5726
  }
5430
- ctx.context.logger.error("OIDC discovery failed", {
5431
- issuer,
5432
- error: e
5433
- });
5727
+ ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
5434
5728
  throw ctx.error("BAD_REQUEST", {
5435
5729
  message: `OIDC discovery failed: Unable to discover configuration from ${issuer}`,
5436
5730
  code: "OIDC_DISCOVERY_FAILED"
@@ -5448,7 +5742,7 @@ const listOrganizationSsoProviders = (options) => {
5448
5742
  use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
5449
5743
  }, async (ctx) => {
5450
5744
  requireOrganizationAccess(ctx);
5451
- if (!ctx.context.options.plugins?.find((p) => p.id === "sso")) return [];
5745
+ if (!ctx.context.getPlugin("sso")) return [];
5452
5746
  try {
5453
5747
  return await ctx.context.adapter.findMany({
5454
5748
  model: "ssoProvider",
@@ -5457,7 +5751,8 @@ const listOrganizationSsoProviders = (options) => {
5457
5751
  value: ctx.params.id
5458
5752
  }]
5459
5753
  });
5460
- } catch {
5754
+ } catch (error) {
5755
+ ctx.context.logger.warn("[Dash] Failed to list SSO providers:", error);
5461
5756
  return [];
5462
5757
  }
5463
5758
  });
@@ -5477,7 +5772,7 @@ const createSsoProvider = (options) => {
5477
5772
  }, async (ctx) => {
5478
5773
  requireOrganizationAccess(ctx);
5479
5774
  const ssoPlugin = getSSOPlugin(ctx);
5480
- if (!ssoPlugin) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled" });
5775
+ if (!ssoPlugin?.endpoints?.registerSSOProvider) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled or feature is not supported in your plugin version" });
5481
5776
  const organizationId = ctx.params.id;
5482
5777
  const { providerId, domain, protocol, samlConfig, oidcConfig, userId } = ctx.body;
5483
5778
  const registerBody = {
@@ -5508,17 +5803,18 @@ const createSsoProvider = (options) => {
5508
5803
  return {
5509
5804
  success: true,
5510
5805
  provider: {
5511
- id: result.id,
5806
+ id: result.providerId,
5512
5807
  providerId: result.providerId || providerId,
5513
5808
  domain: result.domain || domain
5514
5809
  },
5515
5810
  domainVerification: {
5516
5811
  txtRecordName: `better-auth-token-${providerId}`,
5517
- verificationToken: result.domainVerificationToken || null
5812
+ verificationToken: result.domainVerificationToken ?? null
5518
5813
  }
5519
5814
  };
5520
5815
  } catch (e) {
5521
5816
  if (e instanceof APIError$1) throw e;
5817
+ ctx.context.logger.error("[Dash] Failed to create SSO provider:", e);
5522
5818
  throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to create SSO provider" });
5523
5819
  }
5524
5820
  });
@@ -5537,7 +5833,7 @@ const updateSsoProvider = (options) => {
5537
5833
  }, async (ctx) => {
5538
5834
  requireOrganizationAccess(ctx);
5539
5835
  const ssoPlugin = getSSOPlugin(ctx);
5540
- if (!ssoPlugin) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled" });
5836
+ if (!ssoPlugin?.endpoints?.updateSSOProvider) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled or feature is not supported in your plugin version" });
5541
5837
  const organizationId = ctx.params.id;
5542
5838
  const { providerId, domain, protocol, samlConfig, oidcConfig } = ctx.body;
5543
5839
  const existingProvider = await ctx.context.adapter.findOne({
@@ -5591,13 +5887,14 @@ const updateSsoProvider = (options) => {
5591
5887
  return {
5592
5888
  success: true,
5593
5889
  provider: {
5594
- id: result.id || existingProvider.id,
5595
- providerId: result.providerId || existingProvider.providerId,
5596
- domain: result.domain || domain
5890
+ id: existingProvider.id,
5891
+ providerId: result.providerId ?? existingProvider.providerId,
5892
+ domain: result.domain ?? domain
5597
5893
  }
5598
5894
  };
5599
5895
  } catch (e) {
5600
5896
  if (e instanceof APIError$1) throw e;
5897
+ ctx.context.logger.error("[Dash] Failed to update SSO provider:", e);
5601
5898
  throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to update SSO provider" });
5602
5899
  }
5603
5900
  });
@@ -5610,7 +5907,7 @@ const requestSsoVerificationToken = (options) => {
5610
5907
  }, async (ctx) => {
5611
5908
  requireOrganizationAccess(ctx);
5612
5909
  const ssoPlugin = getSSOPlugin(ctx);
5613
- if (!ssoPlugin || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled" });
5910
+ if (!ssoPlugin?.endpoints?.requestDomainVerification || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
5614
5911
  const organizationId = ctx.params.id;
5615
5912
  const { providerId } = ctx.body;
5616
5913
  const provider = await ctx.context.adapter.findOne({
@@ -5642,6 +5939,7 @@ const requestSsoVerificationToken = (options) => {
5642
5939
  };
5643
5940
  } catch (e) {
5644
5941
  if (e instanceof APIError$1) throw e;
5942
+ ctx.context.logger.error("[Dash] Failed to request verification token:", e);
5645
5943
  throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to request verification token" });
5646
5944
  }
5647
5945
  });
@@ -5654,7 +5952,7 @@ const verifySsoProviderDomain = (options) => {
5654
5952
  }, async (ctx) => {
5655
5953
  requireOrganizationAccess(ctx);
5656
5954
  const ssoPlugin = getSSOPlugin(ctx);
5657
- if (!ssoPlugin || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled" });
5955
+ if (!ssoPlugin?.endpoints?.verifyDomain || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
5658
5956
  const organizationId = ctx.params.id;
5659
5957
  const { providerId } = ctx.body;
5660
5958
  const provider = await ctx.context.adapter.findOne({
@@ -5704,7 +6002,7 @@ const deleteSsoProvider = (options) => {
5704
6002
  }, async (ctx) => {
5705
6003
  requireOrganizationAccess(ctx);
5706
6004
  const ssoPlugin = getSSOPlugin(ctx);
5707
- if (!ssoPlugin) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled" });
6005
+ if (!ssoPlugin?.endpoints?.deleteSSOProvider) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled or feature is not supported in your plugin version" });
5708
6006
  const organizationId = ctx.params.id;
5709
6007
  const { providerId } = ctx.body;
5710
6008
  const provider = await ctx.context.adapter.findOne({
@@ -5732,6 +6030,7 @@ const deleteSsoProvider = (options) => {
5732
6030
  };
5733
6031
  } catch (e) {
5734
6032
  if (e instanceof APIError$1) throw e;
6033
+ ctx.context.logger.error("[Dash] Failed to delete SSO provider:", e);
5735
6034
  throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to delete SSO provider" });
5736
6035
  }
5737
6036
  });
@@ -5783,7 +6082,7 @@ const resendInvitation = (options) => {
5783
6082
  }))],
5784
6083
  body: z$1.object({ invitationId: z$1.string() })
5785
6084
  }, async (ctx) => {
5786
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
6085
+ const organizationPlugin = ctx.context.getPlugin("organization");
5787
6086
  if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin is not enabled" });
5788
6087
  const { invitationId, organizationId } = ctx.context.payload;
5789
6088
  const invitation = await ctx.context.adapter.findOne({
@@ -5805,7 +6104,7 @@ const resendInvitation = (options) => {
5805
6104
  if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
5806
6105
  if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
5807
6106
  await organizationPlugin.endpoints.createInvitation({
5808
- headers: ctx.request?.headers,
6107
+ headers: ctx.request?.headers ?? new Headers(),
5809
6108
  body: {
5810
6109
  email: invitation.email,
5811
6110
  role: invitation.role,
@@ -5901,27 +6200,41 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
5901
6200
  await ctx.context.internalAdapter.deleteSessions(userId);
5902
6201
  return ctx.json({ success: true });
5903
6202
  });
6203
+ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
6204
+ method: "POST",
6205
+ use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))]
6206
+ }, async (ctx) => {
6207
+ const { userIds } = ctx.context.payload;
6208
+ await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
6209
+ for (const userId of chunk) await ctx.context.internalAdapter.deleteSessions(userId);
6210
+ }, { concurrency: 3 });
6211
+ return ctx.json({
6212
+ success: true,
6213
+ revokedCount: userIds.length
6214
+ });
6215
+ });
5904
6216
 
5905
6217
  //#endregion
5906
6218
  //#region src/routes/users.ts
6219
+ function parseWhereClause(val) {
6220
+ if (!val) return [];
6221
+ const parsed = JSON.parse(val);
6222
+ if (!Array.isArray(parsed)) return [];
6223
+ return parsed;
6224
+ }
6225
+ const getUsersQuerySchema = z$1.object({
6226
+ limit: z$1.number().or(z$1.string().transform(Number)).optional(),
6227
+ offset: z$1.number().or(z$1.string().transform(Number)).optional(),
6228
+ sortBy: z$1.string().optional(),
6229
+ sortOrder: z$1.enum(["asc", "desc"]).optional(),
6230
+ where: z$1.string().transform(parseWhereClause).optional(),
6231
+ countWhere: z$1.string().transform(parseWhereClause).optional()
6232
+ }).optional();
5907
6233
  const getUsers = (options) => {
5908
6234
  return createAuthEndpoint("/dash/list-users", {
5909
6235
  method: "GET",
5910
6236
  use: [jwtMiddleware(options)],
5911
- query: z$1.object({
5912
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
5913
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
5914
- sortBy: z$1.string().optional(),
5915
- sortOrder: z$1.enum(["asc", "desc"]).optional(),
5916
- where: z$1.string().transform((val) => {
5917
- if (!val) return [];
5918
- return JSON.parse(val);
5919
- }).optional(),
5920
- countWhere: z$1.string().transform((val) => {
5921
- if (!val) return [];
5922
- return JSON.parse(val);
5923
- }).optional()
5924
- }).optional()
6237
+ query: getUsersQuerySchema
5925
6238
  }, async (ctx) => {
5926
6239
  const activityTrackingEnabled = (() => {
5927
6240
  if (!options.activityTracking?.enabled) return false;
@@ -5930,6 +6243,8 @@ const getUsers = (options) => {
5930
6243
  if (fields.lastActiveAt.type !== "date") return false;
5931
6244
  return true;
5932
6245
  })();
6246
+ const where = ctx.query?.where?.length ? ctx.query.where : void 0;
6247
+ const countWhere = ctx.query?.countWhere?.length ? ctx.query.countWhere : void 0;
5933
6248
  const userQuery = ctx.context.adapter.findMany({
5934
6249
  model: "user",
5935
6250
  limit: ctx.query?.limit || 10,
@@ -5938,11 +6253,11 @@ const getUsers = (options) => {
5938
6253
  field: ctx.query?.sortBy || "createdAt",
5939
6254
  direction: ctx.query?.sortOrder || "desc"
5940
6255
  },
5941
- where: ctx.query?.where
6256
+ where
5942
6257
  });
5943
6258
  const totalQuery = ctx.context.adapter.count({
5944
6259
  model: "user",
5945
- where: ctx.query?.countWhere
6260
+ where: countWhere
5946
6261
  });
5947
6262
  const onlineUsersQuery = activityTrackingEnabled ? ctx.context.adapter.count({
5948
6263
  model: "user",
@@ -5951,13 +6266,16 @@ const getUsers = (options) => {
5951
6266
  value: /* @__PURE__ */ new Date(Date.now() - 1e3 * 60 * 2),
5952
6267
  operator: "gte"
5953
6268
  }]
6269
+ }).catch((e) => {
6270
+ ctx.context.logger.error("[Dash] Failed to count online users:", e);
6271
+ return 0;
5954
6272
  }) : Promise.resolve(0);
5955
6273
  const [users, total, onlineUsers] = await Promise.all([
5956
6274
  userQuery,
5957
6275
  totalQuery,
5958
6276
  onlineUsersQuery
5959
6277
  ]);
5960
- const hasAdminPlugin = ctx.context.options.plugins?.some((p) => p.id === "admin");
6278
+ const hasAdminPlugin = ctx.context.hasPlugin("admin");
5961
6279
  return {
5962
6280
  users: users.map((user) => {
5963
6281
  const u = user;
@@ -5976,6 +6294,30 @@ const getUsers = (options) => {
5976
6294
  };
5977
6295
  });
5978
6296
  };
6297
+ const exportUsers = (options) => {
6298
+ return createAuthEndpoint("/dash/export-users", {
6299
+ method: "GET",
6300
+ use: [jwtMiddleware(options)],
6301
+ query: getUsersQuerySchema
6302
+ }, async (ctx) => {
6303
+ const hasAdminPlugin = ctx.context.hasPlugin("admin");
6304
+ return exportFactory({
6305
+ model: "user",
6306
+ limit: ctx.query?.limit,
6307
+ offset: ctx.query?.offset ? ctx.query.offset : 0,
6308
+ sortBy: {
6309
+ field: ctx.query?.sortBy || "createdAt",
6310
+ direction: ctx.query?.sortOrder || "desc"
6311
+ },
6312
+ where: ctx.query?.where
6313
+ }, { processRow: (u) => ({
6314
+ ...u,
6315
+ banned: hasAdminPlugin ? u.banned ?? false : false,
6316
+ banReason: hasAdminPlugin ? u.banReason ?? null : null,
6317
+ banExpires: hasAdminPlugin ? u.banExpires ?? null : null
6318
+ }) })(ctx);
6319
+ });
6320
+ };
5979
6321
  const getOnlineUsersCount = (options) => {
5980
6322
  return createAuthEndpoint("/dash/online-users-count", {
5981
6323
  method: "GET",
@@ -6006,11 +6348,46 @@ const deleteUser = (options) => {
6006
6348
  }]
6007
6349
  });
6008
6350
  } catch (e) {
6009
- logger.error(e);
6351
+ ctx.context.logger.error("[Dash] Failed to delete user:", e);
6010
6352
  throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Internal server error" });
6011
6353
  }
6012
6354
  });
6013
6355
  };
6356
+ const deleteManyUsers = (options) => {
6357
+ return createAuthEndpoint("/dash/delete-many-users", {
6358
+ method: "POST",
6359
+ use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))]
6360
+ }, async (ctx) => {
6361
+ const { userIds } = ctx.context.payload;
6362
+ const deletedUserIds = /* @__PURE__ */ new Set();
6363
+ const skippedUserIds = /* @__PURE__ */ new Set();
6364
+ const start = performance.now();
6365
+ await withConcurrency(chunkArray(userIds), async (chunk) => {
6366
+ const where = [{
6367
+ field: "id",
6368
+ value: chunk,
6369
+ operator: "in"
6370
+ }];
6371
+ await ctx.context.adapter.deleteMany({
6372
+ model: "user",
6373
+ where
6374
+ });
6375
+ const remainingUsers = await ctx.context.adapter.findMany({
6376
+ model: "user",
6377
+ where
6378
+ });
6379
+ for (const id of chunk) if (!remainingUsers.some((u) => u.id === id)) deletedUserIds.add(id);
6380
+ else skippedUserIds.add(id);
6381
+ });
6382
+ const end = performance.now();
6383
+ console.log(`Time taken to bulk delete ${deletedUserIds.size} users: ${(end - start) / 1e3}s`, skippedUserIds.size > 0 ? `Failed: ${skippedUserIds.size}` : "");
6384
+ return ctx.json({
6385
+ success: deletedUserIds.size > 0,
6386
+ skippedUserIds: Array.from(skippedUserIds),
6387
+ deletedUserIds: Array.from(deletedUserIds)
6388
+ });
6389
+ });
6390
+ };
6014
6391
  const impersonateUser = (options) => {
6015
6392
  return createAuthEndpoint("/dash/impersonate-user", {
6016
6393
  method: "GET",
@@ -6096,10 +6473,10 @@ const createUser = (options) => {
6096
6473
  const organizationId = ctx.context.payload?.organizationId || userData.organizationId;
6097
6474
  const organizationRole = ctx.context.payload?.organizationRole || userData.organizationRole;
6098
6475
  if (organizationId) {
6099
- const organizationPlugin = ctx.context.options.plugins?.find((p) => p.id === "organization");
6476
+ const organizationPlugin = ctx.context.getPlugin("organization");
6100
6477
  if (organizationPlugin) {
6101
- const orgOptions = organizationPlugin?.options || {};
6102
- const role = organizationRole || orgOptions.defaultRole || "member";
6478
+ const orgOptions = organizationPlugin.options || {};
6479
+ const role = organizationRole || "member";
6103
6480
  const organization = await ctx.context.adapter.findOne({
6104
6481
  model: "organization",
6105
6482
  where: [{
@@ -6188,22 +6565,33 @@ const unlinkAccount = (options) => {
6188
6565
  const getUserDetails = (options) => {
6189
6566
  return createAuthEndpoint("/dash/user", {
6190
6567
  method: "GET",
6191
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6568
+ use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
6569
+ query: z$1.object({ minimal: z$1.boolean().or(z$1.string().transform((val) => val === "true")).optional() }).optional()
6192
6570
  }, async (ctx) => {
6193
6571
  const { userId } = ctx.context.payload;
6194
- const hasAdminPlugin = ctx.context.options.plugins?.some((p) => p.id === "admin");
6572
+ const minimal = !!ctx.query?.minimal;
6573
+ const hasAdminPlugin = ctx.context.hasPlugin("admin");
6195
6574
  const user = await ctx.context.adapter.findOne({
6196
6575
  model: "user",
6197
6576
  where: [{
6198
6577
  field: "id",
6199
6578
  value: userId
6200
6579
  }],
6201
- join: {
6580
+ ...minimal ? {} : { join: {
6202
6581
  account: true,
6203
6582
  session: true
6204
- }
6583
+ } }
6205
6584
  });
6206
6585
  if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
6586
+ if (minimal) return {
6587
+ ...user,
6588
+ lastActiveAt: user.lastActiveAt ?? null,
6589
+ banned: hasAdminPlugin ? user.banned ?? false : false,
6590
+ banReason: hasAdminPlugin ? user.banReason ?? null : null,
6591
+ banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
6592
+ account: [],
6593
+ session: []
6594
+ };
6207
6595
  const activityTrackingEnabled = !!options.activityTracking?.enabled;
6208
6596
  const sessions = user.session || [];
6209
6597
  let lastActiveAt = null;
@@ -6220,13 +6608,18 @@ const getUserDetails = (options) => {
6220
6608
  shouldUpdateLastActiveAt = true;
6221
6609
  }
6222
6610
  }
6223
- if (shouldUpdateLastActiveAt && lastActiveAt) try {
6224
- await ctx.context.internalAdapter.updateUser(userId, {
6225
- lastActiveAt,
6226
- updatedAt: /* @__PURE__ */ new Date()
6227
- });
6228
- } catch (error) {
6229
- ctx.context.logger.error("Failed to update user lastActiveAt:", error);
6611
+ if (shouldUpdateLastActiveAt && lastActiveAt) {
6612
+ const updateActivity = async () => {
6613
+ try {
6614
+ await ctx.context.internalAdapter.updateUser(userId, {
6615
+ lastActiveAt,
6616
+ updatedAt: /* @__PURE__ */ new Date()
6617
+ });
6618
+ } catch (error) {
6619
+ ctx.context.logger.error("[Dash] Failed to update user lastActiveAt:", error);
6620
+ }
6621
+ };
6622
+ updateActivity();
6230
6623
  }
6231
6624
  }
6232
6625
  return {
@@ -6244,43 +6637,41 @@ const getUserOrganizations = (options) => {
6244
6637
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6245
6638
  }, async (ctx) => {
6246
6639
  const { userId } = ctx.context.payload;
6247
- const isOrgEnabled = ctx.context.options.plugins?.find((p) => p.id === "organization");
6640
+ const isOrgEnabled = ctx.context.getPlugin("organization");
6248
6641
  if (!isOrgEnabled) return { organizations: [] };
6249
- const member = await ctx.context.adapter.findMany({
6642
+ const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
6643
+ const [member, teamMembers] = await Promise.all([ctx.context.adapter.findMany({
6250
6644
  model: "member",
6251
6645
  where: [{
6252
6646
  field: "userId",
6253
6647
  value: userId
6254
6648
  }]
6255
- });
6256
- if (member.length === 0) return { organizations: [] };
6257
- const organizations = await ctx.context.adapter.findMany({
6258
- model: "organization",
6259
- where: [{
6260
- field: "id",
6261
- value: member.map((m) => m.organizationId),
6262
- operator: "in"
6263
- }]
6264
- });
6265
- const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
6266
- const teamMembers = isTeamEnabled ? await ctx.context.adapter.findMany({
6649
+ }), isTeamEnabled ? ctx.context.adapter.findMany({
6267
6650
  model: "teamMember",
6268
6651
  where: [{
6269
6652
  field: "userId",
6270
6653
  value: userId
6271
6654
  }]
6272
6655
  }).catch((e) => {
6273
- ctx.context.logger.error(e);
6656
+ ctx.context.logger.error("[Dash] Failed to fetch team members:", e);
6274
6657
  return [];
6275
- }) : [];
6276
- const teams = isTeamEnabled && teamMembers.length > 0 ? await ctx.context.adapter.findMany({
6658
+ }) : Promise.resolve([])]);
6659
+ if (member.length === 0) return { organizations: [] };
6660
+ const [organizations, teams] = await Promise.all([ctx.context.adapter.findMany({
6661
+ model: "organization",
6662
+ where: [{
6663
+ field: "id",
6664
+ value: member.map((m) => m.organizationId),
6665
+ operator: "in"
6666
+ }]
6667
+ }), isTeamEnabled && teamMembers.length > 0 ? ctx.context.adapter.findMany({
6277
6668
  model: "team",
6278
6669
  where: [{
6279
6670
  field: "id",
6280
6671
  value: teamMembers.map((tm) => tm.teamId),
6281
6672
  operator: "in"
6282
6673
  }]
6283
- }) : [];
6674
+ }) : Promise.resolve([])]);
6284
6675
  return { organizations: organizations.map((organization) => ({
6285
6676
  id: organization.id,
6286
6677
  name: organization.name,
@@ -6314,9 +6705,25 @@ const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
6314
6705
  if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
6315
6706
  return user;
6316
6707
  });
6317
- async function countUniqueActiveUsers(adapter, where) {
6708
+ async function countUniqueActiveUsers(adapter, range, activityTrackingEnabled) {
6709
+ const field = activityTrackingEnabled ? "lastActiveAt" : "updatedAt";
6710
+ const where = [{
6711
+ field,
6712
+ operator: "gte",
6713
+ value: range.from
6714
+ }];
6715
+ if (range.to !== void 0) where.push({
6716
+ field,
6717
+ operator: "lt",
6718
+ value: range.to
6719
+ });
6720
+ if (activityTrackingEnabled) return adapter.count({
6721
+ model: "user",
6722
+ where
6723
+ });
6318
6724
  const sessions = await adapter.findMany({
6319
6725
  model: "session",
6726
+ select: ["userId"],
6320
6727
  where
6321
6728
  });
6322
6729
  return new Set(sessions.map((s) => s.userId)).size;
@@ -6332,6 +6739,7 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6332
6739
  const twoWeeksAgo = /* @__PURE__ */ new Date(now.getTime() - 336 * 60 * 60 * 1e3);
6333
6740
  const oneMonthAgo = /* @__PURE__ */ new Date(now.getTime() - 720 * 60 * 60 * 1e3);
6334
6741
  const twoMonthsAgo = /* @__PURE__ */ new Date(now.getTime() - 1440 * 60 * 60 * 1e3);
6742
+ const activityTrackingEnabled = !!options.activityTracking?.enabled;
6335
6743
  const [dailyCount, previousDailyCount, weeklyCount, previousWeeklyCount, monthlyCount, previousMonthlyCount, totalCount, dailyActiveCount, previousDailyActiveCount, weeklyActiveCount, previousWeeklyActiveCount, monthlyActiveCount, previousMonthlyActiveCount] = await Promise.all([
6336
6744
  ctx.context.adapter.count({
6337
6745
  model: "user",
@@ -6394,48 +6802,21 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6394
6802
  }]
6395
6803
  }),
6396
6804
  ctx.context.adapter.count({ model: "user" }),
6397
- countUniqueActiveUsers(ctx.context.adapter, [{
6398
- field: "updatedAt",
6399
- operator: "gte",
6400
- value: oneDayAgo
6401
- }]),
6402
- countUniqueActiveUsers(ctx.context.adapter, [{
6403
- field: "updatedAt",
6404
- operator: "gte",
6405
- value: twoDaysAgo
6406
- }, {
6407
- field: "updatedAt",
6408
- operator: "lt",
6409
- value: oneDayAgo
6410
- }]),
6411
- countUniqueActiveUsers(ctx.context.adapter, [{
6412
- field: "updatedAt",
6413
- operator: "gte",
6414
- value: oneWeekAgo
6415
- }]),
6416
- countUniqueActiveUsers(ctx.context.adapter, [{
6417
- field: "updatedAt",
6418
- operator: "gte",
6419
- value: twoWeeksAgo
6420
- }, {
6421
- field: "updatedAt",
6422
- operator: "lt",
6423
- value: oneWeekAgo
6424
- }]),
6425
- countUniqueActiveUsers(ctx.context.adapter, [{
6426
- field: "updatedAt",
6427
- operator: "gte",
6428
- value: oneMonthAgo
6429
- }]),
6430
- countUniqueActiveUsers(ctx.context.adapter, [{
6431
- field: "updatedAt",
6432
- operator: "gte",
6433
- value: twoMonthsAgo
6434
- }, {
6435
- field: "updatedAt",
6436
- operator: "lt",
6437
- value: oneMonthAgo
6438
- }])
6805
+ countUniqueActiveUsers(ctx.context.adapter, { from: oneDayAgo }, activityTrackingEnabled),
6806
+ countUniqueActiveUsers(ctx.context.adapter, {
6807
+ from: twoDaysAgo,
6808
+ to: oneDayAgo
6809
+ }, activityTrackingEnabled),
6810
+ countUniqueActiveUsers(ctx.context.adapter, { from: oneWeekAgo }, activityTrackingEnabled),
6811
+ countUniqueActiveUsers(ctx.context.adapter, {
6812
+ from: twoWeeksAgo,
6813
+ to: oneWeekAgo
6814
+ }, activityTrackingEnabled),
6815
+ countUniqueActiveUsers(ctx.context.adapter, { from: oneMonthAgo }, activityTrackingEnabled),
6816
+ countUniqueActiveUsers(ctx.context.adapter, {
6817
+ from: twoMonthsAgo,
6818
+ to: oneMonthAgo
6819
+ }, activityTrackingEnabled)
6439
6820
  ]);
6440
6821
  const calculatePercentage = (current, previous) => {
6441
6822
  if (previous === 0) return current > 0 ? 100 : 0;
@@ -6482,6 +6863,7 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
6482
6863
  }, async (ctx) => {
6483
6864
  const { period } = ctx.query;
6484
6865
  const now = /* @__PURE__ */ new Date();
6866
+ const activityTrackingEnabled = !!options.activityTracking?.enabled;
6485
6867
  const intervals = period === "daily" ? 7 : period === "weekly" ? 8 : 6;
6486
6868
  const msPerInterval = period === "daily" ? 1440 * 60 * 1e3 : period === "weekly" ? 10080 * 60 * 1e3 : 720 * 60 * 60 * 1e3;
6487
6869
  const intervalData = [];
@@ -6522,15 +6904,10 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
6522
6904
  value: interval.endDate
6523
6905
  }]
6524
6906
  }),
6525
- countUniqueActiveUsers(ctx.context.adapter, [{
6526
- field: "updatedAt",
6527
- operator: "gt",
6528
- value: interval.startDate
6529
- }, {
6530
- field: "updatedAt",
6531
- operator: "lte",
6532
- value: interval.endDate
6533
- }])
6907
+ countUniqueActiveUsers(ctx.context.adapter, {
6908
+ from: interval.startDate,
6909
+ to: interval.endDate
6910
+ }, activityTrackingEnabled)
6534
6911
  ]);
6535
6912
  const results = await Promise.all(allQueries);
6536
6913
  return {
@@ -6565,9 +6942,10 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
6565
6942
  * M-N retention:
6566
6943
  * users created during Month(-N) who are active during "this month"
6567
6944
  *
6568
- * Active is determined by session activity (session.updatedAt in the active window).
6945
+ * Active: user.lastActiveAt when activity tracking enabled, else session.updatedAt.
6569
6946
  */
6570
6947
  const { period } = ctx.query;
6948
+ const activityTrackingEnabled = !!options.activityTracking?.enabled;
6571
6949
  const now = /* @__PURE__ */ new Date();
6572
6950
  const startOfUtcDay = (d) => new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
6573
6951
  const startOfUtcMonth = (d) => new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
@@ -6598,6 +6976,7 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
6598
6976
  const cohortEnd = period === "daily" ? addUtcDays(cohortStart, 1) : period === "weekly" ? addUtcDays(cohortStart, 7) : addUtcMonths(cohortStart, 1);
6599
6977
  const cohortUsers = await ctx.context.adapter.findMany({
6600
6978
  model: "user",
6979
+ select: ["id"],
6601
6980
  where: [{
6602
6981
  field: "createdAt",
6603
6982
  operator: "gte",
@@ -6624,27 +7003,57 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
6624
7003
  continue;
6625
7004
  }
6626
7005
  const cohortUserIds = cohortUsers.map((u) => u.id);
6627
- const sessionsInActiveWindow = await ctx.context.adapter.findMany({
6628
- model: "session",
7006
+ let retained;
7007
+ if (activityTrackingEnabled) retained = await ctx.context.adapter.count({
7008
+ model: "user",
6629
7009
  where: [
6630
7010
  {
6631
- field: "userId",
7011
+ field: "id",
6632
7012
  operator: "in",
6633
7013
  value: cohortUserIds
6634
7014
  },
6635
7015
  {
6636
- field: "updatedAt",
7016
+ field: "lastActiveAt",
6637
7017
  operator: "gte",
6638
7018
  value: activeStart
6639
7019
  },
6640
7020
  {
6641
- field: "updatedAt",
7021
+ field: "lastActiveAt",
6642
7022
  operator: "lt",
6643
7023
  value: activeEnd
6644
7024
  }
6645
7025
  ]
6646
- }).catch(() => []);
6647
- const retained = new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
7026
+ }).catch((error) => {
7027
+ ctx.context.logger.warn("[Dash] Failed to count retained users by lastActiveAt:", error);
7028
+ return 0;
7029
+ });
7030
+ else {
7031
+ const sessionsInActiveWindow = await ctx.context.adapter.findMany({
7032
+ model: "session",
7033
+ select: ["userId"],
7034
+ where: [
7035
+ {
7036
+ field: "userId",
7037
+ operator: "in",
7038
+ value: cohortUserIds
7039
+ },
7040
+ {
7041
+ field: "updatedAt",
7042
+ operator: "gte",
7043
+ value: activeStart
7044
+ },
7045
+ {
7046
+ field: "updatedAt",
7047
+ operator: "lt",
7048
+ value: activeEnd
7049
+ }
7050
+ ]
7051
+ }).catch((error) => {
7052
+ ctx.context.logger.warn("[Dash] Failed to fetch cohort sessions:", error);
7053
+ return [];
7054
+ });
7055
+ retained = new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
7056
+ }
6648
7057
  const retentionRate = cohortSize > 0 ? Math.round(retained / cohortSize * 100 * 10) / 10 : 0;
6649
7058
  data.push({
6650
7059
  n,
@@ -6696,6 +7105,57 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
6696
7105
  });
6697
7106
  return { success: true };
6698
7107
  });
7108
+ const banManyUsers = (options) => {
7109
+ return createAuthEndpoint("/dash/ban-many-users", {
7110
+ method: "POST",
7111
+ use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
7112
+ body: z$1.object({
7113
+ banReason: z$1.string().optional(),
7114
+ banExpires: z$1.number().optional()
7115
+ })
7116
+ }, async (ctx) => {
7117
+ const { userIds } = ctx.context.payload;
7118
+ const { banReason, banExpires } = ctx.body;
7119
+ const start = performance.now();
7120
+ await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
7121
+ await ctx.context.adapter.updateMany({
7122
+ model: "user",
7123
+ where: [{
7124
+ field: "id",
7125
+ value: chunk,
7126
+ operator: "in"
7127
+ }],
7128
+ update: {
7129
+ banned: true,
7130
+ banReason: banReason || null,
7131
+ banExpires: banExpires ? new Date(banExpires) : null
7132
+ }
7133
+ });
7134
+ }, { concurrency: 3 });
7135
+ const skippedUserIds = (await ctx.context.adapter.findMany({
7136
+ model: "user",
7137
+ where: [{
7138
+ field: "id",
7139
+ value: userIds,
7140
+ operator: "in",
7141
+ connector: "AND"
7142
+ }, {
7143
+ field: "banned",
7144
+ value: false,
7145
+ operator: "eq"
7146
+ }],
7147
+ select: ["id"]
7148
+ })).map((u) => u.id);
7149
+ const bannedUserIds = userIds.filter((id) => !skippedUserIds.includes(id));
7150
+ const end = performance.now();
7151
+ console.log(`Time taken to ban ${bannedUserIds.length} users: ${(end - start) / 1e3}s`, skippedUserIds.length > 0 ? `Skipped: ${skippedUserIds.length}` : "");
7152
+ return ctx.json({
7153
+ success: bannedUserIds.length > 0,
7154
+ bannedUserIds,
7155
+ skippedUserIds
7156
+ });
7157
+ });
7158
+ };
6699
7159
  const unbanUser = (options) => createAuthEndpoint("/dash/unban-user", {
6700
7160
  method: "POST",
6701
7161
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
@@ -6730,6 +7190,63 @@ const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verifi
6730
7190
  }, user);
6731
7191
  return { success: true };
6732
7192
  });
7193
+ const sendManyVerificationEmails = (options) => {
7194
+ return createAuthEndpoint("/dash/send-many-verification-emails", {
7195
+ method: "POST",
7196
+ use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
7197
+ body: z$1.object({ callbackUrl: z$1.string().url() })
7198
+ }, async (ctx) => {
7199
+ if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
7200
+ const { userIds } = ctx.context.payload;
7201
+ const { callbackUrl } = ctx.body;
7202
+ const sentEmailUserIds = /* @__PURE__ */ new Set();
7203
+ const skippedEmailUserIds = /* @__PURE__ */ new Set();
7204
+ const start = performance.now();
7205
+ await withConcurrency(chunkArray(userIds, { batchSize: 20 }), async (chunk) => {
7206
+ const users = await ctx.context.adapter.findMany({
7207
+ model: "user",
7208
+ where: [{
7209
+ field: "id",
7210
+ value: chunk,
7211
+ operator: "in"
7212
+ }, {
7213
+ field: "emailVerified",
7214
+ value: false
7215
+ }]
7216
+ });
7217
+ if (chunk.length - users.length > 0) for (const id of chunk.filter((id$1) => !users.some((u) => u.id === id$1))) skippedEmailUserIds.add(id);
7218
+ for (const result of await Promise.allSettled(users.map(async (user) => {
7219
+ try {
7220
+ await sendVerificationEmailFn({
7221
+ ...ctx,
7222
+ body: {
7223
+ ...ctx.body,
7224
+ callbackURL: callbackUrl
7225
+ }
7226
+ }, user);
7227
+ } catch {
7228
+ return {
7229
+ success: false,
7230
+ id: user.id
7231
+ };
7232
+ }
7233
+ return {
7234
+ success: true,
7235
+ id: user.id
7236
+ };
7237
+ }))) if (result.status === "fulfilled") if (result.value.success) sentEmailUserIds.add(result.value.id);
7238
+ else skippedEmailUserIds.add(result.value.id);
7239
+ else for (const { id } of users) skippedEmailUserIds.add(id);
7240
+ }, { concurrency: 2 });
7241
+ const end = performance.now();
7242
+ console.log(`Time taken to send verification emails to ${sentEmailUserIds.size} users: ${Math.round((end - start) / 1e3)}s`, skippedEmailUserIds.size > 0 ? `Skipped: ${skippedEmailUserIds.size}` : "");
7243
+ return ctx.json({
7244
+ success: sentEmailUserIds.size > 0,
7245
+ sentEmailUserIds: Array.from(sentEmailUserIds),
7246
+ skippedEmailUserIds: Array.from(skippedEmailUserIds)
7247
+ });
7248
+ });
7249
+ };
6733
7250
  const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset-password-email", {
6734
7251
  method: "POST",
6735
7252
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
@@ -6748,6 +7265,11 @@ const getUserMapData = (options) => createAuthEndpoint("/dash/user-map-data", {
6748
7265
  }, async (ctx) => {
6749
7266
  const sessions = await ctx.context.adapter.findMany({
6750
7267
  model: "session",
7268
+ select: [
7269
+ "country",
7270
+ "city",
7271
+ "userId"
7272
+ ],
6751
7273
  limit: 1e4
6752
7274
  });
6753
7275
  const countryMap = /* @__PURE__ */ new Map();
@@ -6790,7 +7312,7 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
6790
7312
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6791
7313
  }, async (ctx) => {
6792
7314
  const { userId } = ctx.context.payload;
6793
- const twoFactorPlugin = ctx.context.options.plugins?.find((p) => p.id === "two-factor");
7315
+ const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6794
7316
  if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
6795
7317
  if (await ctx.context.adapter.findOne({
6796
7318
  model: "twoFactor",
@@ -6841,7 +7363,7 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
6841
7363
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6842
7364
  }, async (ctx) => {
6843
7365
  const { userId } = ctx.context.payload;
6844
- const twoFactorPlugin = ctx.context.options.plugins?.find((p) => p.id === "two-factor");
7366
+ const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6845
7367
  if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
6846
7368
  const twoFactorRecord = await ctx.context.adapter.findOne({
6847
7369
  model: "twoFactor",
@@ -6901,7 +7423,7 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
6901
7423
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6902
7424
  }, async (ctx) => {
6903
7425
  const { userId } = ctx.context.payload;
6904
- if (!ctx.context.options.plugins?.find((p) => p.id === "two-factor")) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
7426
+ if (!ctx.context.getPlugin("two-factor")) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
6905
7427
  await ctx.context.adapter.delete({
6906
7428
  model: "twoFactor",
6907
7429
  where: [{
@@ -6965,12 +7487,12 @@ async function sha256(message) {
6965
7487
  /**
6966
7488
  * Check if a hash has the required number of leading zero bits
6967
7489
  */
6968
- function hasLeadingZeroBits(hash, bits) {
7490
+ function hasLeadingZeroBits(hash$1, bits) {
6969
7491
  const fullHexChars = Math.floor(bits / 4);
6970
7492
  const remainingBits = bits % 4;
6971
- for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
6972
- if (remainingBits > 0 && fullHexChars < hash.length) {
6973
- if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
7493
+ for (let i = 0; i < fullHexChars; i++) if (hash$1[i] !== "0") return false;
7494
+ if (remainingBits > 0 && fullHexChars < hash$1.length) {
7495
+ if (parseInt(hash$1[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
6974
7496
  }
6975
7497
  return true;
6976
7498
  }
@@ -7069,6 +7591,7 @@ function createSMSSender(config) {
7069
7591
  messageId: (await response.json()).messageId
7070
7592
  };
7071
7593
  } catch (error) {
7594
+ logger.warn("[Dash] SMS send failed:", error);
7072
7595
  return {
7073
7596
  success: false,
7074
7597
  error: error instanceof Error ? error.message : "Failed to send SMS"
@@ -7127,7 +7650,7 @@ const dash = (options) => {
7127
7650
  return {
7128
7651
  id: "dash",
7129
7652
  init(ctx) {
7130
- const organizationPlugin = ctx.options.plugins?.find((p) => p.id === "organization");
7653
+ const organizationPlugin = ctx.getPlugin("organization");
7131
7654
  if (organizationPlugin) {
7132
7655
  const instrumentOrganizationHooks = (organizationPluginOptions) => {
7133
7656
  const organizationHooks = organizationPluginOptions.organizationHooks = organizationPluginOptions.organizationHooks ?? {};
@@ -7217,7 +7740,7 @@ const dash = (options) => {
7217
7740
  };
7218
7741
  };
7219
7742
  instrumentOrganizationHooks(organizationPlugin.options = organizationPlugin.options ?? {});
7220
- } else logger.debug("Organization plugin not active. Skipping instrumentation");
7743
+ } else logger.debug("[Dash] Organization plugin not active. Skipping instrumentation");
7221
7744
  return { options: {
7222
7745
  databaseHooks: {
7223
7746
  user: {
@@ -7269,8 +7792,10 @@ const dash = (options) => {
7269
7792
  const ctx$1 = _ctx;
7270
7793
  if (!ctx$1 || !session.userId) return;
7271
7794
  const location = ctx$1.context.location;
7795
+ const loginMethod = getLoginMethod(ctx$1) ?? void 0;
7272
7796
  const enrichedSession = {
7273
7797
  ...session,
7798
+ loginMethod,
7274
7799
  ipAddress: location?.ipAddress,
7275
7800
  city: location?.city,
7276
7801
  country: location?.country,
@@ -7303,7 +7828,9 @@ const dash = (options) => {
7303
7828
  }],
7304
7829
  update: { lastActiveAt: /* @__PURE__ */ new Date() }
7305
7830
  });
7306
- } catch {}
7831
+ } catch (error) {
7832
+ logger.warn("[Dash] Failed to update user activity", error);
7833
+ }
7307
7834
  }
7308
7835
  },
7309
7836
  delete: { async after(session, _ctx) {
@@ -7387,11 +7914,19 @@ const dash = (options) => {
7387
7914
  },
7388
7915
  hooks: {
7389
7916
  before: [{
7390
- matcher: (ctx) => ctx.request?.method !== "GET",
7917
+ matcher: (ctx) => {
7918
+ if (ctx.request?.method !== "GET") return true;
7919
+ const path = new URL(ctx.request.url).pathname;
7920
+ return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
7921
+ },
7391
7922
  handler: createIdentificationMiddleware(opts)
7392
7923
  }],
7393
7924
  after: [{
7394
- matcher: (ctx) => ctx.request?.method !== "GET",
7925
+ matcher: (ctx) => {
7926
+ if (ctx.request?.method !== "GET") return true;
7927
+ const path = new URL(ctx.request.url).pathname;
7928
+ return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
7929
+ },
7395
7930
  handler: createAuthMiddleware(async (_ctx) => {
7396
7931
  const ctx = _ctx;
7397
7932
  const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? UNKNOWN_USER);
@@ -7400,6 +7935,17 @@ const dash = (options) => {
7400
7935
  if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
7401
7936
  if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
7402
7937
  if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.context.returned instanceof Error) trackSocialSignInRedirectionAttempt(ctx, trigger);
7938
+ const headerRequestId = ctx.request?.headers.get("X-Request-Id");
7939
+ if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
7940
+ maxAge: 600,
7941
+ sameSite: "lax",
7942
+ httpOnly: true,
7943
+ path: "/"
7944
+ });
7945
+ else if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
7946
+ maxAge: 0,
7947
+ path: "/"
7948
+ });
7403
7949
  })
7404
7950
  }, {
7405
7951
  handler: createAuthMiddleware(async (ctx) => {
@@ -7428,10 +7974,13 @@ const dash = (options) => {
7428
7974
  endpoints: {
7429
7975
  getDashConfig: getConfig(opts),
7430
7976
  getDashUsers: getUsers(opts),
7977
+ exportDashUsers: exportUsers(opts),
7431
7978
  getOnlineUsersCount: getOnlineUsersCount(opts),
7432
7979
  createDashUser: createUser(opts),
7433
7980
  deleteDashUser: deleteUser(opts),
7981
+ deleteManyDashUsers: deleteManyUsers(opts),
7434
7982
  listDashOrganizations: listOrganizations(opts),
7983
+ exportDashOrganizations: exportOrganizations(opts),
7435
7984
  getDashOrganization: getOrganization(opts),
7436
7985
  listDashOrganizationMembers: listOrganizationMembers(opts),
7437
7986
  listDashOrganizationInvitations: listOrganizationInvitations(opts),
@@ -7446,6 +7995,7 @@ const dash = (options) => {
7446
7995
  listDashTeamMembers: listTeamMembers(opts),
7447
7996
  createDashOrganization: createOrganization(opts),
7448
7997
  deleteDashOrganization: deleteOrganization(opts),
7998
+ deleteManyDashOrganizations: deleteManyOrganizations(opts),
7449
7999
  getDashOrganizationOptions: getOrganizationOptions(opts),
7450
8000
  deleteDashSessions: deleteSessions(opts),
7451
8001
  getDashUser: getUserDetails(opts),
@@ -7456,6 +8006,7 @@ const dash = (options) => {
7456
8006
  listAllDashSessions: listAllSessions(opts),
7457
8007
  dashRevokeSession: revokeSession(opts),
7458
8008
  dashRevokeAllSessions: revokeAllSessions(opts),
8009
+ dashRevokeManySessions: revokeManySessions(opts),
7459
8010
  dashImpersonateUser: impersonateUser(opts),
7460
8011
  updateDashOrganization: updateOrganization(opts),
7461
8012
  createDashTeam: createTeam(opts),
@@ -7475,8 +8026,10 @@ const dash = (options) => {
7475
8026
  dashGetUserRetentionData: getUserRetentionData(opts),
7476
8027
  dashGetUserMapData: getUserMapData(opts),
7477
8028
  dashBanUser: banUser(opts),
8029
+ dashBanManyUsers: banManyUsers(opts),
7478
8030
  dashUnbanUser: unbanUser(opts),
7479
8031
  dashSendVerificationEmail: sendVerificationEmail(opts),
8032
+ dashSendManyVerificationEmails: sendManyVerificationEmails(opts),
7480
8033
  dashSendResetPasswordEmail: sendResetPasswordEmail(opts),
7481
8034
  dashEnableTwoFactor: enableTwoFactor(opts),
7482
8035
  dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(opts),
@@ -7506,4 +8059,4 @@ const dash = (options) => {
7506
8059
  };
7507
8060
 
7508
8061
  //#endregion
7509
- export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
8062
+ export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };