@better-auth/infra 0.3.7 → 0.4.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,36 @@ 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.1] - 2026-08-19
9
+
10
+ ### Fixed
11
+
12
+ - **React Native / Hermes crypto** — SHA-256 (including Sentinel PoW challenge solve/verify) no longer requires `crypto.subtle`. Prefers Web Crypto when present; otherwise falls back to `@noble/hashes`.
13
+
14
+ ## [0.4.0] - 2026-08-14
15
+
16
+
17
+ ### Added
18
+
19
+ - **Managed SCIM directory sync** — Opt-in `dash({ managedDirectorySync: { enabled: true } })` control plane for Better Auth 1.7+ managed connections. Adds dashboard lifecycle endpoints to create, list, get, rotate credentials, revoke credentials, list events, unpair, and decommission organization directories, plus schema for `directorySyncConnection` / membership provenance, optional SSO pairing hooks, and SCIM → organization membership projection.
20
+ - **Issuer-scoped accounts** — Dashboard user create, set-password, and account unlink paths work with Better Auth 1.7 issuer-scoped account identity (`issuer` + subject) while remaining compatible with pre-issuer schemas.
21
+ - **Configurable API / KV client options** — Shared `apiOptions` / `kvOptions` (timeout and KV retry/backoff) on `dash` and `sentinel`. Legacy `apiTimeout` / `kvTimeout` remain supported as deprecated aliases.
22
+ - **`GET /dash/config` joins insight** — Config insights report `hasJoinsEnabled` from `advanced.database.joins` (and pre-1.7 `experimental.joins` when applicable).
23
+
24
+ ### Changed
25
+
26
+ - **Better Auth 1.7 RC compatibility** — Support matrix and runtime paths updated through 1.7 rc.4 (issuer-scoped accounts, session lifecycle hooks, SSO domain verification contracts).
27
+ - **SSO domain verification TXT records** — Dashboard domain verification uses the SSO plugin `tokenPrefix` and the `_` + prefix + provider id TXT name shape expected by Better Auth domain verification.
28
+ - **Session lifecycle event tracking** — Sign-in / session-created / impersonation tracking deferred correctly when session creation runs before the user is available on the request context (Better Auth 1.7 rc.2+).
29
+ - **Directory list mode split** — Legacy pre-1.7 SCIM provider listing stays on the legacy surface; managed connection APIs only run when managed directory sync is enabled.
30
+ - **Account unlink body** — `unlinkAccount` requires `accountId` (local account row id) together with `providerId`.
31
+
32
+ ### Fixed
33
+
34
+ - **SSO domain verify errors** — `verify-domain` maps provider-changed, already-verified, and missing-DNS cases to stable dashboard responses instead of leaking raw API errors.
35
+ - **Sentinel stale-account handling** — Stale-user blocks clear the session cookie and surface a consistent `STALE_ACCOUNT` error after the endpoint runs, not only inside DB hooks.
36
+ - **SCIM token redaction** — Sensitive SCIM credential fields stay redacted in dash config / plugin option dumps.
37
+
8
38
  ## [0.3.7] - 2026-07-20
9
39
 
10
40
  ### Added
@@ -144,6 +174,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
144
174
 
145
175
  [0.3.7]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.6...infra@v0.3.7
146
176
  [0.3.6]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.5...infra@v0.3.6
177
+ [0.4.0]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.7...infra@v0.4.0
147
178
  [0.3.5]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.4...infra@v0.3.5
148
179
  [0.3.4]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.3...infra@v0.3.4
149
180
  [0.3.3]: https://github.com/better-auth/infrastructure/compare/infra@v0.3.2...infra@v0.3.3
package/README.md CHANGED
@@ -80,6 +80,10 @@ Use this plugin for your React Native / Expo app. This plugin is designed to wor
80
80
  pnpm add react-native @react-native-async-storage/async-storage
81
81
  # Optional, for richer device metadata in the identify payload:
82
82
  pnpm add expo-constants expo-device
83
+ # Optional, secure visitor-id RNG (Expo):
84
+ pnpm add expo-crypto
85
+ # Or plain React Native — first import in the app entry:
86
+ # import "react-native-get-random-values";
83
87
  ```
84
88
 
85
89
  `@react-native-async-storage/async-storage` is optional; if it is not installed, a session-only in-memory visitor id is used. For production, install it or pass `storage` (for example secure storage).
@@ -104,7 +108,9 @@ export const authClient = createAuthClient({
104
108
 
105
109
  ### Options (`sentinelNativeClient`)
106
110
 
107
- - `identifyUrl?: string` — KV identify endpoint base (defaults to `BETTER_AUTH_KV_URL` from env, then `https://kv.better-auth.com`).
111
+ - `identifyUrl?: string` — identify endpoint base (defaults to `BETTER_AUTH_KV_URL` from env, then `https://kv.better-auth.com`).
112
+ - `identifyOptions?: { timeout?: number; retry?: { attempts?: number; baseDelay?: number; maxDelay?: number } }` — identify HTTP timeout (ms, default `1000`) and POST retry (defaults `2` / `400` / `600`).
113
+ - `kvTimeout?: number` — **deprecated**, use `identifyOptions.timeout`.
108
114
  - `autoSolveChallenge?: boolean` — When `true` (default), `423` responses that include `X-PoW-Challenge` are solved and the request is retried once with `X-PoW-Solution`.
109
115
  - `onChallengeReceived?` / `onChallengeSolved?` / `onChallengeFailed?` — PoW lifecycle hooks (reason string, solve time in ms, or error).
110
116
  - `storage?: { getItem, setItem }` — Async key/value storage for a stable per-install visitor id (recommended for production).
@@ -270,7 +276,12 @@ The `dash()` plugin registers these event endpoints:
270
276
  - `apiUrl?: string`
271
277
  - `kvUrl?: string`
272
278
  - `apiKey?: string`
279
+ - `apiOptions?: { timeout?: number }` (ms, default `3000`)
280
+ - `kvOptions?: { timeout?: number; retry?: { attempts?: number; baseDelay?: number; maxDelay?: number } }` — KV timeout default `1000`; retry defaults `2` / `400` / `600`
281
+ - `apiTimeout?: number` — **deprecated**, use `apiOptions.timeout`
282
+ - `kvTimeout?: number` — **deprecated**, use `kvOptions.timeout`
273
283
  - `activityTracking?: { enabled?: boolean; updateInterval?: number }`
284
+ - `managedDirectorySync?: { enabled?: boolean; ssoPairing?: boolean; membershipProjection?: { enabled?: boolean; role?: string } }` — opt into the 1.7+ managed directory-sync control plane (schema + reservation APIs). `enabled` alone turns on reservation tables/APIs. When enabled, dash `init` can also wire sibling-plugin callbacks: `ssoPairing` installs SSO `resolveUser` / `guardProviderMutation` (needed only for paired directories; requires `sso({})` or richer options — bare `sso()` leaves `options` undefined), and `membershipProjection` installs SCIM `projection.reconcileUser` for org membership sync. Defaults: `enabled: false`, `ssoPairing: true`, `membershipProjection.enabled: true`, `membershipProjection.role: "member"`. Required with `scim({ managedConnections })` for managed mode; does not affect or replace legacy pre-1.7 SCIM directory sync.
274
285
 
275
286
  `dash()` no longer accepts sentinel security config.
276
287
 
@@ -279,6 +290,10 @@ The `dash()` plugin registers these event endpoints:
279
290
  - `apiUrl?: string`
280
291
  - `kvUrl?: string`
281
292
  - `apiKey?: string`
293
+ - `apiOptions?: { timeout?: number }` — same as `DashOptions`
294
+ - `kvOptions?: { timeout?: number; retry?: { attempts?: number; baseDelay?: number; maxDelay?: number } }` — same as `DashOptions`
295
+ - `apiTimeout?: number` — **deprecated**, use `apiOptions.timeout`
296
+ - `kvTimeout?: number` — **deprecated**, use `kvOptions.timeout`
282
297
  - `security?: SecurityOptions`
283
298
 
284
299
  All security configuration now belongs in `sentinel()`.
package/dist/client.d.mts CHANGED
@@ -1,5 +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-DXuu2ctl.mjs";
2
-
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";
3
2
  //#region src/sentinel/client.d.ts
4
3
  interface SentinelClientOptions {
5
4
  /**
@@ -8,8 +7,13 @@ interface SentinelClientOptions {
8
7
  */
9
8
  identifyUrl?: string;
10
9
  /**
11
- * Timeout for KV identify and related HTTP requests (milliseconds).
10
+ * Identify HTTP client options.
11
+ */
12
+ identifyOptions?: IdentifyClientOptions;
13
+ /**
14
+ * Timeout for identify and related HTTP requests (milliseconds).
12
15
  * @default 1000
16
+ * @deprecated Use `identifyOptions.timeout` instead.
13
17
  */
14
18
  kvTimeout?: number;
15
19
  /**
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as hash, o as createKV } from "./crypto--3ycICW4.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-Dy1D-TKx.mjs";
1
+ import { n as hash, o as createKV } from "./crypto-CV91nSbp.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-7sPi32Ow.mjs";
3
3
  import { env } from "@better-auth/core/env";
4
4
  //#region src/sentinel/fingerprint.ts
5
5
  function murmurhash3(str, seed = 0) {
@@ -325,7 +325,8 @@ async function generateVisitorId(components) {
325
325
  fonts: components.fonts,
326
326
  maxTouchPoints: components.maxTouchPoints
327
327
  };
328
- return (await hash(JSON.stringify(stableData))).slice(0, 20);
328
+ const dataString = JSON.stringify(stableData);
329
+ return (await hash(dataString)).slice(0, 20);
329
330
  }
330
331
  function calculateConfidence(components) {
331
332
  const weights = {
@@ -435,25 +436,16 @@ function appendIdentification(headers, fingerprint) {
435
436
  const MAX_POW_CHALLENGE_ROUNDS = 2;
436
437
  const sentinelClient = (options) => {
437
438
  const autoSolve = options?.autoSolveChallenge !== false;
439
+ const identifyUrl = resolveSentinelClientIdentifyUrl({
440
+ identifyUrl: options?.identifyUrl,
441
+ envKvUrl: env.BETTER_AUTH_KV_URL
442
+ });
438
443
  const $kv = createKV({
439
- kvUrl: resolveSentinelClientIdentifyUrl({
440
- identifyUrl: options?.identifyUrl,
441
- envKvUrl: env.BETTER_AUTH_KV_URL
442
- }),
443
- kvTimeout: options?.kvTimeout
444
+ kvUrl: identifyUrl,
445
+ timeout: options?.identifyOptions?.timeout ?? options?.kvTimeout
444
446
  }, {
445
447
  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
- }
448
+ retry: resolveIdentifyClientRetry(options?.identifyOptions?.retry)
457
449
  });
458
450
  if (typeof window !== "undefined") {
459
451
  const scheduleIdentify = () => {
@@ -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.3.7";
4
+ const PLUGIN_VERSION = "0.4.1";
5
5
  //#endregion
6
6
  //#region src/constants.ts
7
7
  /**
@@ -1,5 +1,6 @@
1
- import { i as INFRA_USER_AGENT } from "./constants-AfApXLhx.mjs";
1
+ import { i as INFRA_USER_AGENT } from "./constants-CtvGRrlI.mjs";
2
2
  import { createFetch } from "@better-fetch/fetch";
3
+ import { sha256 } from "@noble/hashes/sha2.js";
3
4
  //#region src/fetch.ts
4
5
  function createAPI(options, fetchOptions) {
5
6
  return createFetch({
@@ -8,7 +9,7 @@ function createAPI(options, fetchOptions) {
8
9
  "user-agent": INFRA_USER_AGENT,
9
10
  "x-api-key": options.apiKey
10
11
  },
11
- timeout: options.apiTimeout,
12
+ timeout: options.apiOptions.timeout,
12
13
  ...fetchOptions
13
14
  });
14
15
  }
@@ -18,7 +19,7 @@ function createKV(options, fetchOptions) {
18
19
  return createFetch({
19
20
  baseURL: options.kvUrl,
20
21
  headers,
21
- timeout: options.kvTimeout ?? 1e3,
22
+ timeout: options.timeout ?? 1e3,
22
23
  ...fetchOptions
23
24
  });
24
25
  }
@@ -32,19 +33,33 @@ function randomBytes(length) {
32
33
  function bytesToHex(bytes) {
33
34
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
34
35
  }
36
+ function getSubtle() {
37
+ try {
38
+ const c = globalThis.crypto;
39
+ if (c && typeof c.subtle === "object" && c.subtle != null) return c.subtle;
40
+ } catch {}
41
+ return null;
42
+ }
43
+ /** SHA-256 hex digest. Prefers Web Crypto; falls back to `@noble/hashes`. */
35
44
  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));
45
+ const data = new TextEncoder().encode(message);
46
+ const subtle = getSubtle();
47
+ if (subtle) {
48
+ const hashBuffer = await subtle.digest("SHA-256", data);
49
+ return bytesToHex(new Uint8Array(hashBuffer));
50
+ }
51
+ return bytesToHex(sha256(data));
39
52
  }
40
53
  /** Keyed HMAC-SHA256 for blinded fingerprints (e.g. failed-login passwords). */
41
54
  async function hmacSha256Hex(key, message) {
42
55
  const encoder = new TextEncoder();
43
- const cryptoKey = await crypto.subtle.importKey("raw", encoder.encode(key), {
56
+ const subtle = getSubtle();
57
+ if (!subtle) throw new Error("[@better-auth/infra] crypto.subtle is required for HMAC-SHA256");
58
+ const cryptoKey = await subtle.importKey("raw", encoder.encode(key), {
44
59
  name: "HMAC",
45
60
  hash: "SHA-256"
46
61
  }, false, ["sign"]);
47
- const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message));
62
+ const signature = await subtle.sign("HMAC", cryptoKey, encoder.encode(message));
48
63
  return bytesToHex(new Uint8Array(signature));
49
64
  }
50
65
  //#endregion
package/dist/email.d.mts CHANGED
@@ -146,9 +146,20 @@ interface SendEmailResult {
146
146
  interface EmailConfig {
147
147
  apiKey?: string;
148
148
  apiUrl?: string;
149
+ /**
150
+ * Dash API HTTP client options.
151
+ */
152
+ apiOptions?: {
153
+ /**
154
+ * Timeout for Dash email API HTTP requests (milliseconds).
155
+ * @default 3000
156
+ */
157
+ timeout?: number;
158
+ };
149
159
  /**
150
160
  * Timeout for Dash email API HTTP requests (milliseconds).
151
161
  * @default 3000
162
+ * @deprecated Use `apiOptions.timeout` instead.
152
163
  */
153
164
  apiTimeout?: number;
154
165
  }
package/dist/email.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-AfApXLhx.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-CtvGRrlI.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";
@@ -41,7 +41,7 @@ function createEmailSender(config) {
41
41
  "user-agent": INFRA_USER_AGENT,
42
42
  Authorization: `Bearer ${apiKey}`
43
43
  },
44
- timeout: config?.apiTimeout ?? 3e3
44
+ timeout: config?.apiOptions?.timeout ?? config?.apiTimeout ?? 3e3
45
45
  });
46
46
  /**
47
47
  * Send an email using a template from Better Auth Infra
@@ -1,6 +1,6 @@
1
+ import { m as KvRetryOptions } from "./types-Ddk4r5x9.mjs";
1
2
  import { BetterAuthClientOptions, ClientStore } from "better-auth";
2
3
  import { BetterFetch } from "@better-fetch/fetch";
3
-
4
4
  //#region src/dash-client.d.ts
5
5
  interface DashAuditLog {
6
6
  eventType: string;
@@ -91,4 +91,18 @@ declare const dashClient: (options?: DashClientOptions) => {
91
91
  };
92
92
  };
93
93
  //#endregion
94
- export { DashGetAuditLogsInput as a, DashGetAllAuditLogsInput as i, DashAuditLogsResponse as n, dashClient as o, DashClientOptions as r, DashAuditLog as t };
94
+ //#region src/sentinel/identify-client-options.d.ts
95
+ type IdentifyClientOptions = {
96
+ /**
97
+ * Timeout for identify and related HTTP requests (milliseconds).
98
+ * @default 1000
99
+ */
100
+ timeout?: number;
101
+ /**
102
+ * Retry/backoff for identify POST requests.
103
+ * @default { attempts: 2, baseDelay: 400, maxDelay: 600 }
104
+ */
105
+ retry?: KvRetryOptions;
106
+ };
107
+ //#endregion
108
+ export { DashGetAllAuditLogsInput as a, DashClientOptions as i, DashAuditLog as n, DashGetAuditLogsInput as o, DashAuditLogsResponse as r, dashClient as s, IdentifyClientOptions as t };