@better-auth/infra 0.1.8 → 0.1.9

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,7 +1,9 @@
1
- import { a as INFRA_API_URL, i as sendEmail, n as createEmailSender, o as INFRA_KV_URL, r as sendBulkEmails, t as EMAIL_TEMPLATES } from "./email-D2dL1i3c.mjs";
1
+ import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS, t as INFRA_API_URL } from "./constants-B-e0_Nsv.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";
6
+ import { getCurrentAuthContext } from "@better-auth/core/context";
5
7
  import { betterFetch, createFetch } from "@better-fetch/fetch";
6
8
  import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
7
9
  import { createLocalJWKSet, jwtVerify } from "jose";
@@ -132,21 +134,6 @@ const ORGANIZATION_EVENT_TYPES = {
132
134
  ORGANIZATION_TEAM_MEMBER_REMOVED: "organization_team_member_removed"
133
135
  };
134
136
 
135
- //#endregion
136
- //#region src/events/utils.ts
137
- function backgroundTask(task) {
138
- let result;
139
- try {
140
- result = task();
141
- } catch (error) {
142
- logger.debug("[Dash] Background operation failed: ", error);
143
- return;
144
- }
145
- Promise.resolve(result).catch((error) => {
146
- logger.debug("[Dash] Background operation failed: ", error);
147
- });
148
- }
149
-
150
137
  //#endregion
151
138
  //#region src/events/core/adapter.ts
152
139
  const getUserByEmail = async (email, ctx) => {
@@ -244,7 +231,7 @@ const initAccountEvents = (tracker) => {
244
231
  countryCode: location?.countryCode
245
232
  });
246
233
  };
247
- backgroundTask(track);
234
+ ctx.context.runInBackground(track());
248
235
  };
249
236
  const trackAccountUnlink = (account, trigger, ctx, location) => {
250
237
  const track = async () => {
@@ -268,7 +255,7 @@ const initAccountEvents = (tracker) => {
268
255
  countryCode: location?.countryCode
269
256
  });
270
257
  };
271
- backgroundTask(track);
258
+ ctx.context.runInBackground(track());
272
259
  };
273
260
  const trackAccountPasswordChange = (account, trigger, ctx, location) => {
274
261
  const track = async () => {
@@ -292,7 +279,7 @@ const initAccountEvents = (tracker) => {
292
279
  countryCode: location?.countryCode
293
280
  });
294
281
  };
295
- backgroundTask(track);
282
+ ctx.context.runInBackground(track());
296
283
  };
297
284
  return {
298
285
  trackAccountLinking,
@@ -306,7 +293,7 @@ const initAccountEvents = (tracker) => {
306
293
  const stripQuery = (value) => value.split("?")[0] || value;
307
294
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
308
295
  const routeToRegex = (route) => {
309
- const pattern = escapeRegex(stripQuery(route)).replace(/\\\/:([^/]+)/g, "/[^/]+");
296
+ const pattern = escapeRegex(stripQuery(route)).replace(/\/:([^/]+)/g, "/[^/]+");
310
297
  return /* @__PURE__ */ new RegExp(`${pattern}(?:$|[/?])`);
311
298
  };
312
299
  const matchesAnyRoute = (path, routes$1) => {
@@ -357,7 +344,7 @@ const initSessionEvents = (tracker) => {
357
344
  countryCode: location?.countryCode
358
345
  });
359
346
  };
360
- backgroundTask(track);
347
+ ctx.context.runInBackground(track());
361
348
  };
362
349
  const trackUserSignedOut = (session, trigger, ctx, location) => {
363
350
  const track = async () => {
@@ -383,7 +370,7 @@ const initSessionEvents = (tracker) => {
383
370
  countryCode: location?.countryCode
384
371
  });
385
372
  };
386
- backgroundTask(track);
373
+ ctx.context.runInBackground(track());
387
374
  };
388
375
  const trackSessionRevoked = (session, trigger, ctx, location) => {
389
376
  const track = async () => {
@@ -409,7 +396,7 @@ const initSessionEvents = (tracker) => {
409
396
  countryCode: location?.countryCode
410
397
  });
411
398
  };
412
- backgroundTask(track);
399
+ ctx.context.runInBackground(track());
413
400
  };
414
401
  const trackSessionRevokedAll = (session, trigger, ctx, _location) => {
415
402
  const track = async () => {
@@ -427,7 +414,7 @@ const initSessionEvents = (tracker) => {
427
414
  }
428
415
  });
429
416
  };
430
- backgroundTask(track);
417
+ ctx.context.runInBackground(track());
431
418
  };
432
419
  const trackUserImpersonated = (session, trigger, ctx, location) => {
433
420
  const track = async () => {
@@ -456,7 +443,7 @@ const initSessionEvents = (tracker) => {
456
443
  countryCode: location?.countryCode
457
444
  });
458
445
  };
459
- backgroundTask(track);
446
+ ctx.context.runInBackground(track());
460
447
  };
461
448
  const trackUserImpersonationStop = (session, trigger, ctx, location) => {
462
449
  const track = async () => {
@@ -485,7 +472,7 @@ const initSessionEvents = (tracker) => {
485
472
  countryCode: location?.countryCode
486
473
  });
487
474
  };
488
- backgroundTask(track);
475
+ ctx.context.runInBackground(track());
489
476
  };
490
477
  const trackSessionCreated = (session, trigger, ctx, location) => {
491
478
  const track = async () => {
@@ -511,7 +498,7 @@ const initSessionEvents = (tracker) => {
511
498
  countryCode: location?.countryCode
512
499
  });
513
500
  };
514
- backgroundTask(track);
501
+ ctx.context.runInBackground(track());
515
502
  };
516
503
  const trackEmailVerificationSent = (session, user, trigger, _ctx) => {
517
504
  trackEvent({
@@ -545,7 +532,7 @@ const initSessionEvents = (tracker) => {
545
532
  }
546
533
  });
547
534
  };
548
- backgroundTask(track);
535
+ ctx.context.runInBackground(track());
549
536
  };
550
537
  const trackSocialSignInAttempt = (ctx, trigger) => {
551
538
  const track = async () => {
@@ -564,11 +551,11 @@ const initSessionEvents = (tracker) => {
564
551
  }
565
552
  });
566
553
  };
567
- backgroundTask(track);
554
+ ctx.context.runInBackground(track());
568
555
  };
569
556
  const trackSocialSignInRedirectionAttempt = (ctx, trigger) => {
570
557
  const track = async () => {
571
- const user = await getUserByAuthorizationCode(ctx.body.provider, ctx);
558
+ const user = await getUserByAuthorizationCode(ctx.params?.id, ctx);
572
559
  trackEvent({
573
560
  eventKey: user?.id.toString() ?? UNKNOWN_USER,
574
561
  eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
@@ -583,7 +570,7 @@ const initSessionEvents = (tracker) => {
583
570
  }
584
571
  });
585
572
  };
586
- backgroundTask(track);
573
+ ctx.context.runInBackground(track());
587
574
  };
588
575
  return {
589
576
  trackUserSignedIn,
@@ -772,7 +759,7 @@ const initVerificationEvents = (tracker) => {
772
759
  countryCode: location?.countryCode
773
760
  });
774
761
  };
775
- backgroundTask(track);
762
+ ctx.context.runInBackground(track());
776
763
  };
777
764
  const trackPasswordResetRequestCompletion = (verification, trigger, ctx, location) => {
778
765
  const track = async () => {
@@ -794,7 +781,7 @@ const initVerificationEvents = (tracker) => {
794
781
  countryCode: location?.countryCode
795
782
  });
796
783
  };
797
- backgroundTask(track);
784
+ ctx.context.runInBackground(track());
798
785
  };
799
786
  return {
800
787
  trackPasswordResetRequest,
@@ -805,7 +792,7 @@ const initVerificationEvents = (tracker) => {
805
792
  //#endregion
806
793
  //#region src/events/triggers.ts
807
794
  const getTriggerInfo = (ctx, userId, session) => {
808
- const sessionUserId = session?.userId ?? ctx.context.session?.session.userId ?? UNKNOWN_USER;
795
+ const sessionUserId = session?.userId ?? ctx.context.session?.session.userId ?? userId;
809
796
  return {
810
797
  triggeredBy: sessionUserId,
811
798
  triggerContext: sessionUserId === userId ? "user" : matchesAnyRoute(ctx.path, [routes.ADMIN_ROUTE]) ? "admin" : matchesAnyRoute(ctx.path, [routes.DASH_ROUTE]) ? "dashboard" : sessionUserId === UNKNOWN_USER ? "user" : "unknown"
@@ -850,7 +837,13 @@ const initTrackEvents = (options) => {
850
837
  logger.debug("[Dash] Failed to track event:", e);
851
838
  }
852
839
  };
853
- track();
840
+ const onSuccess = (ctx) => {
841
+ ctx.context.runInBackground(track());
842
+ };
843
+ const onError = () => {
844
+ track();
845
+ };
846
+ getCurrentAuthContext().then(onSuccess, onError);
854
847
  };
855
848
  return { tracker: { trackEvent } };
856
849
  };
@@ -863,6 +856,7 @@ const initTrackEvents = (options) => {
863
856
  * Fetches identification data from the durable-kv service
864
857
  * when a request includes an X-Request-Id header.
865
858
  */
859
+ const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
866
860
  const identificationCache = /* @__PURE__ */ new Map();
867
861
  const CACHE_TTL_MS = 6e4;
868
862
  const CACHE_MAX_SIZE = 1e3;
@@ -892,7 +886,8 @@ async function getIdentification(requestId, apiKey, kvUrl) {
892
886
  for (let attempt = 0; attempt <= maxRetries; attempt++) try {
893
887
  const response = await fetch(`${baseUrl}/identify/${requestId}`, {
894
888
  method: "GET",
895
- headers: { "x-api-key": apiKey }
889
+ headers: { "x-api-key": apiKey },
890
+ signal: AbortSignal.timeout(KV_TIMEOUT_MS)
896
891
  });
897
892
  if (response.ok) {
898
893
  const data = await response.json();
@@ -938,7 +933,8 @@ function extractIdentificationHeaders(request) {
938
933
  */
939
934
  function createIdentificationMiddleware(options) {
940
935
  return createAuthMiddleware(async (ctx) => {
941
- const { visitorId, requestId } = extractIdentificationHeaders(ctx.request);
936
+ const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
937
+ const requestId = headerRequestId ?? ctx.getCookie(IDENTIFICATION_COOKIE_NAME) ?? null;
942
938
  ctx.context.visitorId = visitorId;
943
939
  ctx.context.requestId = requestId;
944
940
  if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
@@ -1249,9 +1245,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
1249
1245
  },
1250
1246
  async checkCompromisedPassword(password) {
1251
1247
  try {
1252
- const hash = await sha1Hash(password);
1253
- const prefix = hash.substring(0, 5);
1254
- const suffix = hash.substring(5);
1248
+ const hash$1 = await sha1Hash(password);
1249
+ const prefix = hash$1.substring(0, 5);
1250
+ const suffix = hash$1.substring(5);
1255
1251
  const data = await $api("/security/breached-passwords", {
1256
1252
  method: "POST",
1257
1253
  body: {
@@ -1633,7 +1629,8 @@ function createEmailValidator(options = {}) {
1633
1629
  });
1634
1630
  const $kv = createFetch({
1635
1631
  baseURL: kvUrl,
1636
- headers: { "x-api-key": apiKey }
1632
+ headers: { "x-api-key": apiKey },
1633
+ timeout: KV_TIMEOUT_MS
1637
1634
  });
1638
1635
  /**
1639
1636
  * Fetch and resolve email validity policy from API with caching
@@ -2037,7 +2034,7 @@ const sentinel = (options) => {
2037
2034
  const { trackEvent } = tracker;
2038
2035
  if (!opts.apiKey) logger.warn("[Sentinel] Missing BETTER_AUTH_API_KEY. Security checks may fall back to allow mode when the Infra API rejects requests.");
2039
2036
  const securityService = createSecurityClient(opts.apiUrl, opts.apiKey, opts.security || {}, (event) => {
2040
- tracker.trackEvent({
2037
+ trackEvent({
2041
2038
  eventKey: event.visitorId || event.userId || "unknown",
2042
2039
  eventType: `security_${event.type}`,
2043
2040
  eventDisplayName: `Security: ${event.type.replace(/_/g, " ")}`,
@@ -2068,7 +2065,7 @@ const sentinel = (options) => {
2068
2065
  const displayName = isNoMxRecord ? `Security: invalid email domain ${actionVerb}` : `Security: disposable email ${actionVerb}`;
2069
2066
  const description = isNoMxRecord ? `${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)} signup attempt with invalid email domain (no MX records): ${data.email}` : `${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)} signup attempt with disposable email: ${data.email} (${data.reason}, ${data.confidence} confidence)`;
2070
2067
  logger.info(`[Sentinel] Tracking ${reason} event for email: ${data.email} (action: ${actionLabel})`);
2071
- tracker.trackEvent({
2068
+ trackEvent({
2072
2069
  eventKey: data.email,
2073
2070
  eventType,
2074
2071
  eventDisplayName: displayName,
@@ -2086,28 +2083,28 @@ const sentinel = (options) => {
2086
2083
  });
2087
2084
  return {
2088
2085
  id: "sentinel",
2089
- init(ctx) {
2086
+ init() {
2090
2087
  return { options: { databaseHooks: {
2091
2088
  user: { create: {
2092
- async before(_user, hookCtx) {
2093
- if (!hookCtx) return;
2094
- const visitorId = hookCtx.context.visitorId;
2089
+ async before(_user, ctx) {
2090
+ if (!ctx) return;
2091
+ const visitorId = ctx.context.visitorId;
2095
2092
  if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
2096
2093
  const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
2097
2094
  if (abuseCheck.isAbuse && abuseCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2098
2095
  }
2099
2096
  },
2100
- async after(user, hookCtx) {
2101
- if (!hookCtx) return;
2102
- const visitorId = hookCtx.context.visitorId;
2103
- if (visitorId && opts.security?.freeTrialAbuse?.enabled) backgroundTask(() => securityService.trackFreeTrialSignup(visitorId, user.id));
2097
+ async after(user, ctx) {
2098
+ if (!ctx) return;
2099
+ const visitorId = ctx.context.visitorId;
2100
+ if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
2104
2101
  }
2105
2102
  } },
2106
2103
  session: { create: {
2107
- async before(session, hookCtx) {
2108
- if (!hookCtx) return;
2109
- const visitorId = hookCtx.context.visitorId;
2110
- const identification = hookCtx.context.identification;
2104
+ async before(session, ctx) {
2105
+ if (!ctx) return;
2106
+ const visitorId = ctx.context.visitorId;
2107
+ const identification = ctx.context.identification;
2111
2108
  if (session.userId && identification?.location && visitorId) {
2112
2109
  const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
2113
2110
  if (travelCheck?.isImpossible) {
@@ -2116,13 +2113,13 @@ const sentinel = (options) => {
2116
2113
  }
2117
2114
  }
2118
2115
  },
2119
- async after(session, hookCtx) {
2120
- if (!hookCtx || !session.userId) return;
2121
- const visitorId = hookCtx.context.visitorId;
2122
- const identification = hookCtx.context.identification;
2116
+ async after(session, ctx) {
2117
+ if (!ctx || !session.userId) return;
2118
+ const visitorId = ctx.context.visitorId;
2119
+ const identification = ctx.context.identification;
2123
2120
  let user = null;
2124
2121
  try {
2125
- user = await hookCtx.context.adapter.findOne({
2122
+ user = await ctx.context.adapter.findOne({
2126
2123
  model: "user",
2127
2124
  select: [
2128
2125
  "email",
@@ -2138,7 +2135,7 @@ const sentinel = (options) => {
2138
2135
  logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2139
2136
  }
2140
2137
  if (visitorId) {
2141
- if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) backgroundTask(() => securityService.notifyUnknownDevice(session.userId, user.email, identification));
2138
+ if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
2142
2139
  }
2143
2140
  if (opts.security?.staleUsers?.enabled && user) {
2144
2141
  const staleCheck = await securityService.checkStaleUser(session.userId, user.lastActiveAt || null);
@@ -2177,7 +2174,7 @@ const sentinel = (options) => {
2177
2174
  });
2178
2175
  }
2179
2176
  }
2180
- if (identification?.location) backgroundTask(() => securityService.storeLastLocation(session.userId, identification.location));
2177
+ if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
2181
2178
  }
2182
2179
  } }
2183
2180
  } } };
@@ -2190,11 +2187,11 @@ const sentinel = (options) => {
2190
2187
  },
2191
2188
  {
2192
2189
  matcher: (ctx) => ctx.request?.method !== "GET",
2193
- handler: createAuthMiddleware(async (hookCtx) => {
2194
- const visitorId = hookCtx.context.visitorId;
2195
- const powSolution = hookCtx.headers?.get?.("X-PoW-Solution");
2190
+ handler: createAuthMiddleware(async (ctx) => {
2191
+ const visitorId = ctx.context.visitorId;
2192
+ const powSolution = ctx.headers?.get?.("X-PoW-Solution");
2196
2193
  if (visitorId && powSolution) {
2197
- if ((await securityService.verifyPoWSolution(visitorId, powSolution)).valid) hookCtx.context.powVerified = true;
2194
+ if ((await securityService.verifyPoWSolution(visitorId, powSolution)).valid) ctx.context.powVerified = true;
2198
2195
  }
2199
2196
  })
2200
2197
  },
@@ -2202,10 +2199,10 @@ const sentinel = (options) => {
2202
2199
  ...phoneValidationHooks.before,
2203
2200
  {
2204
2201
  matcher: (ctx) => ctx.request?.method !== "GET",
2205
- handler: createAuthMiddleware(async (hookCtx) => {
2206
- const visitorId = hookCtx.context.visitorId;
2207
- const identification = hookCtx.context.identification;
2208
- const isSignIn = matchesAnyRoute(hookCtx.path, [
2202
+ handler: createAuthMiddleware(async (ctx) => {
2203
+ const visitorId = ctx.context.visitorId;
2204
+ const identification = ctx.context.identification;
2205
+ const isSignIn = matchesAnyRoute(ctx.path, [
2209
2206
  routes.SIGN_IN_EMAIL,
2210
2207
  routes.SIGN_IN_USERNAME,
2211
2208
  routes.SIGN_IN_EMAIL_OTP,
@@ -2215,17 +2212,17 @@ const sentinel = (options) => {
2215
2212
  routes.SIGN_IN_SSO,
2216
2213
  routes.SIGN_IN_ANONYMOUS
2217
2214
  ]);
2218
- const isSignUp = matchesAnyRoute(hookCtx.path, [routes.SIGN_UP_EMAIL]);
2219
- const isPasswordReset = matchesAnyRoute(hookCtx.path, [routes.FORGET_PASSWORD, routes.REQUEST_PASSWORD_RESET]);
2220
- const isTwoFactor = matchesAnyRoute(hookCtx.path, [
2215
+ const isSignUp = matchesAnyRoute(ctx.path, [routes.SIGN_UP_EMAIL]);
2216
+ const isPasswordReset = matchesAnyRoute(ctx.path, [routes.FORGET_PASSWORD, routes.REQUEST_PASSWORD_RESET]);
2217
+ const isTwoFactor = matchesAnyRoute(ctx.path, [
2221
2218
  routes.TWO_FACTOR_VERIFY_TOTP,
2222
2219
  routes.TWO_FACTOR_VERIFY_BACKUP,
2223
2220
  routes.TWO_FACTOR_VERIFY_OTP
2224
2221
  ]);
2225
- const isOtpSend = matchesAnyRoute(hookCtx.path, [routes.EMAIL_OTP_SEND, routes.PHONE_SEND_OTP]);
2226
- const isMagicLinkVerify = matchesAnyRoute(hookCtx.path, [routes.MAGIC_LINK_VERIFY]);
2227
- const isOrgCreate = matchesAnyRoute(hookCtx.path, [routes.ORG_CREATE]);
2228
- const isSensitiveAction = matchesAnyRoute(hookCtx.path, [
2222
+ const isOtpSend = matchesAnyRoute(ctx.path, [routes.EMAIL_OTP_SEND, routes.PHONE_SEND_OTP]);
2223
+ const isMagicLinkVerify = matchesAnyRoute(ctx.path, [routes.MAGIC_LINK_VERIFY]);
2224
+ const isOrgCreate = matchesAnyRoute(ctx.path, [routes.ORG_CREATE]);
2225
+ const isSensitiveAction = matchesAnyRoute(ctx.path, [
2229
2226
  routes.CHANGE_EMAIL,
2230
2227
  routes.CHANGE_PASSWORD,
2231
2228
  routes.SET_PASSWORD,
@@ -2233,9 +2230,9 @@ const sentinel = (options) => {
2233
2230
  routes.PASSKEY_ADD
2234
2231
  ]);
2235
2232
  if (!(isSignIn || isSignUp || isPasswordReset || isTwoFactor || isOtpSend || isMagicLinkVerify || isOrgCreate || isSensitiveAction)) return;
2236
- const requestBody = hookCtx.body;
2233
+ const requestBody = ctx.body;
2237
2234
  const identifier = requestBody?.email || requestBody?.phone || requestBody?.username || void 0;
2238
- const powVerified = hookCtx.context.powVerified === true;
2235
+ const powVerified = ctx.context.powVerified === true;
2239
2236
  if (visitorId && powVerified) trackEvent({
2240
2237
  eventKey: visitorId,
2241
2238
  eventType: "security_allowed",
@@ -2244,8 +2241,8 @@ const sentinel = (options) => {
2244
2241
  action: "allowed",
2245
2242
  reason: "pow_verified",
2246
2243
  visitorId,
2247
- path: hookCtx.path,
2248
- userAgent: hookCtx.headers?.get?.("user-agent") || "",
2244
+ path: ctx.path,
2245
+ userAgent: ctx.headers?.get?.("user-agent") || "",
2249
2246
  identifier,
2250
2247
  detectionLabel: "Challenge Completed",
2251
2248
  description: identifier ? `Successfully completed security challenge for "${identifier}"` : "Successfully completed security challenge"
@@ -2265,8 +2262,8 @@ const sentinel = (options) => {
2265
2262
  action: "blocked",
2266
2263
  reason: "credential_stuffing",
2267
2264
  visitorId,
2268
- path: hookCtx.path,
2269
- userAgent: hookCtx.headers?.get?.("user-agent") || "",
2265
+ path: ctx.path,
2266
+ userAgent: ctx.headers?.get?.("user-agent") || "",
2270
2267
  identifier,
2271
2268
  detectionType: "cooldown_active",
2272
2269
  detectionLabel: "Credential Stuffing",
@@ -2282,19 +2279,19 @@ const sentinel = (options) => {
2282
2279
  }
2283
2280
  }
2284
2281
  await runSecurityChecks({
2285
- path: hookCtx.path,
2282
+ path: ctx.path,
2286
2283
  identifier,
2287
2284
  visitorId,
2288
2285
  identification,
2289
- userAgent: hookCtx.headers?.get?.("user-agent") || ""
2286
+ userAgent: ctx.headers?.get?.("user-agent") || ""
2290
2287
  }, securityService, trackEvent, powVerified);
2291
- if (matchesAnyRoute(hookCtx.path, [
2288
+ if (matchesAnyRoute(ctx.path, [
2292
2289
  routes.SIGN_UP_EMAIL,
2293
2290
  routes.CHANGE_PASSWORD,
2294
2291
  routes.SET_PASSWORD,
2295
2292
  routes.RESET_PASSWORD
2296
2293
  ])) {
2297
- const body = hookCtx.body;
2294
+ const body = ctx.body;
2298
2295
  const passwordToCheck = body?.newPassword || body?.password;
2299
2296
  if (passwordToCheck) {
2300
2297
  const compromisedResult = await securityService.checkCompromisedPassword(passwordToCheck);
@@ -2311,18 +2308,18 @@ const sentinel = (options) => {
2311
2308
  ],
2312
2309
  after: [{
2313
2310
  matcher: (ctx) => ctx.request?.method !== "GET",
2314
- handler: createAuthMiddleware(async (hookCtx) => {
2315
- const visitorId = hookCtx.context.visitorId;
2316
- const identification = hookCtx.context.identification;
2317
- const body = hookCtx.body;
2318
- if (matchesAnyRoute(hookCtx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && hookCtx.context.returned instanceof Error && body?.email && body?.password && visitorId) {
2311
+ handler: createAuthMiddleware(async (ctx) => {
2312
+ const visitorId = ctx.context.visitorId;
2313
+ const identification = ctx.context.identification;
2314
+ const body = ctx.body;
2315
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email && body?.password && visitorId) {
2319
2316
  const { email, password } = body;
2320
2317
  const ip = identification?.ip || null;
2321
- backgroundTask(() => securityService.trackFailedAttempt(email, visitorId, password, ip));
2318
+ await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(email, visitorId, password, ip));
2322
2319
  }
2323
- if (matchesAnyRoute(hookCtx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && !(hookCtx.context.returned instanceof Error) && body?.email) {
2320
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && !(ctx.context.returned instanceof Error) && body?.email) {
2324
2321
  const email = body.email;
2325
- backgroundTask(() => securityService.clearFailedAttempts(email));
2322
+ await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(email));
2326
2323
  }
2327
2324
  })
2328
2325
  }]
@@ -2635,6 +2632,17 @@ const initTeamEvents = (tracker) => {
2635
2632
  //#endregion
2636
2633
  //#region src/jwt.ts
2637
2634
  /**
2635
+ * Hash the given value
2636
+ * Note: Must match @infra/crypto hash()
2637
+ * @param value - The value to hash
2638
+ */
2639
+ async function hash(value) {
2640
+ const data = new TextEncoder().encode(value);
2641
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
2642
+ const hashArray = new Uint8Array(hashBuffer);
2643
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2644
+ }
2645
+ /**
2638
2646
  * Skip JTI check for JWTs issued within this many seconds.
2639
2647
  * This is safe because replay attacks require time for interception.
2640
2648
  * A freshly issued token is almost certainly legitimate.
@@ -2695,6 +2703,9 @@ const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (c
2695
2703
  }
2696
2704
  })).data?.valid) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2697
2705
  }
2706
+ const apiKeyHash = payload.apiKeyHash;
2707
+ if (typeof apiKeyHash !== "string" || !options.apiKey) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2708
+ if (apiKeyHash !== await hash(options.apiKey)) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2698
2709
  if (schema) {
2699
2710
  const parsed = schema.safeParse(payload);
2700
2711
  if (!parsed.success) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
@@ -2705,6 +2716,33 @@ const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (c
2705
2716
 
2706
2717
  //#endregion
2707
2718
  //#region src/routes/config.ts
2719
+ const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
2720
+ function isPlainSerializable(value) {
2721
+ if (value === null || typeof value !== "object") return true;
2722
+ if (Array.isArray(value)) return true;
2723
+ if (value instanceof Date) return true;
2724
+ const constructor = Object.getPrototypeOf(value)?.constructor;
2725
+ if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
2726
+ return true;
2727
+ }
2728
+ function sanitizePluginOptions(pluginId, options, seen = /* @__PURE__ */ new WeakSet()) {
2729
+ if (options === null || options === void 0) return options;
2730
+ if (typeof options === "function") return void 0;
2731
+ if (typeof options !== "object") return options;
2732
+ if (seen.has(options)) return void 0;
2733
+ seen.add(options);
2734
+ const excludeKeys = PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId];
2735
+ if (Array.isArray(options)) return options.map((item) => sanitizePluginOptions(pluginId, item, seen)).filter((item) => item !== void 0);
2736
+ const result = {};
2737
+ for (const [key, value] of Object.entries(options)) {
2738
+ if (excludeKeys?.has(key)) continue;
2739
+ if (typeof value === "function") continue;
2740
+ if (value !== null && typeof value === "object" && !isPlainSerializable(value)) continue;
2741
+ const sanitized = sanitizePluginOptions(pluginId, value, seen);
2742
+ if (sanitized !== void 0) result[key] = sanitized;
2743
+ }
2744
+ return result;
2745
+ }
2708
2746
  function estimateEntropy(str) {
2709
2747
  const unique = new Set(str).size;
2710
2748
  if (unique === 0) return 0;
@@ -2725,7 +2763,7 @@ const getConfig = (options) => {
2725
2763
  return {
2726
2764
  id: plugin.id,
2727
2765
  schema: plugin.schema,
2728
- options: plugin.options
2766
+ options: sanitizePluginOptions(plugin.id, plugin.options)
2729
2767
  };
2730
2768
  }),
2731
2769
  organization: {
@@ -4041,12 +4079,19 @@ const listOrganizations = (options) => {
4041
4079
  "members"
4042
4080
  ]).optional(),
4043
4081
  sortOrder: z$1.enum(["asc", "desc"]).optional(),
4082
+ filterMembers: z$1.enum([
4083
+ "abandoned",
4084
+ "eq1",
4085
+ "gt1",
4086
+ "gt5",
4087
+ "gt10"
4088
+ ]).optional(),
4044
4089
  search: z$1.string().optional(),
4045
4090
  startDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional(),
4046
4091
  endDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional()
4047
4092
  }).optional()
4048
4093
  }, async (ctx) => {
4049
- const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search } = ctx.query || {};
4094
+ const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search, filterMembers } = ctx.query || {};
4050
4095
  const where = [];
4051
4096
  if (search && search.trim().length > 0) {
4052
4097
  const searchTerm = search.trim();
@@ -4072,26 +4117,64 @@ const listOrganizations = (options) => {
4072
4117
  value: ctx.query.endDate,
4073
4118
  operator: "lte"
4074
4119
  });
4120
+ const needsInMemoryProcessing = sortBy === "members" || !!filterMembers;
4121
+ const dbSortBy = sortBy === "members" ? "createdAt" : sortBy;
4122
+ const fetchLimit = needsInMemoryProcessing ? 1e3 : limit;
4123
+ const fetchOffset = needsInMemoryProcessing ? 0 : offset;
4075
4124
  const [organizations, initialTotal] = await Promise.all([ctx.context.adapter.findMany({
4076
4125
  model: "organization",
4077
4126
  where,
4078
- limit,
4079
- offset,
4127
+ limit: fetchLimit,
4128
+ offset: fetchOffset,
4080
4129
  sortBy: {
4081
- field: sortBy,
4130
+ field: dbSortBy,
4082
4131
  direction: sortOrder
4083
- },
4084
- join: { member: true }
4085
- }), ctx.context.adapter.count({
4132
+ }
4133
+ }), needsInMemoryProcessing ? Promise.resolve(0) : ctx.context.adapter.count({
4086
4134
  model: "organization",
4087
4135
  where
4088
4136
  })]);
4089
- const withCounts = organizations.map((organization) => ({
4090
- ...organization,
4091
- memberCount: organization.member.length
4092
- }));
4137
+ const orgIds = organizations.map((o) => o.id);
4138
+ const allMembers = orgIds.length > 0 ? await ctx.context.adapter.findMany({
4139
+ model: "member",
4140
+ where: [{
4141
+ field: "organizationId",
4142
+ value: orgIds,
4143
+ operator: "in"
4144
+ }]
4145
+ }) : [];
4146
+ const membersByOrg = /* @__PURE__ */ new Map();
4147
+ for (const m of allMembers) {
4148
+ const list = membersByOrg.get(m.organizationId) || [];
4149
+ list.push(m);
4150
+ membersByOrg.set(m.organizationId, list);
4151
+ }
4152
+ let withCounts = organizations.map((organization) => {
4153
+ const orgMembers = membersByOrg.get(organization.id) || [];
4154
+ return {
4155
+ ...organization,
4156
+ _members: orgMembers,
4157
+ memberCount: orgMembers.length
4158
+ };
4159
+ });
4160
+ if (filterMembers) {
4161
+ const predicate = {
4162
+ abandoned: (c) => c === 0,
4163
+ eq1: (c) => c === 1,
4164
+ gt1: (c) => c > 1,
4165
+ gt5: (c) => c > 5,
4166
+ gt10: (c) => c > 10
4167
+ }[filterMembers];
4168
+ if (predicate) withCounts = withCounts.filter((o) => predicate(o.memberCount));
4169
+ }
4170
+ if (sortBy === "members") {
4171
+ const dir = sortOrder === "asc" ? 1 : -1;
4172
+ withCounts.sort((a, b) => (a.memberCount - b.memberCount) * dir);
4173
+ }
4174
+ const total = needsInMemoryProcessing ? withCounts.length : initialTotal;
4175
+ if (needsInMemoryProcessing) withCounts = withCounts.slice(offset, offset + limit);
4093
4176
  const allUserIds = /* @__PURE__ */ new Set();
4094
- for (const organization of withCounts) for (const member of organization.member.slice(0, 5)) allUserIds.add(member.userId);
4177
+ for (const organization of withCounts) for (const member of organization._members.slice(0, 5)) allUserIds.add(member.userId);
4095
4178
  const users = allUserIds.size > 0 ? await ctx.context.adapter.findMany({
4096
4179
  model: "user",
4097
4180
  where: [{
@@ -4103,32 +4186,36 @@ const listOrganizations = (options) => {
4103
4186
  const userMap = new Map(users.map((u) => [u.id, u]));
4104
4187
  return {
4105
4188
  organizations: withCounts.map((organization) => {
4106
- const members = organization.member.slice(0, 5).map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
4189
+ const members = organization._members.slice(0, 5).map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
4107
4190
  id: u.id,
4108
4191
  name: u.name,
4109
4192
  email: u.email,
4110
4193
  image: u.image
4111
4194
  }));
4195
+ const { _members, ...org } = organization;
4112
4196
  return {
4113
- ...organization,
4197
+ ...org,
4114
4198
  members
4115
4199
  };
4116
4200
  }),
4117
- total: initialTotal,
4201
+ total,
4118
4202
  offset,
4119
4203
  limit
4120
4204
  };
4121
4205
  });
4122
4206
  };
4207
+ function parseWhereClause$1(val) {
4208
+ if (!val) return [];
4209
+ const parsed = JSON.parse(val);
4210
+ if (!Array.isArray(parsed)) return [];
4211
+ return parsed;
4212
+ }
4123
4213
  const exportOrganizationsQuerySchema = z$1.object({
4124
4214
  limit: z$1.number().or(z$1.string().transform(Number)).optional(),
4125
4215
  offset: z$1.number().or(z$1.string().transform(Number)).optional(),
4126
4216
  sortBy: z$1.string().optional(),
4127
4217
  sortOrder: z$1.enum(["asc", "desc"]).optional(),
4128
- where: z$1.string().transform((val) => {
4129
- if (!val) return [];
4130
- return JSON.parse(val);
4131
- }).optional()
4218
+ where: z$1.string().transform(parseWhereClause$1).optional()
4132
4219
  }).optional();
4133
4220
  const exportOrganizations = (options) => {
4134
4221
  return createAuthEndpoint("/dash/export-organizations", {
@@ -5149,7 +5236,7 @@ const updateOrganization = (options) => {
5149
5236
  value: organizationId
5150
5237
  }],
5151
5238
  update: {
5152
- updateData,
5239
+ ...updateData,
5153
5240
  metadata: typeof updateData.metadata === "object" ? JSON.stringify(updateData.metadata) : updateData.metadata
5154
5241
  }
5155
5242
  });
@@ -6121,19 +6208,19 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
6121
6208
 
6122
6209
  //#endregion
6123
6210
  //#region src/routes/users.ts
6211
+ function parseWhereClause(val) {
6212
+ if (!val) return [];
6213
+ const parsed = JSON.parse(val);
6214
+ if (!Array.isArray(parsed)) return [];
6215
+ return parsed;
6216
+ }
6124
6217
  const getUsersQuerySchema = z$1.object({
6125
6218
  limit: z$1.number().or(z$1.string().transform(Number)).optional(),
6126
6219
  offset: z$1.number().or(z$1.string().transform(Number)).optional(),
6127
6220
  sortBy: z$1.string().optional(),
6128
6221
  sortOrder: z$1.enum(["asc", "desc"]).optional(),
6129
- where: z$1.string().transform((val) => {
6130
- if (!val) return [];
6131
- return JSON.parse(val);
6132
- }).optional(),
6133
- countWhere: z$1.string().transform((val) => {
6134
- if (!val) return [];
6135
- return JSON.parse(val);
6136
- }).optional()
6222
+ where: z$1.string().transform(parseWhereClause).optional(),
6223
+ countWhere: z$1.string().transform(parseWhereClause).optional()
6137
6224
  }).optional();
6138
6225
  const getUsers = (options) => {
6139
6226
  return createAuthEndpoint("/dash/list-users", {
@@ -6148,6 +6235,8 @@ const getUsers = (options) => {
6148
6235
  if (fields.lastActiveAt.type !== "date") return false;
6149
6236
  return true;
6150
6237
  })();
6238
+ const where = ctx.query?.where?.length ? ctx.query.where : void 0;
6239
+ const countWhere = ctx.query?.countWhere?.length ? ctx.query.countWhere : void 0;
6151
6240
  const userQuery = ctx.context.adapter.findMany({
6152
6241
  model: "user",
6153
6242
  limit: ctx.query?.limit || 10,
@@ -6156,11 +6245,11 @@ const getUsers = (options) => {
6156
6245
  field: ctx.query?.sortBy || "createdAt",
6157
6246
  direction: ctx.query?.sortOrder || "desc"
6158
6247
  },
6159
- where: ctx.query?.where
6248
+ where
6160
6249
  });
6161
6250
  const totalQuery = ctx.context.adapter.count({
6162
6251
  model: "user",
6163
- where: ctx.query?.countWhere
6252
+ where: countWhere
6164
6253
  });
6165
6254
  const onlineUsersQuery = activityTrackingEnabled ? ctx.context.adapter.count({
6166
6255
  model: "user",
@@ -6169,6 +6258,9 @@ const getUsers = (options) => {
6169
6258
  value: /* @__PURE__ */ new Date(Date.now() - 1e3 * 60 * 2),
6170
6259
  operator: "gte"
6171
6260
  }]
6261
+ }).catch((e) => {
6262
+ ctx.context.logger.error("[Dash] Failed to count online users:", e);
6263
+ return 0;
6172
6264
  }) : Promise.resolve(0);
6173
6265
  const [users, total, onlineUsers] = await Promise.all([
6174
6266
  userQuery,
@@ -7387,12 +7479,12 @@ async function sha256(message) {
7387
7479
  /**
7388
7480
  * Check if a hash has the required number of leading zero bits
7389
7481
  */
7390
- function hasLeadingZeroBits(hash, bits) {
7482
+ function hasLeadingZeroBits(hash$1, bits) {
7391
7483
  const fullHexChars = Math.floor(bits / 4);
7392
7484
  const remainingBits = bits % 4;
7393
- for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
7394
- if (remainingBits > 0 && fullHexChars < hash.length) {
7395
- if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
7485
+ for (let i = 0; i < fullHexChars; i++) if (hash$1[i] !== "0") return false;
7486
+ if (remainingBits > 0 && fullHexChars < hash$1.length) {
7487
+ if (parseInt(hash$1[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
7396
7488
  }
7397
7489
  return true;
7398
7490
  }
@@ -7692,8 +7784,10 @@ const dash = (options) => {
7692
7784
  const ctx$1 = _ctx;
7693
7785
  if (!ctx$1 || !session.userId) return;
7694
7786
  const location = ctx$1.context.location;
7787
+ const loginMethod = getLoginMethod(ctx$1) ?? void 0;
7695
7788
  const enrichedSession = {
7696
7789
  ...session,
7790
+ loginMethod,
7697
7791
  ipAddress: location?.ipAddress,
7698
7792
  city: location?.city,
7699
7793
  country: location?.country,
@@ -7812,11 +7906,19 @@ const dash = (options) => {
7812
7906
  },
7813
7907
  hooks: {
7814
7908
  before: [{
7815
- matcher: (ctx) => ctx.request?.method !== "GET",
7909
+ matcher: (ctx) => {
7910
+ if (ctx.request?.method !== "GET") return true;
7911
+ const path = new URL(ctx.request.url).pathname;
7912
+ return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
7913
+ },
7816
7914
  handler: createIdentificationMiddleware(opts)
7817
7915
  }],
7818
7916
  after: [{
7819
- 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
+ },
7820
7922
  handler: createAuthMiddleware(async (_ctx) => {
7821
7923
  const ctx = _ctx;
7822
7924
  const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? UNKNOWN_USER);
@@ -7825,6 +7927,17 @@ const dash = (options) => {
7825
7927
  if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
7826
7928
  if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
7827
7929
  if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.context.returned instanceof Error) trackSocialSignInRedirectionAttempt(ctx, trigger);
7930
+ const headerRequestId = ctx.request?.headers.get("X-Request-Id");
7931
+ if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
7932
+ maxAge: 600,
7933
+ sameSite: "lax",
7934
+ httpOnly: true,
7935
+ path: "/"
7936
+ });
7937
+ else if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
7938
+ maxAge: 0,
7939
+ path: "/"
7940
+ });
7828
7941
  })
7829
7942
  }, {
7830
7943
  handler: createAuthMiddleware(async (ctx) => {
@@ -7933,7 +8046,7 @@ const dash = (options) => {
7933
8046
  getDashDirectoryDetails: getDirectoryDetails(opts),
7934
8047
  dashExecuteAdapter: executeAdapter(opts)
7935
8048
  },
7936
- schema: { ...opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: { type: "date" } } } } : {} }
8049
+ schema: opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: { type: "date" } } } } : {}
7937
8050
  };
7938
8051
  };
7939
8052