@better-auth/infra 0.1.14 → 0.2.0

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.0";
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,
@@ -7188,6 +7370,7 @@ const dash = (options) => {
7188
7370
  return {
7189
7371
  id: "dash",
7190
7372
  options: opts,
7373
+ version: PLUGIN_VERSION,
7191
7374
  init(ctx) {
7192
7375
  const organizationPlugin = ctx.getPlugin("organization");
7193
7376
  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 };
@@ -0,0 +1,292 @@
1
+ import { a as identify, c as dashClient, n as encodePoWSolution, o as bytesToHex, r as solvePoWChallenge, t as decodePoWChallenge } from "./pow-BUuN_EKw.mjs";
2
+ import { env } from "@better-auth/core/env";
3
+ import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
4
+ //#region src/sentinel/native/components.ts
5
+ /**
6
+ * Default first-party component payload for React Native / Expo.
7
+ * Avoids browser-style entropy; sets `clientRuntime` for durable-kv bot scoring.
8
+ */
9
+ async function defaultCollectNativeComponents() {
10
+ const windowDims = Dimensions.get("window");
11
+ const scale = PixelRatio.get();
12
+ let locale = "en";
13
+ let locales = ["en"];
14
+ try {
15
+ locale = Intl.DateTimeFormat().resolvedOptions().locale || locale;
16
+ locales = [locale];
17
+ } catch {}
18
+ const nav = globalThis.navigator;
19
+ if (nav?.languages?.length) locales = [...nav.languages];
20
+ let timezone = "";
21
+ let timezoneOffset = 0;
22
+ try {
23
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
24
+ timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
25
+ } catch {
26
+ timezone = "unknown";
27
+ }
28
+ const components = {
29
+ clientRuntime: "react-native",
30
+ platform: Platform.OS,
31
+ screenResolution: `${Math.round(windowDims.width)}x${Math.round(windowDims.height)}`,
32
+ pixelRatio: scale,
33
+ fontScale: windowDims.fontScale,
34
+ language: locales[0] ?? locale,
35
+ languages: locales,
36
+ locale,
37
+ timezone,
38
+ timezoneOffset,
39
+ touchSupport: true,
40
+ maxTouchPoints: Platform.OS === "ios" ? 5 : 10
41
+ };
42
+ if (nav?.userAgent) components.userAgent = nav.userAgent;
43
+ await applyOptionalExpoMetadata(components);
44
+ return components;
45
+ }
46
+ async function applyOptionalExpoMetadata(components) {
47
+ try {
48
+ const c = (await import("expo-constants")).default;
49
+ if (c?.expoConfig?.version) components.appVersion = c.expoConfig.version;
50
+ else if (c?.nativeAppVersion) components.appVersion = c.nativeAppVersion;
51
+ if (c?.expoConfig?.slug) components.expoSlug = c.expoConfig.slug;
52
+ } catch {}
53
+ try {
54
+ const d = (await import("expo-device")).default;
55
+ if (d?.modelName) components.deviceModel = d.modelName;
56
+ if (d?.osVersion) components.osVersion = d.osVersion;
57
+ } catch {}
58
+ }
59
+ //#endregion
60
+ //#region src/sentinel/native/crypto.ts
61
+ /**
62
+ * Cryptographically secure random bytes for React Native, Node (e.g. Expo API routes),
63
+ * and other runtimes
64
+ */
65
+ async function randomBytesAsync(length) {
66
+ const c = globalThis.crypto;
67
+ if (c && typeof c.getRandomValues === "function") {
68
+ const bytes = new Uint8Array(length);
69
+ c.getRandomValues(bytes);
70
+ return bytes;
71
+ }
72
+ try {
73
+ const { getRandomBytesAsync } = await import("expo-crypto");
74
+ return await getRandomBytesAsync(length);
75
+ } catch {
76
+ throw new Error("[@better-auth/infra] No secure RNG available: use Web Crypto / Node, add `import \"react-native-get-random-values\"` at app entry (before other imports), or install `expo-crypto` with a native dev build.");
77
+ }
78
+ }
79
+ //#endregion
80
+ //#region src/sentinel/native/fingerprint.ts
81
+ async function randomVisitorId() {
82
+ return bytesToHex(await randomBytesAsync(10)).slice(0, 20);
83
+ }
84
+ async function generateRequestId() {
85
+ const h = bytesToHex(await randomBytesAsync(16));
86
+ return [
87
+ h.slice(0, 8),
88
+ h.slice(8, 12),
89
+ h.slice(12, 16),
90
+ h.slice(16, 20),
91
+ h.slice(20, 32)
92
+ ].join("-");
93
+ }
94
+ /** Conservative confidence for first-party native payloads (not web-grade signals). */
95
+ const NATIVE_IDENTIFY_CONFIDENCE = .52;
96
+ /** Logical URL recorded with native identify payloads. */
97
+ const NATIVE_IDENTIFY_CONTEXT_URL = "react-native://identify";
98
+ function createNativeFingerprintRuntime(deps) {
99
+ let cached = null;
100
+ let pending = null;
101
+ let identifySent = false;
102
+ let identifyComplete = null;
103
+ let identifyCompleteResolve = null;
104
+ async function getFingerprint() {
105
+ if (cached != null) return cached;
106
+ if (pending != null) return pending;
107
+ pending = (async () => {
108
+ try {
109
+ const visitorId = await deps.getOrCreateVisitorId();
110
+ const components = await defaultCollectNativeComponents();
111
+ const result = {
112
+ visitorId,
113
+ requestId: await generateRequestId(),
114
+ confidence: NATIVE_IDENTIFY_CONFIDENCE,
115
+ components
116
+ };
117
+ cached = result;
118
+ return result;
119
+ } catch (error) {
120
+ console.warn("[Sentinel native] Fingerprint generation failed:", error);
121
+ pending = null;
122
+ return null;
123
+ }
124
+ })();
125
+ return pending;
126
+ }
127
+ async function sendIdentify() {
128
+ if (identifySent) return;
129
+ const fingerprint = await getFingerprint();
130
+ if (!fingerprint) return;
131
+ identifySent = true;
132
+ identifyComplete = new Promise((resolve) => {
133
+ identifyCompleteResolve = resolve;
134
+ });
135
+ const payload = {
136
+ visitorId: fingerprint.visitorId,
137
+ requestId: fingerprint.requestId,
138
+ confidence: fingerprint.confidence,
139
+ components: fingerprint.components,
140
+ url: NATIVE_IDENTIFY_CONTEXT_URL,
141
+ incognito: false
142
+ };
143
+ try {
144
+ await identify(deps.identifyUrl, payload);
145
+ } catch (error) {
146
+ console.warn("[Sentinel native] Identify request failed:", error);
147
+ } finally {
148
+ identifyCompleteResolve?.();
149
+ identifyCompleteResolve = null;
150
+ }
151
+ }
152
+ async function waitForIdentify(timeoutMs = 500) {
153
+ if (!identifyComplete) return;
154
+ await Promise.race([identifyComplete, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
155
+ }
156
+ return {
157
+ getFingerprint,
158
+ sendIdentify,
159
+ waitForIdentify
160
+ };
161
+ }
162
+ const VISITOR_STORAGE_KEY = "better-auth.infra.sentinel.visitorId";
163
+ let memoryVisitorId = null;
164
+ /**
165
+ * Stable per-install visitor id. Prefer passing `storage` (e.g. secure storage).
166
+ * Otherwise tries `@react-native-async-storage/async-storage`, then falls back to an in-memory id (session only).
167
+ */
168
+ async function getOrCreateVisitorId(storage) {
169
+ if (storage) {
170
+ const existing = await storage.getItem(VISITOR_STORAGE_KEY);
171
+ if (existing) return existing;
172
+ const id = await randomVisitorId();
173
+ await storage.setItem(VISITOR_STORAGE_KEY, id);
174
+ return id;
175
+ }
176
+ try {
177
+ const { default: AsyncStorage } = await import("@react-native-async-storage/async-storage");
178
+ const existing = await AsyncStorage.getItem(VISITOR_STORAGE_KEY);
179
+ if (existing) return existing;
180
+ const id = await randomVisitorId();
181
+ await AsyncStorage.setItem(VISITOR_STORAGE_KEY, id);
182
+ return id;
183
+ } catch {
184
+ memoryVisitorId ??= await randomVisitorId();
185
+ return memoryVisitorId;
186
+ }
187
+ }
188
+ //#endregion
189
+ //#region src/sentinel/native/client.ts
190
+ const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
191
+ function scheduleIdentify(send) {
192
+ const run = () => {
193
+ try {
194
+ send();
195
+ } catch (e) {
196
+ console.warn("[Sentinel native] identify schedule error:", e);
197
+ }
198
+ };
199
+ try {
200
+ const im = InteractionManager;
201
+ if (im && typeof im.runAfterInteractions === "function") {
202
+ im.runAfterInteractions(run);
203
+ return;
204
+ }
205
+ } catch {}
206
+ if (typeof queueMicrotask === "function") queueMicrotask(run);
207
+ else setTimeout(run, 0);
208
+ }
209
+ const sentinelNativeClient = (options) => {
210
+ const autoSolve = options?.autoSolveChallenge !== false;
211
+ const runtime = createNativeFingerprintRuntime({
212
+ identifyUrl: options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL,
213
+ getOrCreateVisitorId: () => getOrCreateVisitorId(options?.storage)
214
+ });
215
+ scheduleIdentify(() => {
216
+ runtime.sendIdentify();
217
+ });
218
+ return {
219
+ id: "sentinel",
220
+ fetchPlugins: [{
221
+ id: "sentinel-fingerprint",
222
+ name: "sentinel-fingerprint",
223
+ hooks: { async onRequest(context) {
224
+ await runtime.waitForIdentify(500);
225
+ const fingerprint = await runtime.getFingerprint();
226
+ if (!fingerprint) return context;
227
+ const headers = context.headers || new Headers();
228
+ if (headers instanceof Headers) {
229
+ headers.set("X-Visitor-Id", fingerprint.visitorId);
230
+ headers.set("X-Request-Id", fingerprint.requestId);
231
+ }
232
+ return {
233
+ ...context,
234
+ headers
235
+ };
236
+ } }
237
+ }, {
238
+ id: "sentinel-pow-solver",
239
+ name: "sentinel-pow-solver",
240
+ hooks: {
241
+ async onResponse(context) {
242
+ if (context.response.status !== 423 || !autoSolve) return context;
243
+ const challengeHeader = context.response.headers.get("X-PoW-Challenge");
244
+ const reason = context.response.headers.get("X-PoW-Reason") || "";
245
+ if (!challengeHeader) return context;
246
+ options?.onChallengeReceived?.(reason);
247
+ const challenge = decodePoWChallenge(challengeHeader);
248
+ if (!challenge) return context;
249
+ try {
250
+ const startTime = Date.now();
251
+ const solution = await solvePoWChallenge(challenge);
252
+ const solveTime = Date.now() - startTime;
253
+ options?.onChallengeSolved?.(solveTime);
254
+ const originalUrl = context.response.url;
255
+ const fingerprint = await runtime.getFingerprint();
256
+ const retryHeaders = new Headers();
257
+ retryHeaders.set("X-PoW-Solution", encodePoWSolution(solution));
258
+ if (fingerprint) {
259
+ retryHeaders.set("X-Visitor-Id", fingerprint.visitorId);
260
+ retryHeaders.set("X-Request-Id", fingerprint.requestId);
261
+ }
262
+ retryHeaders.set("Content-Type", "application/json");
263
+ let body;
264
+ if (context.request?.body) body = context.request._originalBody;
265
+ const retryResponse = await fetch(originalUrl, {
266
+ method: context.request?.method || "POST",
267
+ headers: retryHeaders,
268
+ body
269
+ });
270
+ return {
271
+ ...context,
272
+ response: retryResponse
273
+ };
274
+ } catch (err) {
275
+ console.error("[Sentinel native] Failed to solve PoW challenge:", err);
276
+ options?.onChallengeFailed?.(err instanceof Error ? err : new Error(String(err)));
277
+ return context;
278
+ }
279
+ },
280
+ async onRequest(context) {
281
+ if (context.body) {
282
+ const ctx = context;
283
+ ctx._originalBody = context.body;
284
+ }
285
+ return context;
286
+ }
287
+ }
288
+ }]
289
+ };
290
+ };
291
+ //#endregion
292
+ export { dashClient, sentinelNativeClient };