@better-auth/infra 0.1.14 → 0.2.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,5 +1,4 @@
1
1
  import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS, t as INFRA_API_URL } from "./constants-DdWGfvz1.mjs";
2
- import { n as createIdentificationMiddleware, t as IDENTIFICATION_COOKIE_NAME } from "./identification-DF2nvmng.mjs";
3
2
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
3
  import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
5
4
  import { env } from "@better-auth/core/env";
@@ -10,7 +9,6 @@ import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-j
10
9
  import { createLocalJWKSet, jwtVerify } from "jose";
11
10
  import z$1, { z } from "zod";
12
11
  import { setSessionCookie } from "better-auth/cookies";
13
- import { DEFAULT_MAX_SAML_METADATA_SIZE, DigestAlgorithm, DiscoveryError, SignatureAlgorithm, discoverOIDCConfig } from "@better-auth/sso";
14
12
  //#region src/options.ts
15
13
  function resolveConnectionOptions(options) {
16
14
  return {
@@ -853,6 +851,152 @@ const initTrackEvents = (options) => {
853
851
  return { tracker: { trackEvent } };
854
852
  };
855
853
  //#endregion
854
+ //#region src/identification.ts
855
+ /**
856
+ * Identification Service
857
+ *
858
+ * Fetches identification data from the durable-kv service
859
+ * when a request includes an X-Request-Id header.
860
+ */
861
+ const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
862
+ const identificationCache = /* @__PURE__ */ new Map();
863
+ const CACHE_TTL_MS = 6e4;
864
+ const CACHE_MAX_SIZE = 1e3;
865
+ let lastCleanup = Date.now();
866
+ function cleanupCache() {
867
+ const now = Date.now();
868
+ for (const [key, value] of identificationCache.entries()) if (now - value.timestamp > CACHE_TTL_MS) identificationCache.delete(key);
869
+ lastCleanup = now;
870
+ }
871
+ function maybeCleanup() {
872
+ if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
873
+ }
874
+ /**
875
+ * Fetch identification data from durable-kv by requestId
876
+ */
877
+ async function getIdentification(requestId, apiKey, kvUrl) {
878
+ maybeCleanup();
879
+ const cached = identificationCache.get(requestId);
880
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
881
+ const baseUrl = kvUrl || INFRA_KV_URL;
882
+ const maxRetries = 3;
883
+ const retryDelays = [
884
+ 50,
885
+ 100,
886
+ 200
887
+ ];
888
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
889
+ const response = await fetch(`${baseUrl}/identify/${requestId}`, {
890
+ method: "GET",
891
+ headers: { "x-api-key": apiKey },
892
+ signal: AbortSignal.timeout(KV_TIMEOUT_MS)
893
+ });
894
+ if (response.ok) {
895
+ const data = await response.json();
896
+ identificationCache.set(requestId, {
897
+ data,
898
+ timestamp: Date.now()
899
+ });
900
+ return data;
901
+ }
902
+ if (response.status === 404 && attempt < maxRetries) {
903
+ await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
904
+ continue;
905
+ }
906
+ if (response.status !== 404) identificationCache.set(requestId, {
907
+ data: null,
908
+ timestamp: Date.now()
909
+ });
910
+ return null;
911
+ } catch (error) {
912
+ if (attempt === maxRetries) {
913
+ logger.error("[Dash] Failed to fetch identification:", error);
914
+ return null;
915
+ }
916
+ await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt] || 50));
917
+ }
918
+ return null;
919
+ }
920
+ /**
921
+ * Extract identification headers from a request
922
+ */
923
+ function extractIdentificationHeaders(request) {
924
+ if (!request) return {
925
+ visitorId: null,
926
+ requestId: null
927
+ };
928
+ return {
929
+ visitorId: request.headers.get("X-Visitor-Id"),
930
+ requestId: request.headers.get("X-Request-Id")
931
+ };
932
+ }
933
+ /**
934
+ * Early middleware that loads identification data
935
+ */
936
+ function createIdentificationMiddleware(options) {
937
+ return createAuthMiddleware(async (ctx) => {
938
+ const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
939
+ const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
940
+ ctx.context.visitorId = visitorId;
941
+ ctx.context.requestId = requestId;
942
+ if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
943
+ else ctx.context.identification = null;
944
+ const ipConfig = ctx.context.options?.advanced?.ipAddress;
945
+ if (ipConfig?.disableIpTracking === true) {
946
+ ctx.context.location = void 0;
947
+ return;
948
+ }
949
+ const identification = ctx.context.identification;
950
+ if (requestId && identification) {
951
+ const loc = getLocation(identification);
952
+ ctx.context.location = {
953
+ ipAddress: identification.ip || void 0,
954
+ city: loc?.city || void 0,
955
+ country: loc?.country?.name || void 0,
956
+ countryCode: loc?.country?.code || void 0
957
+ };
958
+ return;
959
+ }
960
+ const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
961
+ const countryCode = getCountryCodeFromRequest(ctx.request);
962
+ if (ipAddress || countryCode) {
963
+ ctx.context.location = {
964
+ ipAddress,
965
+ countryCode
966
+ };
967
+ return;
968
+ }
969
+ ctx.context.location = void 0;
970
+ });
971
+ }
972
+ /**
973
+ * Get the visitor's location
974
+ */
975
+ function getLocation(identification) {
976
+ if (!identification) return null;
977
+ return identification.location;
978
+ }
979
+ function getClientIpFromRequest(request, ipAddressHeaders) {
980
+ if (!request) return void 0;
981
+ const headers = ipAddressHeaders?.length ? ipAddressHeaders : [
982
+ "cf-connecting-ip",
983
+ "x-forwarded-for",
984
+ "x-real-ip",
985
+ "x-vercel-forwarded-for"
986
+ ];
987
+ for (const headerName of headers) {
988
+ const value = request.headers.get(headerName);
989
+ if (!value) continue;
990
+ const ip = value.split(",")[0]?.trim();
991
+ if (ip) return ip;
992
+ }
993
+ }
994
+ function getCountryCodeFromRequest(request) {
995
+ if (!request) return void 0;
996
+ const cc = request.headers.get("cf-ipcountry") ?? request.headers.get("x-vercel-ip-country");
997
+ return cc ? cc.toUpperCase() : void 0;
998
+ }
999
+ //#endregion
856
1000
  //#region src/validation/matchers.ts
857
1001
  const paths = [
858
1002
  "/sign-up/email",
@@ -2602,7 +2746,7 @@ const jwtValidateMiddleware = (options) => createAuthMiddleware(async (ctx) => {
2602
2746
  });
2603
2747
  //#endregion
2604
2748
  //#region src/version.ts
2605
- const PLUGIN_VERSION = "0.1.14";
2749
+ const PLUGIN_VERSION = "0.2.1";
2606
2750
  //#endregion
2607
2751
  //#region src/routes/auth/config.ts
2608
2752
  const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
@@ -2655,7 +2799,7 @@ const getConfig = (options) => {
2655
2799
  version: plugin.version,
2656
2800
  options: sanitizePluginOptions(plugin.id, plugin.options)
2657
2801
  };
2658
- if (plugin.id === "dash") return {
2802
+ if (plugin.id === "dash" && !plugin.version) return {
2659
2803
  ...base,
2660
2804
  version: PLUGIN_VERSION
2661
2805
  };
@@ -4089,11 +4233,13 @@ const listOrganizations = (options) => {
4089
4233
  field: "name",
4090
4234
  value: searchTerm,
4091
4235
  operator: "starts_with",
4236
+ mode: "insensitive",
4092
4237
  connector: "OR"
4093
4238
  }, {
4094
4239
  field: "slug",
4095
4240
  value: searchTerm,
4096
4241
  operator: "starts_with",
4242
+ mode: "insensitive",
4097
4243
  connector: "OR"
4098
4244
  });
4099
4245
  }
@@ -4109,7 +4255,7 @@ const listOrganizations = (options) => {
4109
4255
  });
4110
4256
  const needsInMemoryProcessing = sortBy === "members" || !!filterMembers;
4111
4257
  const dbSortBy = sortBy === "members" ? "createdAt" : sortBy;
4112
- const fetchLimit = needsInMemoryProcessing ? 1500 : limit;
4258
+ const fetchLimit = needsInMemoryProcessing ? 2500 : limit;
4113
4259
  const fetchOffset = needsInMemoryProcessing ? 0 : offset;
4114
4260
  const [organizations, initialTotal] = await Promise.all([ctx.context.adapter.findMany({
4115
4261
  model: "organization",
@@ -5254,8 +5400,16 @@ function getSSOPlugin(ctx) {
5254
5400
  }
5255
5401
  //#endregion
5256
5402
  //#region src/routes/sso-validation.ts
5257
- const DEPRECATED_SIGNATURE_ALGORITHMS = [SignatureAlgorithm.RSA_SHA1];
5258
- const DEPRECATED_DIGEST_ALGORITHMS = [DigestAlgorithm.SHA1];
5403
+ /**
5404
+ * SAML metadata limits and algorithm URIs aligned with @better-auth/sso
5405
+ * (see packages/sso dist constants). Inlined so consumers (e.g. Metro) never
5406
+ * statically pull @better-auth/sso for validation-only code paths.
5407
+ */
5408
+ const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
5409
+ const RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
5410
+ const SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1";
5411
+ const DEPRECATED_SIGNATURE_ALGORITHMS = [RSA_SHA1];
5412
+ const DEPRECATED_DIGEST_ALGORITHMS = [SHA1];
5259
5413
  function validateSAMLMetadataSize(metadataXml, maxSize = DEFAULT_MAX_SAML_METADATA_SIZE) {
5260
5414
  if (new TextEncoder().encode(metadataXml).byteLength > maxSize) throw new Error(`IdP metadata exceeds maximum allowed size (${Math.round(maxSize / 1024)}KB)`);
5261
5415
  }
@@ -5283,6 +5437,10 @@ function validateSAMLMetadataAlgorithms(metadataXml) {
5283
5437
  }
5284
5438
  //#endregion
5285
5439
  //#region src/routes/sso/index.ts
5440
+ let ssoRuntimeModule;
5441
+ function loadSsoRuntime() {
5442
+ return ssoRuntimeModule ??= import("@better-auth/sso");
5443
+ }
5286
5444
  function requireOrganizationAccess(ctx) {
5287
5445
  const orgIdFromUrl = tryDecode(ctx.params.id);
5288
5446
  const orgIdFromToken = ctx.context.payload?.organizationId;
@@ -5355,6 +5513,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
5355
5513
  }] } : {},
5356
5514
  ...samlConfig.cert ? { cert: samlConfig.cert } : {}
5357
5515
  };
5516
+ const m = samlConfig.mapping;
5358
5517
  return {
5359
5518
  config: {
5360
5519
  issuer: samlConfig.entityId ?? `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`,
@@ -5363,7 +5522,15 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
5363
5522
  spMetadata: {},
5364
5523
  entryPoint: samlConfig.entryPoint ?? "",
5365
5524
  cert: samlConfig.cert ?? "",
5366
- ...samlConfig.mapping ? { mapping: samlConfig.mapping } : {}
5525
+ ...m ? { mapping: {
5526
+ id: m.id ?? "nameID",
5527
+ email: m.email ?? "email",
5528
+ name: m.name ?? "name",
5529
+ emailVerified: m.emailVerified,
5530
+ firstName: m.firstName,
5531
+ lastName: m.lastName,
5532
+ extraFields: m.extraFields
5533
+ } } : {}
5367
5534
  },
5368
5535
  ...metadataAlgorithmWarnings.length > 0 ? { warnings: metadataAlgorithmWarnings } : {}
5369
5536
  };
@@ -5377,8 +5544,9 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
5377
5544
  }
5378
5545
  const issuerHint = oidcConfig.issuer || `https://${normalizedDomain}`;
5379
5546
  const issuer = issuerHint.startsWith("http") ? issuerHint : `https://${issuerHint}`;
5547
+ const sso = await loadSsoRuntime();
5380
5548
  try {
5381
- const hydratedConfig = await discoverOIDCConfig({
5549
+ const hydratedConfig = await sso.discoverOIDCConfig({
5382
5550
  issuer,
5383
5551
  discoveryEndpoint: oidcConfig.discoveryUrl,
5384
5552
  isTrustedOrigin: (url) => {
@@ -5390,6 +5558,7 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
5390
5558
  }
5391
5559
  }
5392
5560
  });
5561
+ const om = oidcConfig.mapping;
5393
5562
  return {
5394
5563
  config: {
5395
5564
  clientId: oidcConfig.clientId,
@@ -5402,12 +5571,19 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
5402
5571
  userInfoEndpoint: hydratedConfig.userInfoEndpoint,
5403
5572
  tokenEndpointAuthentication: hydratedConfig.tokenEndpointAuthentication,
5404
5573
  pkce: true,
5405
- ...oidcConfig.mapping ? { mapping: oidcConfig.mapping } : {}
5574
+ ...om ? { mapping: {
5575
+ id: om.id ?? "sub",
5576
+ email: om.email ?? "email",
5577
+ name: om.name ?? "name",
5578
+ emailVerified: om.emailVerified,
5579
+ image: om.image,
5580
+ extraFields: om.extraFields
5581
+ } } : {}
5406
5582
  },
5407
5583
  issuer: hydratedConfig.issuer
5408
5584
  };
5409
5585
  } catch (e) {
5410
- if (e instanceof DiscoveryError) {
5586
+ if (e instanceof sso.DiscoveryError) {
5411
5587
  ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
5412
5588
  throw ctx.error("BAD_REQUEST", {
5413
5589
  message: `OIDC discovery failed: ${e.message}`,
@@ -5494,6 +5670,8 @@ const createSsoProvider = (options) => {
5494
5670
  ...buildSessionContext(userId)
5495
5671
  }
5496
5672
  });
5673
+ let verificationToken = null;
5674
+ if ("domainVerificationToken" in result && typeof result.domainVerificationToken === "string") verificationToken = result.domainVerificationToken;
5497
5675
  return {
5498
5676
  success: true,
5499
5677
  provider: {
@@ -5503,7 +5681,7 @@ const createSsoProvider = (options) => {
5503
5681
  },
5504
5682
  domainVerification: {
5505
5683
  txtRecordName: `better-auth-token-${providerId}`,
5506
- verificationToken: result.domainVerificationToken ?? null
5684
+ verificationToken
5507
5685
  }
5508
5686
  };
5509
5687
  } catch (e) {
@@ -5604,7 +5782,9 @@ const requestSsoVerificationToken = (options) => {
5604
5782
  requireOrganizationPlugin(ctx);
5605
5783
  requireOrganizationAccess(ctx);
5606
5784
  const ssoPlugin = getSSOPlugin(ctx);
5607
- 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" });
5785
+ if (!ssoPlugin || !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" });
5786
+ const endpoints = ssoPlugin.endpoints;
5787
+ if (typeof endpoints.requestDomainVerification !== "function") throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
5608
5788
  const organizationId = tryDecode(ctx.params.id);
5609
5789
  const { providerId } = ctx.body;
5610
5790
  const provider = await ctx.context.adapter.findOne({
@@ -5620,7 +5800,7 @@ const requestSsoVerificationToken = (options) => {
5620
5800
  if (!provider) throw ctx.error("NOT_FOUND", { message: "SSO provider not found" });
5621
5801
  const txtRecordName = `${ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token"}-${provider.providerId}`;
5622
5802
  try {
5623
- const result = await ssoPlugin.endpoints.requestDomainVerification({
5803
+ const result = await endpoints.requestDomainVerification({
5624
5804
  body: { providerId },
5625
5805
  context: {
5626
5806
  ...ctx.context,
@@ -5650,7 +5830,9 @@ const verifySsoProviderDomain = (options) => {
5650
5830
  requireOrganizationPlugin(ctx);
5651
5831
  requireOrganizationAccess(ctx);
5652
5832
  const ssoPlugin = getSSOPlugin(ctx);
5653
- 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" });
5833
+ if (!ssoPlugin || !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" });
5834
+ const dvEndpoints = ssoPlugin.endpoints;
5835
+ if (typeof dvEndpoints.verifyDomain !== "function") throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
5654
5836
  const organizationId = tryDecode(ctx.params.id);
5655
5837
  const { providerId } = ctx.body;
5656
5838
  const provider = await ctx.context.adapter.findOne({
@@ -5665,7 +5847,7 @@ const verifySsoProviderDomain = (options) => {
5665
5847
  });
5666
5848
  if (!provider) throw ctx.error("NOT_FOUND", { message: "SSO provider not found" });
5667
5849
  try {
5668
- await ssoPlugin.endpoints.verifyDomain({
5850
+ await dvEndpoints.verifyDomain({
5669
5851
  body: { providerId },
5670
5852
  context: {
5671
5853
  ...ctx.context,
@@ -6443,7 +6625,7 @@ async function countUniqueActiveUsers(ctx, range, options) {
6443
6625
  break;
6444
6626
  }
6445
6627
  }
6446
- }, { concurrency: 50 });
6628
+ }, { concurrency: 10 });
6447
6629
  return activeUserIds.size;
6448
6630
  }
6449
6631
  const sessions = await ctx.context.adapter.findMany({
@@ -6453,6 +6635,34 @@ async function countUniqueActiveUsers(ctx, range, options) {
6453
6635
  });
6454
6636
  return new Set(sessions.map((s) => s.userId)).size;
6455
6637
  }
6638
+ function optionalQuery(ctx, label, getCount) {
6639
+ return async function runQuery() {
6640
+ try {
6641
+ return {
6642
+ ok: true,
6643
+ value: await getCount()
6644
+ };
6645
+ } catch (error) {
6646
+ ctx.context.logger.warn(`[Dash] User stats query "${label}" failed`, error);
6647
+ return {
6648
+ ok: false,
6649
+ value: null
6650
+ };
6651
+ }
6652
+ };
6653
+ }
6654
+ function signUpPeriod(current, previous, calculatePercentage) {
6655
+ return {
6656
+ signUps: current.ok ? current.value : null,
6657
+ percentage: current.ok && previous.ok ? calculatePercentage(current.value, previous.value) : null
6658
+ };
6659
+ }
6660
+ function activePeriod(current, previous, calculatePercentage) {
6661
+ return {
6662
+ active: current.ok ? current.value : null,
6663
+ percentage: current.ok && previous.ok ? calculatePercentage(current.value, previous.value) : null
6664
+ };
6665
+ }
6456
6666
  const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6457
6667
  method: "GET",
6458
6668
  use: [jwtMiddleware(options)]
@@ -6466,16 +6676,16 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6466
6676
  const twoMonthsAgo = /* @__PURE__ */ new Date(now.getTime() - 1440 * 60 * 60 * 1e3);
6467
6677
  const activityTrackingEnabled = !!options.activityTracking?.enabled;
6468
6678
  const storeInSecondaryStorageOnly = isSessionInSecondaryStorageOnly(ctx.context);
6469
- const [dailyCount, previousDailyCount, weeklyCount, previousWeeklyCount, monthlyCount, previousMonthlyCount, totalCount, dailyActiveCount, previousDailyActiveCount, weeklyActiveCount, previousWeeklyActiveCount, monthlyActiveCount, previousMonthlyActiveCount] = await Promise.all([
6470
- ctx.context.adapter.count({
6679
+ const [rDailySignups, rPrevDaySignups, rWeeklySignups, rPrevWeekSignups, rMonthlySignups, rPrevMonthSignups, rTotalUsers, rActiveDaily, rActivePrevDay, rActiveWeekly, rActivePrevWeek, rActiveMonthly, rActivePrevMonth] = await withConcurrency([
6680
+ optionalQuery(ctx, "daily signups", () => ctx.context.adapter.count({
6471
6681
  model: "user",
6472
6682
  where: [{
6473
6683
  field: "createdAt",
6474
6684
  operator: "gte",
6475
6685
  value: oneDayAgo
6476
6686
  }]
6477
- }),
6478
- ctx.context.adapter.count({
6687
+ })),
6688
+ optionalQuery(ctx, "previous day signups", () => ctx.context.adapter.count({
6479
6689
  model: "user",
6480
6690
  where: [{
6481
6691
  field: "createdAt",
@@ -6486,16 +6696,16 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6486
6696
  operator: "lt",
6487
6697
  value: oneDayAgo
6488
6698
  }]
6489
- }),
6490
- ctx.context.adapter.count({
6699
+ })),
6700
+ optionalQuery(ctx, "weekly signups", () => ctx.context.adapter.count({
6491
6701
  model: "user",
6492
6702
  where: [{
6493
6703
  field: "createdAt",
6494
6704
  operator: "gte",
6495
6705
  value: oneWeekAgo
6496
6706
  }]
6497
- }),
6498
- ctx.context.adapter.count({
6707
+ })),
6708
+ optionalQuery(ctx, "previous week signups", () => ctx.context.adapter.count({
6499
6709
  model: "user",
6500
6710
  where: [{
6501
6711
  field: "createdAt",
@@ -6506,16 +6716,16 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6506
6716
  operator: "lt",
6507
6717
  value: oneWeekAgo
6508
6718
  }]
6509
- }),
6510
- ctx.context.adapter.count({
6719
+ })),
6720
+ optionalQuery(ctx, "monthly signups", () => ctx.context.adapter.count({
6511
6721
  model: "user",
6512
6722
  where: [{
6513
6723
  field: "createdAt",
6514
6724
  operator: "gte",
6515
6725
  value: oneMonthAgo
6516
6726
  }]
6517
- }),
6518
- ctx.context.adapter.count({
6727
+ })),
6728
+ optionalQuery(ctx, "previous month signups", () => ctx.context.adapter.count({
6519
6729
  model: "user",
6520
6730
  where: [{
6521
6731
  field: "createdAt",
@@ -6526,75 +6736,74 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
6526
6736
  operator: "lt",
6527
6737
  value: oneMonthAgo
6528
6738
  }]
6529
- }),
6530
- ctx.context.adapter.count({ model: "user" }),
6531
- countUniqueActiveUsers(ctx, { from: oneDayAgo }, {
6739
+ })),
6740
+ optionalQuery(ctx, "total users", () => ctx.context.adapter.count({ model: "user" })),
6741
+ optionalQuery(ctx, "active users (daily window)", () => countUniqueActiveUsers(ctx, { from: oneDayAgo }, {
6532
6742
  storeInSecondaryStorageOnly,
6533
6743
  activityTrackingEnabled
6534
- }),
6535
- countUniqueActiveUsers(ctx, {
6744
+ })),
6745
+ optionalQuery(ctx, "active users (previous day window)", () => countUniqueActiveUsers(ctx, {
6536
6746
  from: twoDaysAgo,
6537
6747
  to: oneDayAgo
6538
6748
  }, {
6539
6749
  storeInSecondaryStorageOnly,
6540
6750
  activityTrackingEnabled
6541
- }),
6542
- countUniqueActiveUsers(ctx, { from: oneWeekAgo }, {
6751
+ })),
6752
+ optionalQuery(ctx, "active users (weekly window)", () => countUniqueActiveUsers(ctx, { from: oneWeekAgo }, {
6543
6753
  storeInSecondaryStorageOnly,
6544
6754
  activityTrackingEnabled
6545
- }),
6546
- countUniqueActiveUsers(ctx, {
6755
+ })),
6756
+ optionalQuery(ctx, "active users (previous week window)", () => countUniqueActiveUsers(ctx, {
6547
6757
  from: twoWeeksAgo,
6548
6758
  to: oneWeekAgo
6549
6759
  }, {
6550
6760
  storeInSecondaryStorageOnly,
6551
6761
  activityTrackingEnabled
6552
- }),
6553
- countUniqueActiveUsers(ctx, { from: oneMonthAgo }, {
6762
+ })),
6763
+ optionalQuery(ctx, "active users (monthly window)", () => countUniqueActiveUsers(ctx, { from: oneMonthAgo }, {
6554
6764
  storeInSecondaryStorageOnly,
6555
6765
  activityTrackingEnabled
6556
- }),
6557
- countUniqueActiveUsers(ctx, {
6766
+ })),
6767
+ optionalQuery(ctx, "active users (previous month window)", () => countUniqueActiveUsers(ctx, {
6558
6768
  from: twoMonthsAgo,
6559
6769
  to: oneMonthAgo
6560
6770
  }, {
6561
6771
  storeInSecondaryStorageOnly,
6562
6772
  activityTrackingEnabled
6563
- })
6564
- ]);
6773
+ }))
6774
+ ], (fn) => fn(), { concurrency: 5 });
6565
6775
  const calculatePercentage = (current, previous) => {
6566
6776
  if (previous === 0) return current > 0 ? 100 : 0;
6567
6777
  return (current - previous) / previous * 100;
6568
6778
  };
6569
- return {
6570
- daily: {
6571
- signUps: dailyCount,
6572
- percentage: calculatePercentage(dailyCount, previousDailyCount)
6573
- },
6574
- weekly: {
6575
- signUps: weeklyCount,
6576
- percentage: calculatePercentage(weeklyCount, previousWeeklyCount)
6577
- },
6578
- monthly: {
6579
- signUps: monthlyCount,
6580
- percentage: calculatePercentage(monthlyCount, previousMonthlyCount)
6581
- },
6582
- total: totalCount,
6779
+ const degraded = [
6780
+ rDailySignups,
6781
+ rPrevDaySignups,
6782
+ rWeeklySignups,
6783
+ rPrevWeekSignups,
6784
+ rMonthlySignups,
6785
+ rPrevMonthSignups,
6786
+ rTotalUsers,
6787
+ rActiveDaily,
6788
+ rActivePrevDay,
6789
+ rActiveWeekly,
6790
+ rActivePrevWeek,
6791
+ rActiveMonthly,
6792
+ rActivePrevMonth
6793
+ ].some((r) => !r.ok);
6794
+ const body = {
6795
+ daily: signUpPeriod(rDailySignups, rPrevDaySignups, calculatePercentage),
6796
+ weekly: signUpPeriod(rWeeklySignups, rPrevWeekSignups, calculatePercentage),
6797
+ monthly: signUpPeriod(rMonthlySignups, rPrevMonthSignups, calculatePercentage),
6798
+ total: rTotalUsers.ok ? rTotalUsers.value : null,
6583
6799
  activeUsers: {
6584
- daily: {
6585
- active: dailyActiveCount,
6586
- percentage: calculatePercentage(dailyActiveCount, previousDailyActiveCount)
6587
- },
6588
- weekly: {
6589
- active: weeklyActiveCount,
6590
- percentage: calculatePercentage(weeklyActiveCount, previousWeeklyActiveCount)
6591
- },
6592
- monthly: {
6593
- active: monthlyActiveCount,
6594
- percentage: calculatePercentage(monthlyActiveCount, previousMonthlyActiveCount)
6595
- }
6800
+ daily: activePeriod(rActiveDaily, rActivePrevDay, calculatePercentage),
6801
+ weekly: activePeriod(rActiveWeekly, rActivePrevWeek, calculatePercentage),
6802
+ monthly: activePeriod(rActiveMonthly, rActivePrevMonth, calculatePercentage)
6596
6803
  }
6597
6804
  };
6805
+ if (degraded) body.degraded = true;
6806
+ return body;
6598
6807
  });
6599
6808
  const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data", {
6600
6809
  method: "GET",
@@ -7188,6 +7397,7 @@ const dash = (options) => {
7188
7397
  return {
7189
7398
  id: "dash",
7190
7399
  options: opts,
7400
+ version: PLUGIN_VERSION,
7191
7401
  init(ctx) {
7192
7402
  const organizationPlugin = ctx.getPlugin("organization");
7193
7403
  if (organizationPlugin) {
@@ -0,0 +1,18 @@
1
+ import { a as dashClient, i as DashGetAuditLogsInput, n as DashAuditLogsResponse, r as DashClientOptions, t as DashAuditLog } from "./dash-client-hJHp7l_X.mjs";
2
+ import { BetterAuthClientPlugin } from "better-auth";
3
+
4
+ //#region src/sentinel/native/client.d.ts
5
+ interface SentinelNativeClientOptions {
6
+ identifyUrl?: string;
7
+ autoSolveChallenge?: boolean;
8
+ onChallengeReceived?: (reason: string) => void;
9
+ onChallengeSolved?: (solveTimeMs: number) => void;
10
+ onChallengeFailed?: (error: Error) => void;
11
+ storage?: {
12
+ getItem: (key: string) => Promise<string | null>;
13
+ setItem: (key: string, value: string) => Promise<void>;
14
+ };
15
+ }
16
+ declare const sentinelNativeClient: (options?: SentinelNativeClientOptions) => BetterAuthClientPlugin;
17
+ //#endregion
18
+ export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAuditLogsInput, type SentinelNativeClientOptions, dashClient, sentinelNativeClient };