@better-auth/infra 0.4.2 → 0.4.4

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/CHANGELOG.md CHANGED
@@ -5,8 +5,25 @@ All notable changes to `@better-auth/infra` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.4] - 2026-08-27
9
+
10
+ ### Added
11
+
12
+ - **`GET /dash/config` adapter insight** — Config insights report database adapter identity and capability flags.
13
+
14
+ ### Changed
15
+
16
+ - **Sentinel PoW vs rate limits** — Proof-of-work challenges no longer consume Better Auth rate-limit budget.
17
+
18
+ ## [0.4.3] - 2026-08-25
19
+
20
+ ### Added
21
+
22
+ - **SAML `wantAssertionsSigned`** — Dash SSO create/update accept and persist `samlConfig.wantAssertionsSigned` (default `true`). Required for directory sync SAML pairing; can be disabled only when the IdP cannot sign assertions.
23
+
8
24
  ## [0.4.2] - 2026-08-21
9
25
 
26
+
10
27
  ### Fixed
11
28
 
12
29
  - **Identification on OAuth callbacks** — Sentinel and dash run identification on GET OAuth callback routes and persist `X-Request-Id` as the `__infra-rid` cookie in an after hook, so visitor identity resolves from KV across redirect-based social/OAuth sign-in (where identify headers are absent).
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-CgijjwVT.mjs";
1
+ import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-Cim5Fztg.mjs";
2
2
  //#region src/sentinel/client.d.ts
3
3
  interface SentinelClientOptions {
4
4
  /**
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as hash, o as createKV } from "./crypto-D3yZH0Cy.mjs";
2
- import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, s as generateRequestId, t as createPowRetryTimeout } from "./pow-retry-jWGI__oh.mjs";
1
+ import { n as hash, o as createKV } from "./crypto-DZClUivd.mjs";
2
+ import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, s as generateRequestId, t as createPowRetryTimeout } from "./pow-retry-BvjK8YBw.mjs";
3
3
  import { env } from "@better-auth/core/env";
4
4
  //#region src/sentinel/fingerprint.ts
5
5
  function murmurhash3(str, seed = 0) {
@@ -1,7 +1,7 @@
1
1
  import { env } from "@better-auth/core/env";
2
2
  //#endregion
3
3
  //#region src/version.ts
4
- const PLUGIN_VERSION = "0.4.2";
4
+ const PLUGIN_VERSION = "0.4.4";
5
5
  //#endregion
6
6
  //#region src/constants.ts
7
7
  /**
@@ -1,4 +1,4 @@
1
- import { i as INFRA_USER_AGENT } from "./constants-CDhjfr8S.mjs";
1
+ import { i as INFRA_USER_AGENT } from "./constants-CARN9f1y.mjs";
2
2
  import { createFetch } from "@better-fetch/fetch";
3
3
  import { sha256 } from "@noble/hashes/sha2.js";
4
4
  //#region src/fetch.ts
package/dist/email.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-CDhjfr8S.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-CARN9f1y.mjs";
2
2
  import { logger } from "better-auth";
3
3
  import { env } from "@better-auth/core/env";
4
4
  import { createFetch } from "@better-fetch/fetch";
@@ -1,4 +1,4 @@
1
- import { m as KvRetryOptions } from "./types-Ddk4r5x9.mjs";
1
+ import { m as KvRetryOptions } from "./types-sP9ucYj1.mjs";
2
2
  import { BetterAuthClientOptions, ClientStore } from "better-auth";
3
3
  import { BetterFetch } from "@better-fetch/fetch";
4
4
  //#region src/dash-client.d.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as ImpossibleTravelResult, D as SecurityVerdict, E as SecurityOptions, O as StaleUserResult, S as CredentialStuffingResult, T as SecurityEventType, _ as LocationDataContext, a as DashOptionsInternal, b as sentinel, c as EndpointOptions, d as InfraPluginConnectionOptionsInternal, f as KvOptions, g as LocationData, h as KvRetryOptionsResolved, i as DashOptions, k as ThresholdConfig, l as InfraEndpointContext, m as KvRetryOptions, n as ApiOptions, o as DashOptionsResolved, p as KvOptionsResolved, r as ApiOptionsResolved, s as Endpoint, t as APIError, u as InfraPluginConnectionOptions, v as SentinelOptions, w as SecurityEvent, x as CompromisedPasswordResult, y as SentinelOptionsInternal } from "./types-Ddk4r5x9.mjs";
1
+ import { C as ImpossibleTravelResult, D as SecurityVerdict, E as SecurityOptions, O as StaleUserResult, S as CredentialStuffingResult, T as SecurityEventType, _ as LocationDataContext, a as DashOptionsInternal, b as sentinel, c as EndpointOptions, d as InfraPluginConnectionOptionsInternal, f as KvOptions, g as LocationData, h as KvRetryOptionsResolved, i as DashOptions, k as ThresholdConfig, l as InfraEndpointContext, m as KvRetryOptions, n as ApiOptions, o as DashOptionsResolved, p as KvOptionsResolved, r as ApiOptionsResolved, s as Endpoint, t as APIError, u as InfraPluginConnectionOptions, v as SentinelOptions, w as SecurityEvent, x as CompromisedPasswordResult, y as SentinelOptionsInternal } from "./types-sP9ucYj1.mjs";
2
2
  import { EMAIL_TEMPLATES, EmailConfig, EmailTemplateId, EmailTemplateVariables, SendBulkEmailsOptions, SendBulkEmailsResult, SendEmailOptions, SendEmailResult, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
3
3
  import { Account, AuthContext, GenericEndpointContext, Session, User } from "better-auth";
4
4
  import z from "zod";
@@ -1157,6 +1157,7 @@ declare const dash: <O extends DashOptions>(options?: O) => {
1157
1157
  entryPoint: import("zod").ZodOptional<import("zod").ZodString>;
1158
1158
  cert: import("zod").ZodOptional<import("zod").ZodString>;
1159
1159
  entityId: import("zod").ZodOptional<import("zod").ZodString>;
1160
+ wantAssertionsSigned: import("zod").ZodOptional<import("zod").ZodBoolean>;
1160
1161
  mapping: import("zod").ZodOptional<import("zod").ZodObject<{
1161
1162
  id: import("zod").ZodOptional<import("zod").ZodString>;
1162
1163
  email: import("zod").ZodOptional<import("zod").ZodString>;
@@ -1214,6 +1215,7 @@ declare const dash: <O extends DashOptions>(options?: O) => {
1214
1215
  entryPoint: import("zod").ZodOptional<import("zod").ZodString>;
1215
1216
  cert: import("zod").ZodOptional<import("zod").ZodString>;
1216
1217
  entityId: import("zod").ZodOptional<import("zod").ZodString>;
1218
+ wantAssertionsSigned: import("zod").ZodOptional<import("zod").ZodBoolean>;
1217
1219
  mapping: import("zod").ZodOptional<import("zod").ZodObject<{
1218
1220
  id: import("zod").ZodOptional<import("zod").ZodString>;
1219
1221
  email: import("zod").ZodOptional<import("zod").ZodString>;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CDhjfr8S.mjs";
2
- import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-D3yZH0Cy.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CARN9f1y.mjs";
2
+ import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-DZClUivd.mjs";
3
3
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
4
  import { getCurrentAdapter, getCurrentAuthContext, getCurrentDBAdapterAsyncLocalStorage, runWithTransaction } from "@better-auth/core/context";
5
5
  import { APIError, generateId, getAuthTables, logger } from "better-auth";
@@ -56,6 +56,27 @@ function resolveSentinelOptions(options) {
56
56
  };
57
57
  }
58
58
  //#endregion
59
+ //#region ../utils/dist/ip.mjs
60
+ /** Platform-set client IP headers (not accepted from clients on the open internet). */
61
+ const DEFAULT_TRUSTED_IP_HEADERS = [
62
+ "cf-connecting-ip",
63
+ "true-client-ip",
64
+ "x-vercel-forwarded-for"
65
+ ];
66
+ /**
67
+ * Resolves client IP from request headers.
68
+ * Defaults to platform-trusted headers only; set `ipAddressHeaders` for self-hosted proxies.
69
+ */
70
+ function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
71
+ const headerNames = ipAddressHeaders?.length ? ipAddressHeaders : [...DEFAULT_TRUSTED_IP_HEADERS];
72
+ for (const headerName of headerNames) {
73
+ const value = headers.get(headerName);
74
+ if (!value) continue;
75
+ const ip = value.split(",")[0]?.trim();
76
+ if (ip) return ip;
77
+ }
78
+ }
79
+ //#endregion
59
80
  //#region src/events/constants.ts
60
81
  const EVENT_TYPES = {
61
82
  USER_CREATED: "user_created",
@@ -1060,27 +1081,6 @@ const initTrackEvents = ($api) => {
1060
1081
  return { tracker: { trackEvent } };
1061
1082
  };
1062
1083
  //#endregion
1063
- //#region ../utils/dist/ip.mjs
1064
- /** Platform-set client IP headers (not accepted from clients on the open internet). */
1065
- const DEFAULT_TRUSTED_IP_HEADERS = [
1066
- "cf-connecting-ip",
1067
- "true-client-ip",
1068
- "x-vercel-forwarded-for"
1069
- ];
1070
- /**
1071
- * Resolves client IP from request headers.
1072
- * Defaults to platform-trusted headers only; set `ipAddressHeaders` for self-hosted proxies.
1073
- */
1074
- function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
1075
- const headerNames = ipAddressHeaders?.length ? ipAddressHeaders : [...DEFAULT_TRUSTED_IP_HEADERS];
1076
- for (const headerName of headerNames) {
1077
- const value = headers.get(headerName);
1078
- if (!value) continue;
1079
- const ip = value.split(",")[0]?.trim();
1080
- if (ip) return ip;
1081
- }
1082
- }
1083
- //#endregion
1084
1084
  //#region src/identification.ts
1085
1085
  /**
1086
1086
  * Identification Service
@@ -2316,6 +2316,9 @@ const ERROR_MESSAGES = {
2316
2316
  compromised_password: "This password has been found in data breaches. Please choose a different password.",
2317
2317
  impossible_travel: "Login blocked due to suspicious location change."
2318
2318
  };
2319
+ function securityBlockMessage(reason) {
2320
+ return ERROR_MESSAGES[reason] || "Access denied.";
2321
+ }
2319
2322
  /**
2320
2323
  * Throw a challenge error with appropriate headers
2321
2324
  */
@@ -2330,56 +2333,152 @@ function throwChallengeError(challenge, reason, message = "Please complete a sec
2330
2333
  };
2331
2334
  throw error;
2332
2335
  }
2333
- /** Emits the `security_check` event inline before throwing — the after-hook won't run on throw. */
2334
- async function handleSecurityVerdict(verdict, hookCtx, checkCtx, trackEvent, securityService) {
2335
- if (verdict.powVerified) recordCheck(hookCtx, "pow_challenge");
2336
- if (verdict.action === "allow") return;
2336
+ /**
2337
+ * Cooldown → consolidated security check → challenge/block/allow.
2338
+ * Does not emit events or throw; callers apply the result.
2339
+ */
2340
+ async function resolveAdmission(input, securityService) {
2341
+ const requestId = input.identification?.requestId ?? input.requestId ?? null;
2342
+ if (input.visitorId || input.ip) {
2343
+ if (await securityService.isBlocked(input.visitorId ?? "", input.ip, requestId)) return { type: "cooldown" };
2344
+ }
2345
+ const verdict = await securityService.checkSecurity({
2346
+ visitorId: input.visitorId,
2347
+ requestId,
2348
+ ip: input.ip,
2349
+ path: input.path,
2350
+ identifier: input.identifier,
2351
+ powSolution: input.powSolution ?? void 0
2352
+ });
2353
+ if (verdict.action === "allow") return {
2354
+ type: "allow",
2355
+ powVerified: verdict.powVerified
2356
+ };
2337
2357
  const reason = verdict.reason || "unknown";
2338
- if (verdict.action === "challenge" && checkCtx.visitorId) {
2339
- const challenge = verdict.challenge || await securityService.generateChallenge(checkCtx.visitorId, checkCtx.identification?.requestId ?? null);
2358
+ if (verdict.action === "challenge") {
2359
+ if (!input.visitorId) return {
2360
+ type: "allow",
2361
+ powVerified: verdict.powVerified
2362
+ };
2363
+ const challenge = verdict.challenge || await securityService.generateChallenge(input.visitorId, requestId);
2340
2364
  if (!challenge?.trim()) {
2341
2365
  logger.warn("[Sentinel] Could not generate PoW challenge (service may be unavailable). Falling back to allow.");
2342
- return;
2366
+ return {
2367
+ type: "allow",
2368
+ powVerified: verdict.powVerified
2369
+ };
2343
2370
  }
2344
- setOutcome(hookCtx, "challenged", "server_security_check", {
2371
+ return {
2372
+ type: "challenge",
2373
+ challenge,
2345
2374
  reason,
2346
- ...verdict.details
2347
- });
2348
- emitEvaluation(hookCtx, trackEvent, {
2349
- identifier: checkCtx.identifier,
2350
- userAgent: checkCtx.userAgent
2351
- });
2352
- throwChallengeError(challenge, reason);
2375
+ details: verdict.details
2376
+ };
2353
2377
  }
2354
- if (verdict.action === "block") {
2355
- setOutcome(hookCtx, "blocked", "server_security_check", {
2356
- reason,
2357
- ...verdict.details
2358
- });
2359
- emitEvaluation(hookCtx, trackEvent, {
2360
- identifier: checkCtx.identifier,
2361
- userAgent: checkCtx.userAgent
2378
+ return {
2379
+ type: "block",
2380
+ reason,
2381
+ details: verdict.details
2382
+ };
2383
+ }
2384
+ /**
2385
+ * Record evaluation side effects and throw on deny. Returns on allow.
2386
+ */
2387
+ function applyAdmission(admission, hookCtx, meta, trackEvent) {
2388
+ if (hookCtx.context.visitorId || hookCtx.context.ip) recordCheck(hookCtx, "credential_stuffing");
2389
+ if (admission.type === "allow") {
2390
+ recordCheck(hookCtx, "server_security_check");
2391
+ if (admission.powVerified) recordCheck(hookCtx, "pow_challenge");
2392
+ return;
2393
+ }
2394
+ if (admission.type === "cooldown") {
2395
+ setOutcome(hookCtx, "blocked", "credential_stuffing", {
2396
+ phase: "cooldown_active",
2397
+ visitorId: hookCtx.context.untrustedVisitorId ?? void 0,
2398
+ confidence: 1
2399
+ });
2400
+ emitEvaluation(hookCtx, trackEvent, meta);
2401
+ throw new APIError(403, { message: "Too many failed attempts. Please try again later." });
2402
+ }
2403
+ if (admission.type === "challenge") {
2404
+ recordCheck(hookCtx, "server_security_check");
2405
+ setOutcome(hookCtx, "challenged", "server_security_check", {
2406
+ reason: admission.reason,
2407
+ ...admission.details
2362
2408
  });
2363
- throw new APIError("FORBIDDEN", { message: ERROR_MESSAGES[reason] || "Access denied." });
2409
+ emitEvaluation(hookCtx, trackEvent, meta);
2410
+ throwChallengeError(admission.challenge, admission.reason);
2364
2411
  }
2412
+ recordCheck(hookCtx, "server_security_check");
2413
+ setOutcome(hookCtx, "blocked", "server_security_check", {
2414
+ reason: admission.reason,
2415
+ ...admission.details
2416
+ });
2417
+ emitEvaluation(hookCtx, trackEvent, meta);
2418
+ throw new APIError("FORBIDDEN", { message: securityBlockMessage(admission.reason) });
2365
2419
  }
2366
2420
  /**
2367
- * Single API call from the plugin; server runs all configured rules and returns
2368
- * one verdict — counts as one billable check.
2421
+ * Resolve + apply in one step (before-hook when rate-limit did not pre-decide).
2369
2422
  */
2370
2423
  async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent, powSolution) {
2371
- recordCheck(hookCtx, "server_security_check");
2372
- await handleSecurityVerdict(await securityService.checkSecurity({
2424
+ applyAdmission(await resolveAdmission({
2373
2425
  visitorId: checkCtx.visitorId,
2374
- requestId: checkCtx.identification?.requestId ?? checkCtx.requestId ?? null,
2426
+ requestId: checkCtx.requestId,
2427
+ identification: checkCtx.identification,
2375
2428
  ip: checkCtx.ip,
2376
2429
  path: checkCtx.path,
2377
2430
  identifier: checkCtx.identifier,
2378
- powSolution: powSolution ?? void 0
2379
- }), hookCtx, checkCtx, trackEvent, securityService);
2431
+ powSolution
2432
+ }, securityService), hookCtx, {
2433
+ identifier: checkCtx.identifier,
2434
+ userAgent: checkCtx.userAgent
2435
+ }, trackEvent);
2380
2436
  }
2381
2437
  //#endregion
2382
2438
  //#region src/sentinel/sentinel.ts
2439
+ /**
2440
+ * Strip basePath from a request URL for path matching.
2441
+ * Local copy of BA's normalizePathname — `@better-auth/core/utils/url` is not
2442
+ * exported on 1.4; `@better-auth/core/utils` barrel is gone on 1.7.
2443
+ */
2444
+ function normalizeAuthPath(requestUrl, basePath) {
2445
+ try {
2446
+ const pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
2447
+ const base = basePath.replace(/\/+$/, "") || "";
2448
+ if (!base || base === "/") return pathname;
2449
+ if (pathname === base) return "/";
2450
+ if (pathname.startsWith(`${base}/`)) return pathname.slice(base.length).replace(/\/+$/, "") || "/";
2451
+ return pathname;
2452
+ } catch {
2453
+ return "/";
2454
+ }
2455
+ }
2456
+ const SECURITY_PROTECTED_ROUTES = [
2457
+ routes.SIGN_IN_EMAIL,
2458
+ routes.SIGN_IN_USERNAME,
2459
+ routes.SIGN_IN_EMAIL_OTP,
2460
+ routes.SIGN_IN_SOCIAL,
2461
+ routes.SIGN_IN_PASSKEY,
2462
+ routes.SIGN_IN_MAGIC_LINK,
2463
+ routes.SIGN_IN_SSO,
2464
+ routes.SIGN_IN_ANONYMOUS,
2465
+ routes.SIGN_UP_EMAIL,
2466
+ routes.FORGET_PASSWORD,
2467
+ routes.REQUEST_PASSWORD_RESET,
2468
+ routes.RESET_PASSWORD,
2469
+ routes.TWO_FACTOR_VERIFY_TOTP,
2470
+ routes.TWO_FACTOR_VERIFY_BACKUP,
2471
+ routes.TWO_FACTOR_VERIFY_OTP,
2472
+ routes.EMAIL_OTP_SEND,
2473
+ routes.PHONE_SEND_OTP,
2474
+ routes.MAGIC_LINK_VERIFY,
2475
+ routes.ORG_CREATE,
2476
+ routes.CHANGE_EMAIL,
2477
+ routes.CHANGE_PASSWORD,
2478
+ routes.SET_PASSWORD,
2479
+ routes.LINK_SOCIAL,
2480
+ routes.PASSKEY_ADD
2481
+ ];
2383
2482
  const sentinel = (options) => {
2384
2483
  const opts = resolveSentinelOptions(options);
2385
2484
  const $api = createAPI(opts);
@@ -2402,6 +2501,12 @@ const sentinel = (options) => {
2402
2501
  return typeof err.body === "object" && err.body !== null && "code" in err.body && err.body.code === STALE_ACCOUNT_BLOCK_ERROR.code;
2403
2502
  }
2404
2503
  let activityTrackingEnabled = false;
2504
+ /**
2505
+ * Admission from rateLimit.customRules for this Request. Reused by the
2506
+ * before-hook and by BA 1.5's second customRules pass on onResponse.
2507
+ * Do not delete — response-phase must still hit. GC cleans up with Request.
2508
+ */
2509
+ const rateLimitAdmission = /* @__PURE__ */ new WeakMap();
2405
2510
  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.");
2406
2511
  const securityService = createSecurityClient(opts, $apiThrowing, opts.security || {}, (event) => {
2407
2512
  trackEvent({
@@ -2432,114 +2537,165 @@ const sentinel = (options) => {
2432
2537
  id: "sentinel",
2433
2538
  init(ctx) {
2434
2539
  activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
2435
- return { options: {
2436
- emailValidation: opts.security?.emailValidation,
2437
- emailNormalization: opts.security?.emailNormalization,
2438
- databaseHooks: {
2439
- user: {
2440
- create: {
2441
- async before(user, ctx) {
2442
- if (!ctx) return;
2443
- if (isDashRoute(ctx.path)) {
2540
+ const basePath = new URL(ctx.baseURL).pathname;
2541
+ const ipHeaders = ctx.options.advanced?.ipAddress?.ipAddressHeaders || null;
2542
+ const disableIpTracking = ctx.options.advanced?.ipAddress?.disableIpTracking === true;
2543
+ /**
2544
+ * After any user customRule resolves, run Sentinel admission so PoW
2545
+ * challenges skip BA consume (protocol round-trip) even when the app
2546
+ * owns path rules or `**`. Block/cooldown still consume.
2547
+ */
2548
+ const applyCoordination = async (request, afterUser) => {
2549
+ const path = normalizeAuthPath(request.url, basePath);
2550
+ if (request.method === "GET" || isDashRoute(path) || !matchesAnyRoute(path, SECURITY_PROTECTED_ROUTES)) return afterUser;
2551
+ try {
2552
+ const cached = rateLimitAdmission.get(request);
2553
+ if (cached) return cached.type === "challenge" ? false : afterUser;
2554
+ const requestId = request.headers.get("X-Request-Id");
2555
+ const identification = requestId ? await getIdentification(requestId, $kv, opts.kvOptions.retry) : null;
2556
+ const ip = disableIpTracking ? identification?.ip ?? null : identification?.ip ?? resolveClientIpFromHeaders(request.headers, ipHeaders) ?? null;
2557
+ const admission = await resolveAdmission({
2558
+ visitorId: identification?.visitorId ?? null,
2559
+ requestId,
2560
+ identification,
2561
+ ip,
2562
+ path,
2563
+ powSolution: request.headers.get("X-PoW-Solution")
2564
+ }, securityService);
2565
+ rateLimitAdmission.set(request, admission);
2566
+ if (admission.type === "challenge") return false;
2567
+ return afterUser;
2568
+ } catch {
2569
+ return afterUser;
2570
+ }
2571
+ };
2572
+ const wrapCustomRule = (userRule) => {
2573
+ return async (request, current) => {
2574
+ const userResult = typeof userRule === "function" ? await userRule(request, current) : userRule;
2575
+ return applyCoordination(request, userResult === false ? false : userResult || current);
2576
+ };
2577
+ };
2578
+ const customRules = {};
2579
+ for (const [key, rule] of Object.entries(ctx.rateLimit?.customRules ?? {})) {
2580
+ if (rule === void 0) continue;
2581
+ customRules[key] = wrapCustomRule(rule);
2582
+ }
2583
+ if (!Object.hasOwn(customRules, "**")) customRules["**"] = async (request, current) => applyCoordination(request, current);
2584
+ return {
2585
+ context: { rateLimit: {
2586
+ ...ctx.rateLimit,
2587
+ customRules
2588
+ } },
2589
+ options: {
2590
+ emailValidation: opts.security?.emailValidation,
2591
+ emailNormalization: opts.security?.emailNormalization,
2592
+ rateLimit: { customRules },
2593
+ databaseHooks: {
2594
+ user: {
2595
+ create: {
2596
+ async before(user, ctx) {
2597
+ if (!ctx) return;
2598
+ if (isDashRoute(ctx.path)) {
2599
+ if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2600
+ ...user,
2601
+ email: normalizeEmail(user.email, ctx.context)
2602
+ } };
2603
+ return;
2604
+ }
2605
+ const visitorId = ctx.context.visitorId;
2606
+ if (opts.security?.freeTrialAbuse?.enabled) {
2607
+ if (!visitorId) {
2608
+ recordCheck(ctx, "free_trial_abuse");
2609
+ setOutcome(ctx, "blocked", "free_trial_abuse", { reason: "identification_required" });
2610
+ emitEvaluation(ctx, trackEvent);
2611
+ throw new APIError("FORBIDDEN", { message: "Account creation is not allowed without device identification." });
2612
+ }
2613
+ recordCheck(ctx, "free_trial_abuse");
2614
+ const reservationId = crypto.randomUUID();
2615
+ ctx.context.freeTrialReservationId = reservationId;
2616
+ const abuseCheck = await securityService.reserveFreeTrialSignup(visitorId, reservationId, ctx.context.requestId ?? null);
2617
+ if (abuseCheck.isAbuse && abuseCheck.action === "block") {
2618
+ setOutcome(ctx, "blocked", "free_trial_abuse", {
2619
+ accountCount: abuseCheck.accountCount,
2620
+ maxAccounts: abuseCheck.maxAccounts
2621
+ });
2622
+ emitEvaluation(ctx, trackEvent);
2623
+ throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2624
+ }
2625
+ }
2444
2626
  if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2445
2627
  ...user,
2446
2628
  email: normalizeEmail(user.email, ctx.context)
2447
2629
  } };
2448
- return;
2449
- }
2450
- const visitorId = ctx.context.visitorId;
2451
- if (opts.security?.freeTrialAbuse?.enabled) {
2452
- if (!visitorId) {
2453
- recordCheck(ctx, "free_trial_abuse");
2454
- setOutcome(ctx, "blocked", "free_trial_abuse", { reason: "identification_required" });
2455
- emitEvaluation(ctx, trackEvent);
2456
- throw new APIError("FORBIDDEN", { message: "Account creation is not allowed without device identification." });
2457
- }
2458
- recordCheck(ctx, "free_trial_abuse");
2459
- const reservationId = crypto.randomUUID();
2460
- ctx.context.freeTrialReservationId = reservationId;
2461
- const abuseCheck = await securityService.reserveFreeTrialSignup(visitorId, reservationId, ctx.context.requestId ?? null);
2462
- if (abuseCheck.isAbuse && abuseCheck.action === "block") {
2463
- setOutcome(ctx, "blocked", "free_trial_abuse", {
2464
- accountCount: abuseCheck.accountCount,
2465
- maxAccounts: abuseCheck.maxAccounts
2466
- });
2467
- emitEvaluation(ctx, trackEvent);
2468
- throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
2469
- }
2630
+ },
2631
+ async after(user, ctx) {
2632
+ if (!ctx || isDashRoute(ctx.path)) return;
2633
+ const visitorId = ctx.context.visitorId;
2634
+ const reservationId = ctx.context.freeTrialReservationId;
2635
+ if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
2470
2636
  }
2637
+ },
2638
+ update: { async before(user, ctx) {
2639
+ if (!ctx) return;
2471
2640
  if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2472
2641
  ...user,
2473
2642
  email: normalizeEmail(user.email, ctx.context)
2474
2643
  } };
2475
- },
2476
- async after(user, ctx) {
2644
+ } }
2645
+ },
2646
+ session: { create: {
2647
+ async before(session, ctx) {
2477
2648
  if (!ctx || isDashRoute(ctx.path)) return;
2478
2649
  const visitorId = ctx.context.visitorId;
2479
- const reservationId = ctx.context.freeTrialReservationId;
2480
- if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
2481
- }
2482
- },
2483
- update: { async before(user, ctx) {
2484
- if (!ctx) return;
2485
- if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2486
- ...user,
2487
- email: normalizeEmail(user.email, ctx.context)
2488
- } };
2489
- } }
2490
- },
2491
- session: { create: {
2492
- async before(session, ctx) {
2493
- if (!ctx || isDashRoute(ctx.path)) return;
2494
- const visitorId = ctx.context.visitorId;
2495
- const identification = ctx.context.identification;
2496
- if (session.userId && identification?.location && visitorId) {
2497
- recordCheck(ctx, "impossible_travel");
2498
- const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
2499
- const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution, identification.requestId);
2500
- if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
2501
- if (travelCheck?.isImpossible) {
2502
- if (travelCheck.action === "block") {
2503
- setOutcome(ctx, "blocked", "impossible_travel", {
2504
- from: travelCheck.from,
2505
- to: travelCheck.to,
2506
- distance: travelCheck.distance,
2507
- speedRequired: travelCheck.speedRequired
2508
- });
2509
- emitEvaluation(ctx, trackEvent);
2510
- throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
2511
- }
2512
- if (travelCheck.action === "challenge" && travelCheck.challenge) {
2513
- setOutcome(ctx, "challenged", "impossible_travel", {
2514
- from: travelCheck.from,
2515
- to: travelCheck.to,
2516
- distance: travelCheck.distance,
2517
- speedRequired: travelCheck.speedRequired
2518
- });
2519
- emitEvaluation(ctx, trackEvent);
2520
- throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
2650
+ const identification = ctx.context.identification;
2651
+ if (session.userId && identification?.location && visitorId) {
2652
+ recordCheck(ctx, "impossible_travel");
2653
+ const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
2654
+ const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution, identification.requestId);
2655
+ if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
2656
+ if (travelCheck?.isImpossible) {
2657
+ if (travelCheck.action === "block") {
2658
+ setOutcome(ctx, "blocked", "impossible_travel", {
2659
+ from: travelCheck.from,
2660
+ to: travelCheck.to,
2661
+ distance: travelCheck.distance,
2662
+ speedRequired: travelCheck.speedRequired
2663
+ });
2664
+ emitEvaluation(ctx, trackEvent);
2665
+ throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
2666
+ }
2667
+ if (travelCheck.action === "challenge" && travelCheck.challenge) {
2668
+ setOutcome(ctx, "challenged", "impossible_travel", {
2669
+ from: travelCheck.from,
2670
+ to: travelCheck.to,
2671
+ distance: travelCheck.distance,
2672
+ speedRequired: travelCheck.speedRequired
2673
+ });
2674
+ emitEvaluation(ctx, trackEvent);
2675
+ throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
2676
+ }
2521
2677
  }
2522
2678
  }
2679
+ },
2680
+ async after(session, ctx) {
2681
+ if (!ctx || !session.userId || isDashRoute(ctx.path)) return;
2682
+ const visitorId = ctx.context.visitorId;
2683
+ const identification = ctx.context.identification;
2684
+ let user = null;
2685
+ try {
2686
+ user = await getUserById(session.userId, ctx, { includeLastActiveAt: activityTrackingEnabled });
2687
+ } catch (error) {
2688
+ logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2689
+ }
2690
+ if (visitorId) {
2691
+ if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
2692
+ }
2693
+ if (opts.security?.impossibleTravel?.enabled && identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location, identification.ip));
2523
2694
  }
2524
- },
2525
- async after(session, ctx) {
2526
- if (!ctx || !session.userId || isDashRoute(ctx.path)) return;
2527
- const visitorId = ctx.context.visitorId;
2528
- const identification = ctx.context.identification;
2529
- let user = null;
2530
- try {
2531
- user = await getUserById(session.userId, ctx, { includeLastActiveAt: activityTrackingEnabled });
2532
- } catch (error) {
2533
- logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
2534
- }
2535
- if (visitorId) {
2536
- if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
2537
- }
2538
- if (opts.security?.impossibleTravel?.enabled && identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location, identification.ip));
2539
- }
2540
- } }
2695
+ } }
2696
+ }
2541
2697
  }
2542
- } };
2698
+ };
2543
2699
  },
2544
2700
  hooks: {
2545
2701
  before: [
@@ -2558,64 +2714,25 @@ const sentinel = (options) => {
2558
2714
  const visitorId = ctx.context.visitorId;
2559
2715
  const identification = ctx.context.identification;
2560
2716
  const ip = ctx.context.ip;
2561
- const isSignIn = matchesAnyRoute(ctx.path, [
2562
- routes.SIGN_IN_EMAIL,
2563
- routes.SIGN_IN_USERNAME,
2564
- routes.SIGN_IN_EMAIL_OTP,
2565
- routes.SIGN_IN_SOCIAL,
2566
- routes.SIGN_IN_PASSKEY,
2567
- routes.SIGN_IN_MAGIC_LINK,
2568
- routes.SIGN_IN_SSO,
2569
- routes.SIGN_IN_ANONYMOUS
2570
- ]);
2571
- const isSignUp = matchesAnyRoute(ctx.path, [routes.SIGN_UP_EMAIL]);
2572
- const isPasswordReset = matchesAnyRoute(ctx.path, [
2573
- routes.FORGET_PASSWORD,
2574
- routes.REQUEST_PASSWORD_RESET,
2575
- routes.RESET_PASSWORD
2576
- ]);
2577
- const isTwoFactor = matchesAnyRoute(ctx.path, [
2578
- routes.TWO_FACTOR_VERIFY_TOTP,
2579
- routes.TWO_FACTOR_VERIFY_BACKUP,
2580
- routes.TWO_FACTOR_VERIFY_OTP
2581
- ]);
2582
- const isOtpSend = matchesAnyRoute(ctx.path, [routes.EMAIL_OTP_SEND, routes.PHONE_SEND_OTP]);
2583
- const isMagicLinkVerify = matchesAnyRoute(ctx.path, [routes.MAGIC_LINK_VERIFY]);
2584
- const isOrgCreate = matchesAnyRoute(ctx.path, [routes.ORG_CREATE]);
2585
- const isSensitiveAction = matchesAnyRoute(ctx.path, [
2586
- routes.CHANGE_EMAIL,
2587
- routes.CHANGE_PASSWORD,
2588
- routes.SET_PASSWORD,
2589
- routes.LINK_SOCIAL,
2590
- routes.PASSKEY_ADD
2591
- ]);
2592
- if (!(isSignIn || isSignUp || isPasswordReset || isTwoFactor || isOtpSend || isMagicLinkVerify || isOrgCreate || isSensitiveAction)) return;
2717
+ if (!matchesAnyRoute(ctx.path, SECURITY_PROTECTED_ROUTES)) return;
2593
2718
  const requestBody = ctx.body;
2594
2719
  const identifier = requestBody?.email || requestBody?.phone || requestBody?.username || void 0;
2595
2720
  const powSolution = ctx.headers?.get?.("X-PoW-Solution");
2596
- if (visitorId || ip) {
2597
- recordCheck(ctx, "credential_stuffing");
2598
- if (await securityService.isBlocked(visitorId ?? "", ip, ctx.context.requestId ?? null)) {
2599
- setOutcome(ctx, "blocked", "credential_stuffing", {
2600
- phase: "cooldown_active",
2601
- visitorId: ctx.context.untrustedVisitorId ?? void 0,
2602
- confidence: 1
2603
- });
2604
- emitEvaluation(ctx, trackEvent, {
2605
- identifier,
2606
- userAgent: ctx.headers?.get?.("user-agent") || ""
2607
- });
2608
- throw new APIError(403, { message: "Too many failed attempts. Please try again later." });
2609
- }
2610
- }
2611
- await runSecurityChecks(ctx, {
2612
- path: ctx.path,
2721
+ const userAgent = ctx.headers?.get?.("user-agent") || "";
2722
+ const meta = {
2723
+ identifier,
2724
+ userAgent
2725
+ };
2726
+ const cached = ctx.request ? rateLimitAdmission.get(ctx.request) : void 0;
2727
+ if (cached) applyAdmission(cached, ctx, meta, trackEvent);
2728
+ else await runSecurityChecks(ctx, {
2729
+ path: ctx.path ?? "",
2613
2730
  identifier,
2614
2731
  visitorId,
2615
2732
  requestId: ctx.context.requestId ?? null,
2616
2733
  identification,
2617
- ip: ctx.context.ip,
2618
- userAgent: ctx.headers?.get?.("user-agent") || ""
2734
+ ip,
2735
+ userAgent
2619
2736
  }, securityService, trackEvent, powSolution);
2620
2737
  if (matchesAnyRoute(ctx.path, [
2621
2738
  routes.SIGN_UP_EMAIL,
@@ -2633,7 +2750,7 @@ const sentinel = (options) => {
2633
2750
  setOutcome(ctx, "blocked", "compromised_password", { breachCount: compromisedResult.breachCount });
2634
2751
  emitEvaluation(ctx, trackEvent, {
2635
2752
  identifier,
2636
- userAgent: ctx.headers?.get?.("user-agent") || ""
2753
+ userAgent
2637
2754
  });
2638
2755
  throw new APIError("BAD_REQUEST", {
2639
2756
  message: "This password has been found in data breaches. Please choose a different password.",
@@ -4280,6 +4397,23 @@ const getConfig = (options) => {
4280
4397
  emailVerification: { sendVerificationEmailEnabled: !!ctx.context.options.emailVerification?.sendVerificationEmail },
4281
4398
  insights: {
4282
4399
  hasDatabase: !!ctx.context.options.database,
4400
+ adapter: (() => {
4401
+ const adapter = ctx.context.adapter;
4402
+ if (!adapter) return null;
4403
+ const adapterConfig = adapter.options?.adapterConfig;
4404
+ return {
4405
+ id: adapter.id ?? adapterConfig?.adapterId ?? null,
4406
+ name: adapterConfig?.adapterName ?? adapter.id ?? adapterConfig?.adapterId ?? null,
4407
+ usePlural: adapterConfig?.usePlural ?? false,
4408
+ supportsTransactions: typeof adapterConfig?.transaction === "function",
4409
+ supportsJSON: typeof adapterConfig?.supportsJSON === "boolean" ? adapterConfig.supportsJSON : null,
4410
+ supportsDates: typeof adapterConfig?.supportsDates === "boolean" ? adapterConfig.supportsDates : null,
4411
+ supportsBooleans: typeof adapterConfig?.supportsBooleans === "boolean" ? adapterConfig.supportsBooleans : null,
4412
+ supportsArrays: typeof adapterConfig?.supportsArrays === "boolean" ? adapterConfig.supportsArrays : null,
4413
+ supportsNumericIds: typeof adapterConfig?.supportsNumericIds === "boolean" ? adapterConfig.supportsNumericIds : null,
4414
+ supportsUUIDs: typeof adapterConfig?.supportsUUIDs === "boolean" ? adapterConfig.supportsUUIDs : null
4415
+ };
4416
+ })(),
4283
4417
  cookies: advancedOptions?.cookies ? Object.entries(advancedOptions.cookies).map(([key, value]) => ({
4284
4418
  key,
4285
4419
  name: value?.name,
@@ -8236,6 +8370,7 @@ const samlConfigSchema = z.object({
8236
8370
  entryPoint: z.string().optional(),
8237
8371
  cert: z.string().optional(),
8238
8372
  entityId: z.string().optional(),
8373
+ wantAssertionsSigned: z.boolean().optional(),
8239
8374
  mapping: z.object({
8240
8375
  /** @deprecated Removed in better-auth 1.7+; SAML subject is NameID. */
8241
8376
  id: z.string().optional(),
@@ -8304,6 +8439,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
8304
8439
  config: {
8305
8440
  issuer: ba17OrLater && !idpMetadataXml ? spIssuer : samlConfig.entityId ?? spIssuer,
8306
8441
  idpMetadata,
8442
+ wantAssertionsSigned: samlConfig.wantAssertionsSigned ?? true,
8307
8443
  ...saml17OrNewer ? {
8308
8444
  entryPoint: resolvedEntryPoint,
8309
8445
  ...samlConfig.cert ? { cert: samlConfig.cert } : {}
package/dist/native.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-CgijjwVT.mjs";
1
+ import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-Cim5Fztg.mjs";
2
2
  import { BetterAuthClientPlugin } from "better-auth";
3
3
  //#region src/sentinel/native/client.d.ts
4
4
  interface SentinelNativeClientOptions {
package/dist/native.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { o as createKV, t as bytesToHex } from "./crypto-D3yZH0Cy.mjs";
2
- import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-jWGI__oh.mjs";
1
+ import { o as createKV, t as bytesToHex } from "./crypto-DZClUivd.mjs";
2
+ import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-BvjK8YBw.mjs";
3
3
  import { env } from "@better-auth/core/env";
4
4
  import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
5
5
  //#region src/sentinel/native/components.ts
@@ -1,4 +1,4 @@
1
- import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-D3yZH0Cy.mjs";
1
+ import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-DZClUivd.mjs";
2
2
  //#region src/dash-client.ts
3
3
  function resolveDashUserId(input, options) {
4
4
  return input.userId || options?.resolveUserId?.({
@@ -1,5 +1,6 @@
1
1
  import { BetterAuthPlugin, GenericEndpointContext } from "better-auth";
2
2
  import { createFetch } from "@better-fetch/fetch";
3
+ import { BetterAuthRateLimitRule } from "@better-auth/core";
3
4
  import { APIError as APIError$1, Endpoint, EndpointOptions } from "better-call";
4
5
  //#region src/identification.d.ts
5
6
  interface IPLocation {
@@ -167,6 +168,20 @@ type SecurityEventType = "unknown_device" | "credential_stuffing" | "impossible_
167
168
  declare const sentinel: (options?: SentinelOptions) => {
168
169
  id: "sentinel";
169
170
  init(ctx: import("better-auth").AuthContext): {
171
+ context: {
172
+ rateLimit: {
173
+ customRules: {
174
+ [key: string]: false | BetterAuthRateLimitRule | ((request: Request, currentRule: BetterAuthRateLimitRule) => import("@better-auth/core").Awaitable<false | BetterAuthRateLimitRule>);
175
+ };
176
+ enabled: boolean;
177
+ window: number;
178
+ max: number;
179
+ storage: "memory" | "database" | "secondary-storage";
180
+ modelName?: import("better-auth").LiteralString | "rateLimit" | undefined;
181
+ fields?: Partial<Record<"key" | "count" | "lastRequest", string>> | undefined;
182
+ customStorage?: import("better-auth").BetterAuthRateLimitStorage | undefined;
183
+ };
184
+ };
170
185
  options: {
171
186
  emailValidation: {
172
187
  enabled?: boolean;
@@ -177,6 +192,11 @@ declare const sentinel: (options?: SentinelOptions) => {
177
192
  emailNormalization: {
178
193
  enabled?: boolean;
179
194
  } | undefined;
195
+ rateLimit: {
196
+ customRules: {
197
+ [key: string]: false | BetterAuthRateLimitRule | ((request: Request, currentRule: BetterAuthRateLimitRule) => import("@better-auth/core").Awaitable<false | BetterAuthRateLimitRule>);
198
+ };
199
+ };
180
200
  databaseHooks: {
181
201
  user: {
182
202
  create: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/infra",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Dashboard and analytics plugin for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -74,7 +74,7 @@
74
74
  "happy-dom": "^20.9.0",
75
75
  "msw": "^2.14.6",
76
76
  "tsdown": "^0.22.14",
77
- "typescript": "^5.9.2",
77
+ "typescript": "^6.0.3",
78
78
  "zod": "4.3.6"
79
79
  },
80
80
  "dependencies": {
@@ -83,7 +83,7 @@
83
83
  "@noble/hashes": "^2.2.0",
84
84
  "@xmldom/xmldom": "^0.9.10",
85
85
  "better-call": "1.3.7",
86
- "jose": "^6.2.4",
86
+ "jose": "^6.2.9",
87
87
  "libphonenumber-js": "^1.13.11"
88
88
  },
89
89
  "peerDependencies": {