@better-auth/infra 0.2.9 → 0.2.10

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/client.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./constants-CvriWQVc.mjs";
2
2
  import { n as createKV } from "./fetch-DiAhoiKA.mjs";
3
- import { a as generateRequestId, c as hash, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as identify, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-BTL4g3RP.mjs";
3
+ import { a as resolveSentinelClientIdentifyUrl, i as solvePoWChallenge, l as hash, n as decodePoWChallenge, o as generateRequestId, r as encodePoWSolution, s as identify, t as createPowRetryTimeout, u as dashClient } from "./pow-retry-D2RRftJr.mjs";
4
4
  import { env } from "@better-auth/core/env";
5
5
  //#region src/sentinel/fingerprint.ts
6
6
  function murmurhash3(str, seed = 0) {
@@ -422,11 +422,13 @@ async function waitForIdentify(timeoutMs) {
422
422
  }
423
423
  //#endregion
424
424
  //#region src/sentinel/client.ts
425
- const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
426
425
  const sentinelClient = (options) => {
427
426
  const autoSolve = options?.autoSolveChallenge !== false;
428
427
  const $kv = createKV({
429
- kvUrl: options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL,
428
+ kvUrl: resolveSentinelClientIdentifyUrl({
429
+ identifyUrl: options?.identifyUrl,
430
+ envKvUrl: env.BETTER_AUTH_KV_URL
431
+ }),
430
432
  kvTimeout: options?.kvTimeout
431
433
  });
432
434
  if (typeof window !== "undefined") {
package/dist/index.d.mts CHANGED
@@ -770,7 +770,7 @@ interface DashIdRow {
770
770
  id: string;
771
771
  }
772
772
  //#endregion
773
- //#region ../../node_modules/.bun/@better-auth+scim@1.6.9+dd812cddb6773b08/node_modules/@better-auth/scim/dist/index.d.mts
773
+ //#region ../../node_modules/.bun/@better-auth+scim@1.6.11+89e630460ed02574/node_modules/@better-auth/scim/dist/index.d.mts
774
774
  //#region src/types.d.ts
775
775
  interface SCIMProvider {
776
776
  id: string;
package/dist/index.mjs CHANGED
@@ -2794,7 +2794,7 @@ const jwtValidateMiddleware = (options) => {
2794
2794
  };
2795
2795
  //#endregion
2796
2796
  //#region src/version.ts
2797
- const PLUGIN_VERSION = "0.2.9";
2797
+ const PLUGIN_VERSION = "0.2.10";
2798
2798
  //#endregion
2799
2799
  //#region src/routes/auth/config.ts
2800
2800
  const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
@@ -3825,6 +3825,23 @@ const checkUserExists = (options) => {
3825
3825
  });
3826
3826
  };
3827
3827
  //#endregion
3828
+ //#region src/routes/organizations/roles.ts
3829
+ const DEFAULT_ORG_MEMBER_ROLES = [
3830
+ "member",
3831
+ "admin",
3832
+ "owner"
3833
+ ];
3834
+ function getOrganizationRoleKeys(orgOptions) {
3835
+ const roles = orgOptions?.roles;
3836
+ if (roles && typeof roles === "object" && Object.keys(roles).length > 0) return Object.keys(roles);
3837
+ return [...DEFAULT_ORG_MEMBER_ROLES];
3838
+ }
3839
+ const organizationMemberRoleInputSchema = z$1.string().trim().min(1, "Role is required").max(64, "Role is too long").regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, "Role must start with a letter and contain only letters, numbers, hyphens, and underscores");
3840
+ function validateOrganizationMemberRole(ctx, role, orgOptions) {
3841
+ const allowedRoles = getOrganizationRoleKeys(orgOptions);
3842
+ if (!allowedRoles.includes(role)) throw ctx.error("BAD_REQUEST", { message: `Invalid role. Allowed roles: ${allowedRoles.join(", ")}` });
3843
+ }
3844
+ //#endregion
3828
3845
  //#region src/routes/organizations/invitations.ts
3829
3846
  const listOrganizationInvitations = (options) => {
3830
3847
  return createAuthEndpoint("/dash/organization/:id/invitations", {
@@ -3873,7 +3890,7 @@ const inviteMember = (options) => {
3873
3890
  method: "POST",
3874
3891
  body: z$1.object({
3875
3892
  email: z$1.string(),
3876
- role: z$1.string(),
3893
+ role: organizationMemberRoleInputSchema,
3877
3894
  invitedBy: z$1.string()
3878
3895
  }),
3879
3896
  use: [jwtMiddleware(options, z$1.object({
@@ -3883,6 +3900,7 @@ const inviteMember = (options) => {
3883
3900
  }, async (ctx) => {
3884
3901
  const { organizationId } = ctx.context.payload;
3885
3902
  const organizationPlugin = requireOrganizationPlugin(ctx);
3903
+ validateOrganizationMemberRole(ctx, ctx.body.role, organizationPlugin.options);
3886
3904
  if (!organizationPlugin.options?.sendInvitationEmail) throw ctx.error("BAD_REQUEST", { message: "Invitation email is not enabled" });
3887
3905
  const invitedBy = await ctx.context.adapter.findOne({
3888
3906
  model: "user",
@@ -4132,11 +4150,12 @@ const addMember = (options) => {
4132
4150
  use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
4133
4151
  body: z$1.object({
4134
4152
  userId: z$1.string(),
4135
- role: z$1.string()
4153
+ role: organizationMemberRoleInputSchema
4136
4154
  })
4137
4155
  }, async (ctx) => {
4138
4156
  const { organizationId } = ctx.context.payload;
4139
4157
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4158
+ validateOrganizationMemberRole(ctx, ctx.body.role, orgOptions);
4140
4159
  const organization = await ctx.context.adapter.findOne({
4141
4160
  model: "organization",
4142
4161
  where: [{
@@ -4265,11 +4284,12 @@ const updateMemberRole = (options) => {
4265
4284
  use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
4266
4285
  body: z$1.object({
4267
4286
  memberId: z$1.string(),
4268
- role: z$1.string()
4287
+ role: organizationMemberRoleInputSchema
4269
4288
  })
4270
4289
  }, async (ctx) => {
4271
4290
  const { organizationId } = ctx.context.payload;
4272
4291
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4292
+ validateOrganizationMemberRole(ctx, ctx.body.role, orgOptions);
4273
4293
  const existingMember = await ctx.context.adapter.findOne({
4274
4294
  model: "member",
4275
4295
  where: [{
@@ -5658,13 +5678,13 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
5658
5678
  });
5659
5679
  });
5660
5680
  //#endregion
5661
- //#region src/validation/ssrf.ts
5681
+ //#region ../utils/dist/ip.mjs
5662
5682
  /**
5663
5683
  * SSRF (Server-Side Request Forgery) Protection
5664
5684
  *
5665
- * Validates URLs before server-side fetches to block requests to private/reserved
5666
- * networks. Covers IPv4 private ranges, IPv6 private ranges, and IPv4-mapped IPv6
5667
- * bypass vectors.
5685
+ * IP and URL utilities to validate hostnames and URLs before server-side fetches.
5686
+ * Blocks requests to private/reserved networks. Covers IPv4 private ranges,
5687
+ * IPv6 private ranges, and IPv4-mapped IPv6 bypass vectors.
5668
5688
  */
5669
5689
  function isPrivateIPv4(a, b) {
5670
5690
  if (a === 10) return true;
@@ -5673,6 +5693,24 @@ function isPrivateIPv4(a, b) {
5673
5693
  if (a === 127) return true;
5674
5694
  if (a === 169 && b === 254) return true;
5675
5695
  if (a === 0) return true;
5696
+ if (a === 100 && b >= 64 && b <= 127) return true;
5697
+ return false;
5698
+ }
5699
+ /**
5700
+ * Loopback, RFC1918, link-local, CGNAT, ULA, documentation, NAT64 well-known,
5701
+ * IPv4-mapped private, unspecified, and multicast (RFC 4291 section 2.7).
5702
+ */
5703
+ function ipv6GroupsAreNonPublic(groups) {
5704
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) return true;
5705
+ if (groups.every((g) => g === 0)) return true;
5706
+ if ((groups[0] & 65472) === 65152) return true;
5707
+ if ((groups[0] & 65024) === 64512) return true;
5708
+ if ((groups[0] & 65280) === 65280) return true;
5709
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
5710
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 65535 && groups[5] === 0) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
5711
+ if (groups[0] === 100 && groups[1] === 65435 && groups.slice(2, 6).every((g) => g === 0)) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
5712
+ if (groups[0] === 8193 && groups[1] === 3512) return true;
5713
+ if (groups[0] === 256 && groups.slice(1, 4).every((g) => g === 0)) return true;
5676
5714
  return false;
5677
5715
  }
5678
5716
  function parseIPv6(addr) {
@@ -5694,36 +5732,174 @@ function parseIPv6(addr) {
5694
5732
  if (left.length !== 8) return null;
5695
5733
  return left;
5696
5734
  }
5735
+ /** Returns true if the hostname resolves to a private/reserved IP or is a local hostname. */
5697
5736
  function isPrivateHost(hostname) {
5698
5737
  if (hostname === "localhost" || hostname.endsWith(".local") || hostname.endsWith(".internal")) return true;
5699
5738
  const bare = hostname.replace(/^\[|\]$/g, "");
5700
5739
  const v4parts = bare.split(".").map(Number);
5701
5740
  if (v4parts.length === 4 && v4parts.every((p) => !isNaN(p) && p >= 0 && p <= 255)) return isPrivateIPv4(v4parts[0], v4parts[1]);
5702
5741
  const groups = parseIPv6(bare);
5703
- if (groups && groups.length === 8) {
5704
- if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) return true;
5705
- if (groups.every((g) => g === 0)) return true;
5706
- if ((groups[0] & 65472) === 65152) return true;
5707
- if ((groups[0] & 65024) === 64512) return true;
5708
- if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
5709
- if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 65535 && groups[5] === 0) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
5710
- if (groups[0] === 100 && groups[1] === 65435 && groups.slice(2, 6).every((g) => g === 0)) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
5711
- if (groups[0] === 8193 && groups[1] === 3512) return true;
5712
- if (groups[0] === 256 && groups.slice(1, 4).every((g) => g === 0)) return true;
5713
- }
5742
+ if (groups && groups.length === 8) return ipv6GroupsAreNonPublic(groups);
5714
5743
  return false;
5715
5744
  }
5716
- function parseAndValidateUrl(url) {
5745
+ //#endregion
5746
+ //#region ../utils/dist/url.mjs
5747
+ /**
5748
+ * URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
5749
+ */
5750
+ const HTTP_PROTOCOLS = ["http:", "https:"];
5751
+ function isAllowedProtocol(protocol, allowedProtocols) {
5752
+ return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
5753
+ }
5754
+ function toAllowedOriginSet(allowedOrigins) {
5755
+ const set = /* @__PURE__ */ new Set();
5756
+ for (const raw of allowedOrigins) try {
5757
+ const parsed = new URL(raw.trim());
5758
+ if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
5759
+ } catch {}
5760
+ return set;
5761
+ }
5762
+ function isAllowedOrigin(origin, allowedOrigins) {
5763
+ if (!allowedOrigins.length) return false;
5764
+ return toAllowedOriginSet(allowedOrigins).has(origin);
5765
+ }
5766
+ async function resolveHostnameAddresses(hostname) {
5767
+ try {
5768
+ const dns = await import("node:dns").catch((e) => {
5769
+ console.warn("Failed to load node:dns for DNS resolution", e);
5770
+ return null;
5771
+ });
5772
+ if (!dns?.promises?.resolve) return null;
5773
+ return await dns.promises.resolve(hostname);
5774
+ } catch {
5775
+ return null;
5776
+ }
5777
+ }
5778
+ function validateUrlSync(rawUrl, options = {}) {
5779
+ const trimmed = rawUrl.trim();
5780
+ let parsed;
5781
+ try {
5782
+ parsed = new URL(trimmed);
5783
+ } catch {
5784
+ return {
5785
+ ok: false,
5786
+ reason: "invalid_url",
5787
+ url: trimmed
5788
+ };
5789
+ }
5790
+ if (!isAllowedProtocol(parsed.protocol, options.allowedProtocols)) return {
5791
+ ok: false,
5792
+ reason: "disallowed_protocol",
5793
+ url: trimmed,
5794
+ protocol: parsed.protocol
5795
+ };
5796
+ if (parsed.username !== "" || parsed.password !== "") return {
5797
+ ok: false,
5798
+ reason: "embedded_credentials",
5799
+ url: trimmed
5800
+ };
5801
+ if (isPrivateHost(parsed.hostname)) return {
5802
+ ok: false,
5803
+ reason: "private_host",
5804
+ url: trimmed
5805
+ };
5806
+ if (options.allowedOrigins?.length) {
5807
+ if (!isAllowedOrigin(parsed.origin, options.allowedOrigins)) return {
5808
+ ok: false,
5809
+ reason: "disallowed_origin",
5810
+ url: trimmed,
5811
+ hostname: parsed.hostname,
5812
+ origin: parsed.origin
5813
+ };
5814
+ }
5815
+ return {
5816
+ ok: true,
5817
+ url: parsed
5818
+ };
5819
+ }
5820
+ /** SSRF checks with DNS resolution when available. */
5821
+ async function validateUrl(rawUrl, options = {}) {
5822
+ const sync = validateUrlSync(rawUrl, options);
5823
+ if (!sync.ok) return sync;
5824
+ if (options.dns === false) return sync;
5825
+ const addresses = await resolveHostnameAddresses(sync.url.hostname);
5826
+ if (addresses) {
5827
+ for (const addr of addresses) if (isPrivateHost(addr)) return {
5828
+ ok: false,
5829
+ reason: "private_dns",
5830
+ url: sync.url.href,
5831
+ hostname: sync.url.hostname,
5832
+ address: addr
5833
+ };
5834
+ }
5835
+ return sync;
5836
+ }
5837
+ function safeResolveUrl(raw, baseURL) {
5838
+ const trim = raw.trim();
5839
+ if (!trim) return null;
5840
+ if (trim.startsWith("//") || trim.includes("\\")) return null;
5841
+ if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
5717
5842
  try {
5718
- const parsed = new URL(url);
5719
- if (!["http:", "https:"].includes(parsed.protocol)) return null;
5720
- if (isPrivateHost(parsed.hostname)) return null;
5721
- return parsed;
5843
+ if (trim.startsWith("/")) return new URL(trim, baseURL);
5844
+ return new URL(trim);
5722
5845
  } catch {
5723
5846
  return null;
5724
5847
  }
5725
5848
  }
5726
5849
  //#endregion
5850
+ //#region src/validation/ssrf.ts
5851
+ /**
5852
+ * SSRF protection for dash plugin outbound fetches.
5853
+ *
5854
+ * Uses @infra/utils at build time; tsdown bundles it into published dist so
5855
+ * consumers do not install the private workspace package.
5856
+ */
5857
+ const REDIRECT_STATUSES = new Set([
5858
+ 301,
5859
+ 302,
5860
+ 303,
5861
+ 307,
5862
+ 308
5863
+ ]);
5864
+ const MAX_REDIRECTS = 10;
5865
+ var SsrfBlockedError = class extends Error {
5866
+ name = "SsrfBlockedError";
5867
+ };
5868
+ function isSsrfBlockedError(error) {
5869
+ return error instanceof SsrfBlockedError;
5870
+ }
5871
+ /** Validates URL hostname literals and resolved DNS addresses. */
5872
+ async function parseAndValidateUrl(url) {
5873
+ const result = await validateUrl(url);
5874
+ return result.ok ? result.url : null;
5875
+ }
5876
+ /**
5877
+ * Fetch with SSRF checks on the initial URL and every redirect target.
5878
+ */
5879
+ async function $outbound(url, options = {}) {
5880
+ const { timeout, signal: callerSignal, ...fetchInit } = options;
5881
+ let currentUrl = url;
5882
+ for (let hop = 0;; hop++) {
5883
+ const validated = await parseAndValidateUrl(currentUrl);
5884
+ if (!validated) throw new SsrfBlockedError("Invalid or blocked URL");
5885
+ const signals = [];
5886
+ if (callerSignal) signals.push(callerSignal);
5887
+ if (timeout !== void 0 && timeout > 0) signals.push(AbortSignal.timeout(timeout));
5888
+ const response = await fetch(validated.href, {
5889
+ ...fetchInit,
5890
+ redirect: "manual",
5891
+ signal: signals.length === 0 ? void 0 : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
5892
+ });
5893
+ if (!REDIRECT_STATUSES.has(response.status)) return response;
5894
+ if (hop >= MAX_REDIRECTS) throw new SsrfBlockedError("Too many redirects");
5895
+ await response.body?.cancel().catch(() => {});
5896
+ const location = response.headers.get("location");
5897
+ const next = location ? safeResolveUrl(location, validated.href) : null;
5898
+ if (!next) throw new SsrfBlockedError("Invalid redirect location");
5899
+ currentUrl = next.href;
5900
+ }
5901
+ }
5902
+ //#endregion
5727
5903
  //#region src/routes/plugin-session.ts
5728
5904
  /**
5729
5905
  * Builds an in-memory session context for calling plugin endpoints
@@ -5836,13 +6012,19 @@ const oidcConfigSchema = z$1.object({
5836
6012
  async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
5837
6013
  let idpMetadataXml = samlConfig.idpMetadata?.metadata;
5838
6014
  if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) {
5839
- const validatedMetadataUrl = parseAndValidateUrl(samlConfig.idpMetadata.metadataUrl);
6015
+ const validatedMetadataUrl = await parseAndValidateUrl(samlConfig.idpMetadata.metadataUrl);
5840
6016
  if (!validatedMetadataUrl) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
5841
6017
  try {
5842
- const metadataResponse = await fetch(validatedMetadataUrl.toString());
6018
+ const metadataResponse = await $outbound(validatedMetadataUrl.href, {
6019
+ method: "GET",
6020
+ headers: { Accept: "application/xml, text/xml" },
6021
+ timeout: 15e3
6022
+ });
5843
6023
  if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
5844
6024
  idpMetadataXml = await metadataResponse.text();
5845
6025
  } catch (e) {
6026
+ if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
6027
+ if (e instanceof APIError$1) throw e;
5846
6028
  ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
5847
6029
  throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
5848
6030
  }
package/dist/native.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./constants-CvriWQVc.mjs";
2
2
  import { n as createKV } from "./fetch-DiAhoiKA.mjs";
3
- import { i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as identify, r as encodePoWSolution, s as bytesToHex, t as createPowRetryTimeout } from "./pow-retry-BTL4g3RP.mjs";
3
+ import { a as resolveSentinelClientIdentifyUrl, c as bytesToHex, i as solvePoWChallenge, n as decodePoWChallenge, r as encodePoWSolution, s as identify, t as createPowRetryTimeout, u as dashClient } from "./pow-retry-D2RRftJr.mjs";
4
4
  import { env } from "@better-auth/core/env";
5
5
  import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
6
6
  //#region src/sentinel/native/components.ts
@@ -190,7 +190,6 @@ async function getOrCreateVisitorId(storage) {
190
190
  }
191
191
  //#endregion
192
192
  //#region src/sentinel/native/client.ts
193
- const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
194
193
  function scheduleIdentify(send) {
195
194
  const run = () => {
196
195
  try {
@@ -213,7 +212,10 @@ const sentinelNativeClient = (options) => {
213
212
  const autoSolve = options?.autoSolveChallenge !== false;
214
213
  const runtime = createNativeFingerprintRuntime({
215
214
  $kv: createKV({
216
- kvUrl: options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL,
215
+ kvUrl: resolveSentinelClientIdentifyUrl({
216
+ identifyUrl: options?.identifyUrl,
217
+ envKvUrl: env.BETTER_AUTH_KV_URL
218
+ }),
217
219
  kvTimeout: options?.kvTimeout
218
220
  }),
219
221
  getOrCreateVisitorId: () => getOrCreateVisitorId(options?.storage)
@@ -72,6 +72,39 @@ async function identify($kv, payload) {
72
72
  });
73
73
  }
74
74
  //#endregion
75
+ //#region src/sentinel/identify-url.ts
76
+ const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
77
+ function normalizeIdentifyUrl(url) {
78
+ return url.replace(/\/$/, "");
79
+ }
80
+ /** True when the URL targets project-scoped ingestion, e.g. .../projects/{orgId}. */
81
+ function isProjectScopedIdentifyUrl(url) {
82
+ return /\/projects\/[^/]+$/.test(normalizeIdentifyUrl(url));
83
+ }
84
+ const DEFAULT_GLOBAL_INGESTION_WARNING = "[Sentinel] Default global identify ingestion is active but not recommended. Get your ingestion url from your project settings page (e.g. https://kv.better-auth.com/projects/{id}) and configure sentinelClient({ identifyUrl }) with it.";
85
+ const CONFIGURED_GLOBAL_INGESTION_WARNING = "[Sentinel] Global identify ingestion is configured but not recommended. Get your ingestion url from your project settings page (e.g. https://kv.better-auth.com/projects/{id}) and configure sentinelClient({ identifyUrl }) with it.";
86
+ function warnOnGlobalIdentifyIngestion(url, message) {
87
+ if (isProjectScopedIdentifyUrl(url)) return;
88
+ console.warn(message);
89
+ }
90
+ function resolveSentinelClientIdentifyUrl(options) {
91
+ const explicit = options?.identifyUrl?.trim();
92
+ if (explicit) {
93
+ const url = normalizeIdentifyUrl(explicit);
94
+ warnOnGlobalIdentifyIngestion(url, CONFIGURED_GLOBAL_INGESTION_WARNING);
95
+ return url;
96
+ }
97
+ const fromEnv = options?.envKvUrl?.trim();
98
+ if (fromEnv) {
99
+ const url = normalizeIdentifyUrl(fromEnv);
100
+ warnOnGlobalIdentifyIngestion(url, CONFIGURED_GLOBAL_INGESTION_WARNING);
101
+ return url;
102
+ }
103
+ const url = DEFAULT_IDENTIFY_URL;
104
+ warnOnGlobalIdentifyIngestion(url, DEFAULT_GLOBAL_INGESTION_WARNING);
105
+ return url;
106
+ }
107
+ //#endregion
75
108
  //#region src/sentinel/pow.ts
76
109
  function hasLeadingZeroBits(hash, bits) {
77
110
  const fullHexChars = Math.floor(bits / 4);
@@ -148,4 +181,4 @@ function createPowRetryTimeout(timeoutMs) {
148
181
  };
149
182
  }
150
183
  //#endregion
151
- export { generateRequestId as a, hash as c, solvePoWChallenge as i, dashClient as l, decodePoWChallenge as n, identify as o, encodePoWSolution as r, bytesToHex as s, createPowRetryTimeout as t };
184
+ export { resolveSentinelClientIdentifyUrl as a, bytesToHex as c, solvePoWChallenge as i, hash as l, decodePoWChallenge as n, generateRequestId as o, encodePoWSolution as r, identify as s, createPowRetryTimeout as t, dashClient as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/infra",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Dashboard and analytics plugin for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -61,24 +61,25 @@
61
61
  },
62
62
  "homepage": "https://better-auth.com",
63
63
  "devDependencies": {
64
- "@better-auth/scim": "catalog:",
65
- "@better-auth/sso": "catalog:",
66
- "@infra/mocks": "workspace:*",
67
- "@types/bun": "catalog:",
68
- "@types/node": "catalog:",
69
- "better-auth": "catalog:",
64
+ "@better-auth/scim": "1.6.11",
65
+ "@better-auth/sso": "1.6.11",
66
+ "@infra/mocks": "0.0.0",
67
+ "@infra/utils": "0.0.0",
68
+ "@types/bun": "latest",
69
+ "@types/node": "^24.12.0",
70
+ "better-auth": "1.6.11",
70
71
  "expo-crypto": "^14.0.2",
71
72
  "happy-dom": "^20.9.0",
72
- "msw": "catalog:",
73
+ "msw": "^2.14.6",
73
74
  "tsdown": "^0.22.0",
74
- "typescript": "catalog:",
75
- "zod": "catalog:"
75
+ "typescript": "^5.9.2",
76
+ "zod": "^4.3.6"
76
77
  },
77
78
  "dependencies": {
78
- "@better-fetch/fetch": "catalog:",
79
- "better-call": "catalog:",
79
+ "@better-fetch/fetch": "^1.1.21",
80
+ "better-call": "^1.3.2",
80
81
  "jose": "^6.1.0",
81
- "libphonenumber-js": "^1.13.1"
82
+ "libphonenumber-js": "^1.13.2"
82
83
  },
83
84
  "peerDependencies": {
84
85
  "better-auth": ">=1.4.0",