@better-auth/infra 0.2.14 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,54 @@ 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.3.1] - 2026-06-22
9
+
10
+ ### Changed
11
+
12
+ - **`dashClient` typing** — The client plugin now declares an explicit `BetterAuthClientPlugin` uses the updated signature, so `createAuthClient` inference works without casts when composing with other plugins.
13
+
14
+ ### Fixed
15
+
16
+ - **Package metadata** — Repository and issue tracker URLs now point to `better-auth/infrastructure`.
17
+
18
+ ## [0.3.0] - 2026-06-19
19
+
20
+ ### Added
21
+
22
+ - **Better Auth 1.7.x support** — SCIM directory sync works without the removed `providerOwnership` option; SSO dashboard routes no longer require `@better-auth/sso` as a separate peer dependency.
23
+ - **Backwards compatibility for Better Auth 1.4–1.6** — Session deletion and session listing work across internal adapter API changes; secondary-storage sessions missing `id` are normalized for dashboard use.
24
+ - **Expanded invitation auth modes** — Invitation acceptance supports `credential_setup`, `create_with_session`, and `create_no_session` in addition to `auth` and `direct`, aligned with the platform dashboard.
25
+ - **`deleteAllSessions` on ban endpoints** — `banUser` and `banManyUsers` accept an optional `deleteAllSessions` flag (defaults to `true`); `banManyUsers` now revokes sessions for banned users.
26
+ - **Strict user schemas** — Dashboard user create and update routes validate against schemas derived from the auth user table.
27
+
28
+ ### Changed
29
+
30
+ - **Better Auth upgrade** — Compatibility target raised to Better Auth 1.7.x.
31
+ - **Dash config redaction** — Plugin options and sensitive auth configuration use shared redaction utilities for more consistent masking.
32
+ - **Client IP resolution** — Platform proxy headers are trusted by default; custom `ipAddressHeaders` must be configured explicitly.
33
+ - **Sentinel security evaluation** — Security check events fall back to an untrusted visitor ID when trusted device identification is unavailable.
34
+ - **Free-trial abuse prevention** — Uses atomic signup reservation and confirmation instead of a pre-create check.
35
+ - **SMS client IP forwarding** — Outbound SMS requests can include the end-user IP via `x-better-auth-client-ip` for server-side abuse limits.
36
+
37
+ ### Fixed
38
+
39
+ - **Generic OAuth callback events** — Failed sign-in events are tracked for generic OAuth2 callbacks (`/oauth2/callback/:providerId`).
40
+ - **Event property typo** — Tracked failed sign-in events now use `userName` instead of `nameName`.
41
+ - **Credential stuffing on reset-password** — Sentinel breached-password checks now run on the reset-password route.
42
+ - **Sentinel identification** — Identify requests retry with exponential backoff, stale identification sessions expire and regenerate, and auth routes retry identification when the page-load attempt failed.
43
+ - **Free-trial security checks** — Fixed race conditions on signup reservation and confirmation.
44
+ - **Invitation scoping** — Cancel and resend invitation operations validate the invitation ID and scope queries to the authenticated organization.
45
+
46
+ ### Security
47
+
48
+ - **Session tokens in dash responses** — Bearer tokens are stripped from dashboard user session listings.
49
+ - **Credential stuffing fingerprints** — Failed-login password fingerprints use per-tenant HMAC-SHA256 instead of unsalted SHA-256.
50
+ - **Identification for free-trial abuse** — Sign-up is blocked when free-trial abuse prevention is enabled but device identification is missing.
51
+ - **Security check binding** — Sentinel security API calls include `requestId` for tighter operation binding.
52
+ - **Redirect URL validation** — Dashboard and invitation routes validate callback and redirect URLs as safe `http(s)` URLs without embedded credentials.
53
+ - **Organization slug validation** — Organization routes enforce comprehensive slug validation.
54
+ - **User schema enforcement** — Dashboard user mutations are restricted to writable fields from the auth schema.
55
+
8
56
  ## [0.2.14] - 2026-06-09
9
57
 
10
58
  ### Added
@@ -37,5 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
85
 
38
86
  - Fixed impossible travel security challenges.
39
87
 
88
+ [0.3.1]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.0...infra@v0.3.1
89
+ [0.3.0]: https://github.com/better-auth/infrastructure/compare/infra@v0.2.14...infra@v0.3.0
40
90
  [0.2.14]: https://github.com/better-auth/better-auth-infra/compare/@better-auth/infra@0.2.13...@better-auth/infra@0.2.14
41
91
  [0.2.13]: https://github.com/better-auth/better-auth-infra/compare/@better-auth/infra@0.2.12...@better-auth/infra@0.2.13
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAuditLogsInput, i as DashGetAllAuditLogsInput, n as DashAuditLogsResponse, o as dashClient, r as DashClientOptions, t as DashAuditLog } from "./dash-client-CZzJyRrV.mjs";
1
+ import { a as DashGetAuditLogsInput, i as DashGetAllAuditLogsInput, n as DashAuditLogsResponse, o as dashClient, r as DashClientOptions, t as DashAuditLog } from "./dash-client-CImS7uA_.mjs";
2
2
 
3
3
  //#region src/sentinel/client.d.ts
4
4
  interface SentinelClientOptions {
package/dist/client.mjs CHANGED
@@ -1,6 +1,5 @@
1
- import "./constants-CLYqEwMV.mjs";
2
- import { n as createKV } from "./fetch-Bl0S3xUi.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";
1
+ import { n as hash, o as createKV } from "./crypto-DuXBYsfj.mjs";
2
+ import { a as resolveSentinelClientIdentifyUrl, c as dashClient, i as solvePoWChallenge, n as decodePoWChallenge, o as generateRequestId, r as encodePoWSolution, s as identify, t as createPowRetryTimeout } from "./pow-retry-DyWb0par.mjs";
4
3
  import { env } from "@better-auth/core/env";
5
4
  //#region src/sentinel/fingerprint.ts
6
5
  function murmurhash3(str, seed = 0) {
@@ -358,13 +357,23 @@ function calculateConfidence(components) {
358
357
  }
359
358
  let cachedFingerprint = null;
360
359
  let fingerprintPromise = null;
361
- let identifySent = false;
362
- let identifyCompletePromise = null;
363
- let identifyCompleteResolve = null;
360
+ let identifySucceeded = false;
361
+ let identifyExpiresAt = null;
362
+ let identifyInFlight = null;
363
+ function expireIdentifySessionIfStale() {
364
+ if (!identifySucceeded || identifyExpiresAt === null) return;
365
+ if (Date.now() < identifyExpiresAt) return;
366
+ identifySucceeded = false;
367
+ identifyExpiresAt = null;
368
+ if (cachedFingerprint) cachedFingerprint = {
369
+ ...cachedFingerprint,
370
+ requestId: generateRequestId()
371
+ };
372
+ }
364
373
  async function getFingerprint() {
365
374
  if (typeof window === "undefined") return null;
366
- if (await cachedFingerprint) return cachedFingerprint;
367
- if (await fingerprintPromise) return fingerprintPromise;
375
+ if (cachedFingerprint) return cachedFingerprint;
376
+ if (fingerprintPromise !== null) return fingerprintPromise;
368
377
  fingerprintPromise = (async () => {
369
378
  const components = await collectFingerprint();
370
379
  const visitorId = await generateVisitorId(components);
@@ -386,39 +395,39 @@ async function getFingerprint() {
386
395
  }
387
396
  }
388
397
  async function sendIdentify($kv) {
389
- if (identifySent || typeof window === "undefined") return;
390
- const fingerprint = await getFingerprint();
391
- if (!fingerprint) return;
392
- identifySent = true;
393
- identifyCompletePromise = new Promise((resolve) => {
394
- identifyCompleteResolve = resolve;
395
- });
396
- const payload = {
397
- visitorId: fingerprint.visitorId,
398
- requestId: fingerprint.requestId,
399
- confidence: fingerprint.confidence,
400
- components: fingerprint.components,
401
- url: window.location.href,
402
- incognito: detectIncognito()
403
- };
404
- try {
405
- await identify($kv, payload);
406
- } catch (error) {
407
- console.warn("[Dash] Identify request failed:", error);
408
- } finally {
409
- identifyCompleteResolve?.();
410
- }
398
+ if (typeof window === "undefined") return identifySucceeded;
399
+ expireIdentifySessionIfStale();
400
+ if (identifySucceeded) return true;
401
+ if (identifyInFlight !== null) return identifyInFlight;
402
+ identifyInFlight = (async () => {
403
+ const fingerprint = await getFingerprint();
404
+ if (!fingerprint) return false;
405
+ const payload = {
406
+ visitorId: fingerprint.visitorId,
407
+ requestId: fingerprint.requestId,
408
+ confidence: fingerprint.confidence,
409
+ components: fingerprint.components,
410
+ url: window.location.href,
411
+ incognito: detectIncognito()
412
+ };
413
+ try {
414
+ const result = await identify($kv, payload);
415
+ identifySucceeded = true;
416
+ identifyExpiresAt = result.expiresAt ?? null;
417
+ return true;
418
+ } catch (error) {
419
+ console.warn("[Dash] Identify request failed:", error);
420
+ return false;
421
+ } finally {
422
+ identifyInFlight = null;
423
+ }
424
+ })();
425
+ return identifyInFlight;
411
426
  }
412
- /**
413
- * Wait for identify to complete, with a timeout
414
- * Returns immediately if identify hasn't started or has completed
415
- *
416
- * When `timeoutMs` is omitted or nullish, {@link KV_TIMEOUT_MS} is used.
417
- */
418
- async function waitForIdentify(timeoutMs) {
419
- if (!identifyCompletePromise) return;
420
- const ms = timeoutMs ?? 1e3;
421
- await Promise.race([identifyCompletePromise, new Promise((resolve) => setTimeout(resolve, ms))]);
427
+ /** Append visitor id always; request id only after a successful identify POST. */
428
+ function appendIdentification(headers, fingerprint) {
429
+ headers.set("X-Visitor-Id", fingerprint.visitorId);
430
+ if (identifySucceeded) headers.set("X-Request-Id", fingerprint.requestId);
422
431
  }
423
432
  //#endregion
424
433
  //#region src/sentinel/client.ts
@@ -432,6 +441,19 @@ const sentinelClient = (options) => {
432
441
  envKvUrl: env.BETTER_AUTH_KV_URL
433
442
  }),
434
443
  kvTimeout: options?.kvTimeout
444
+ }, {
445
+ throw: true,
446
+ retry: {
447
+ type: "exponential",
448
+ attempts: 2,
449
+ baseDelay: 400,
450
+ maxDelay: 600,
451
+ shouldRetry(response) {
452
+ if (response === null) return true;
453
+ if (response.status === 429) return true;
454
+ return response.status >= 500;
455
+ }
456
+ }
435
457
  });
436
458
  if (typeof window !== "undefined") {
437
459
  const scheduleIdentify = () => {
@@ -452,14 +474,11 @@ const sentinelClient = (options) => {
452
474
  name: "sentinel-fingerprint",
453
475
  hooks: { async onRequest(context) {
454
476
  if (typeof window === "undefined") return context;
455
- await waitForIdentify(options?.kvTimeout);
477
+ await sendIdentify($kv);
456
478
  const fingerprint = await getFingerprint();
457
479
  if (!fingerprint) return context;
458
480
  const headers = context.headers || new Headers();
459
- if (headers instanceof Headers) {
460
- headers.set("X-Visitor-Id", fingerprint.visitorId);
461
- headers.set("X-Request-Id", fingerprint.requestId);
462
- }
481
+ if (headers instanceof Headers) appendIdentification(headers, fingerprint);
463
482
  return {
464
483
  ...context,
465
484
  headers
@@ -492,10 +511,7 @@ const sentinelClient = (options) => {
492
511
  const fingerprint = await getFingerprint();
493
512
  const retryHeaders = new Headers();
494
513
  retryHeaders.set("X-PoW-Solution", encodePoWSolution(solution));
495
- if (fingerprint) {
496
- retryHeaders.set("X-Visitor-Id", fingerprint.visitorId);
497
- retryHeaders.set("X-Request-Id", fingerprint.requestId);
498
- }
514
+ if (fingerprint) appendIdentification(retryHeaders, fingerprint);
499
515
  retryHeaders.set("Content-Type", "application/json");
500
516
  const powRetry = createPowRetryTimeout(req.timeout);
501
517
  try {
@@ -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.2.14";
4
+ const PLUGIN_VERSION = "0.3.1";
5
5
  //#endregion
6
6
  //#region src/constants.ts
7
7
  /**
@@ -0,0 +1,51 @@
1
+ import { i as INFRA_USER_AGENT } from "./constants-Cjw_ECs7.mjs";
2
+ import { createFetch } from "@better-fetch/fetch";
3
+ //#region src/fetch.ts
4
+ function createAPI(options, fetchOptions) {
5
+ return createFetch({
6
+ baseURL: options.apiUrl,
7
+ headers: {
8
+ "user-agent": INFRA_USER_AGENT,
9
+ "x-api-key": options.apiKey
10
+ },
11
+ timeout: options.apiTimeout,
12
+ ...fetchOptions
13
+ });
14
+ }
15
+ function createKV(options, fetchOptions) {
16
+ const headers = { "user-agent": INFRA_USER_AGENT };
17
+ if (options.apiKey) headers["x-api-key"] = options.apiKey;
18
+ return createFetch({
19
+ baseURL: options.kvUrl,
20
+ headers,
21
+ timeout: options.kvTimeout ?? 1e3,
22
+ ...fetchOptions
23
+ });
24
+ }
25
+ //#endregion
26
+ //#region src/crypto.ts
27
+ function randomBytes(length) {
28
+ const bytes = new Uint8Array(length);
29
+ crypto.getRandomValues(bytes);
30
+ return bytes;
31
+ }
32
+ function bytesToHex(bytes) {
33
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
34
+ }
35
+ async function hash(message) {
36
+ const msgBuffer = new TextEncoder().encode(message);
37
+ const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
38
+ return bytesToHex(new Uint8Array(hashBuffer));
39
+ }
40
+ /** Keyed HMAC-SHA256 for blinded fingerprints (e.g. failed-login passwords). */
41
+ async function hmacSha256Hex(key, message) {
42
+ const encoder = new TextEncoder();
43
+ const cryptoKey = await crypto.subtle.importKey("raw", encoder.encode(key), {
44
+ name: "HMAC",
45
+ hash: "SHA-256"
46
+ }, false, ["sign"]);
47
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message));
48
+ return bytesToHex(new Uint8Array(signature));
49
+ }
50
+ //#endregion
51
+ export { createAPI as a, randomBytes as i, hash as n, createKV as o, hmacSha256Hex as r, bytesToHex as t };
@@ -1,3 +1,5 @@
1
+ import { BetterAuthClientPlugin } from "better-auth";
2
+
1
3
  //#region src/dash-client.d.ts
2
4
  interface DashAuditLog {
3
5
  eventType: string;
@@ -54,38 +56,6 @@ interface DashClientOptions {
54
56
  session?: SessionLike | null;
55
57
  }) => string | undefined;
56
58
  }
57
- declare const dashClient: (options?: DashClientOptions) => {
58
- id: "dash";
59
- getActions: ($fetch: import("@better-fetch/fetch").BetterFetch) => {
60
- dash: {
61
- getAuditLogs: (input?: DashGetAuditLogsInput) => Promise<{
62
- data: null;
63
- error: {
64
- message?: string | undefined;
65
- status: number;
66
- statusText: string;
67
- };
68
- } | {
69
- data: DashAuditLogsResponse;
70
- error: null;
71
- }>;
72
- getAllAuditLogs: (input?: DashGetAllAuditLogsInput) => Promise<{
73
- data: null;
74
- error: {
75
- message?: string | undefined;
76
- status: number;
77
- statusText: string;
78
- };
79
- } | {
80
- data: DashAuditLogsResponse;
81
- error: null;
82
- }>;
83
- };
84
- };
85
- pathMethods: {
86
- "/events/audit-logs": "GET";
87
- "/events/all-audit-logs": "GET";
88
- };
89
- };
59
+ declare const dashClient: (options?: DashClientOptions) => BetterAuthClientPlugin;
90
60
  //#endregion
91
61
  export { DashGetAuditLogsInput as a, DashGetAllAuditLogsInput as i, DashAuditLogsResponse as n, dashClient as o, DashClientOptions as r, DashAuditLog as t };
package/dist/email.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-CLYqEwMV.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-Cjw_ECs7.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";