@better-auth/infra 0.4.3 → 0.4.5
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 +16 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +2 -2
- package/dist/{constants-CA5EAv5K.mjs → constants-DjM7toCN.mjs} +1 -1
- package/dist/{crypto-SmxL66Tk.mjs → crypto-BhIjjZNe.mjs} +1 -1
- package/dist/email.mjs +1 -1
- package/dist/{identify-client-options-CgijjwVT.d.mts → identify-client-options-Cim5Fztg.d.mts} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +338 -199
- package/dist/native.d.mts +1 -1
- package/dist/native.mjs +2 -2
- package/dist/{pow-retry-BWDJUT8X.mjs → pow-retry-CsInOtOL.mjs} +1 -1
- package/dist/{types-Ddk4r5x9.d.mts → types-sP9ucYj1.d.mts} +20 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.5] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Duplicate `__infra-rid` Set-Cookie** — When both `dash()` and `sentinel()` are installed, the identification cookie after-hook no longer appends the same `Set-Cookie` twice. The writer runs once per request via a shared context guard.
|
|
13
|
+
|
|
14
|
+
## [0.4.4] - 2026-08-27
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`GET /dash/config` adapter insight** — Config insights report database adapter identity and capability flags.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Sentinel PoW vs rate limits** — Proof-of-work challenges no longer consume Better Auth rate-limit budget.
|
|
23
|
+
|
|
8
24
|
## [0.4.3] - 2026-08-25
|
|
9
25
|
|
|
10
26
|
### Added
|
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-
|
|
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-
|
|
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-
|
|
1
|
+
import { n as hash, o as createKV } from "./crypto-BhIjjZNe.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-CsInOtOL.mjs";
|
|
3
3
|
import { env } from "@better-auth/core/env";
|
|
4
4
|
//#region src/sentinel/fingerprint.ts
|
|
5
5
|
function murmurhash3(str, seed = 0) {
|
package/dist/email.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-DjM7toCN.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";
|
package/dist/{identify-client-options-CgijjwVT.d.mts → identify-client-options-Cim5Fztg.d.mts}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as KvRetryOptions } from "./types-
|
|
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-
|
|
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";
|
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-
|
|
2
|
-
import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-DjM7toCN.mjs";
|
|
2
|
+
import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-BhIjjZNe.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
|
|
@@ -1113,9 +1113,14 @@ function shouldRunIdentification(ctx, getRoutes = IDENTIFICATION_GET_ROUTES) {
|
|
|
1113
1113
|
* Persist `X-Request-Id` as `__infra-rid` so redirect-flow OAuth callbacks
|
|
1114
1114
|
* (GET, no identify headers) can resolve visitor identification from KV.
|
|
1115
1115
|
* Must run in an after hook so Set-Cookie reaches the response.
|
|
1116
|
+
*
|
|
1117
|
+
* Dedupes when both `dash()` and `sentinel()` register this hook (each alone
|
|
1118
|
+
* needs it; together they would append duplicate Set-Cookie headers).
|
|
1116
1119
|
*/
|
|
1117
1120
|
function createIdentificationCookieAfterMiddleware() {
|
|
1118
1121
|
return createAuthMiddleware(async (ctx) => {
|
|
1122
|
+
if (ctx.context.infraRidCookieSet) return;
|
|
1123
|
+
ctx.context.infraRidCookieSet = true;
|
|
1119
1124
|
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
1120
1125
|
if (headerRequestId) {
|
|
1121
1126
|
ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
@@ -2316,6 +2321,9 @@ const ERROR_MESSAGES = {
|
|
|
2316
2321
|
compromised_password: "This password has been found in data breaches. Please choose a different password.",
|
|
2317
2322
|
impossible_travel: "Login blocked due to suspicious location change."
|
|
2318
2323
|
};
|
|
2324
|
+
function securityBlockMessage(reason) {
|
|
2325
|
+
return ERROR_MESSAGES[reason] || "Access denied.";
|
|
2326
|
+
}
|
|
2319
2327
|
/**
|
|
2320
2328
|
* Throw a challenge error with appropriate headers
|
|
2321
2329
|
*/
|
|
@@ -2330,56 +2338,152 @@ function throwChallengeError(challenge, reason, message = "Please complete a sec
|
|
|
2330
2338
|
};
|
|
2331
2339
|
throw error;
|
|
2332
2340
|
}
|
|
2333
|
-
/**
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2341
|
+
/**
|
|
2342
|
+
* Cooldown → consolidated security check → challenge/block/allow.
|
|
2343
|
+
* Does not emit events or throw; callers apply the result.
|
|
2344
|
+
*/
|
|
2345
|
+
async function resolveAdmission(input, securityService) {
|
|
2346
|
+
const requestId = input.identification?.requestId ?? input.requestId ?? null;
|
|
2347
|
+
if (input.visitorId || input.ip) {
|
|
2348
|
+
if (await securityService.isBlocked(input.visitorId ?? "", input.ip, requestId)) return { type: "cooldown" };
|
|
2349
|
+
}
|
|
2350
|
+
const verdict = await securityService.checkSecurity({
|
|
2351
|
+
visitorId: input.visitorId,
|
|
2352
|
+
requestId,
|
|
2353
|
+
ip: input.ip,
|
|
2354
|
+
path: input.path,
|
|
2355
|
+
identifier: input.identifier,
|
|
2356
|
+
powSolution: input.powSolution ?? void 0
|
|
2357
|
+
});
|
|
2358
|
+
if (verdict.action === "allow") return {
|
|
2359
|
+
type: "allow",
|
|
2360
|
+
powVerified: verdict.powVerified
|
|
2361
|
+
};
|
|
2337
2362
|
const reason = verdict.reason || "unknown";
|
|
2338
|
-
if (verdict.action === "challenge"
|
|
2339
|
-
|
|
2363
|
+
if (verdict.action === "challenge") {
|
|
2364
|
+
if (!input.visitorId) return {
|
|
2365
|
+
type: "allow",
|
|
2366
|
+
powVerified: verdict.powVerified
|
|
2367
|
+
};
|
|
2368
|
+
const challenge = verdict.challenge || await securityService.generateChallenge(input.visitorId, requestId);
|
|
2340
2369
|
if (!challenge?.trim()) {
|
|
2341
2370
|
logger.warn("[Sentinel] Could not generate PoW challenge (service may be unavailable). Falling back to allow.");
|
|
2342
|
-
return
|
|
2371
|
+
return {
|
|
2372
|
+
type: "allow",
|
|
2373
|
+
powVerified: verdict.powVerified
|
|
2374
|
+
};
|
|
2343
2375
|
}
|
|
2344
|
-
|
|
2376
|
+
return {
|
|
2377
|
+
type: "challenge",
|
|
2378
|
+
challenge,
|
|
2345
2379
|
reason,
|
|
2346
|
-
|
|
2347
|
-
}
|
|
2348
|
-
emitEvaluation(hookCtx, trackEvent, {
|
|
2349
|
-
identifier: checkCtx.identifier,
|
|
2350
|
-
userAgent: checkCtx.userAgent
|
|
2351
|
-
});
|
|
2352
|
-
throwChallengeError(challenge, reason);
|
|
2380
|
+
details: verdict.details
|
|
2381
|
+
};
|
|
2353
2382
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2383
|
+
return {
|
|
2384
|
+
type: "block",
|
|
2385
|
+
reason,
|
|
2386
|
+
details: verdict.details
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Record evaluation side effects and throw on deny. Returns on allow.
|
|
2391
|
+
*/
|
|
2392
|
+
function applyAdmission(admission, hookCtx, meta, trackEvent) {
|
|
2393
|
+
if (hookCtx.context.visitorId || hookCtx.context.ip) recordCheck(hookCtx, "credential_stuffing");
|
|
2394
|
+
if (admission.type === "allow") {
|
|
2395
|
+
recordCheck(hookCtx, "server_security_check");
|
|
2396
|
+
if (admission.powVerified) recordCheck(hookCtx, "pow_challenge");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (admission.type === "cooldown") {
|
|
2400
|
+
setOutcome(hookCtx, "blocked", "credential_stuffing", {
|
|
2401
|
+
phase: "cooldown_active",
|
|
2402
|
+
visitorId: hookCtx.context.untrustedVisitorId ?? void 0,
|
|
2403
|
+
confidence: 1
|
|
2404
|
+
});
|
|
2405
|
+
emitEvaluation(hookCtx, trackEvent, meta);
|
|
2406
|
+
throw new APIError(403, { message: "Too many failed attempts. Please try again later." });
|
|
2407
|
+
}
|
|
2408
|
+
if (admission.type === "challenge") {
|
|
2409
|
+
recordCheck(hookCtx, "server_security_check");
|
|
2410
|
+
setOutcome(hookCtx, "challenged", "server_security_check", {
|
|
2411
|
+
reason: admission.reason,
|
|
2412
|
+
...admission.details
|
|
2362
2413
|
});
|
|
2363
|
-
|
|
2414
|
+
emitEvaluation(hookCtx, trackEvent, meta);
|
|
2415
|
+
throwChallengeError(admission.challenge, admission.reason);
|
|
2364
2416
|
}
|
|
2417
|
+
recordCheck(hookCtx, "server_security_check");
|
|
2418
|
+
setOutcome(hookCtx, "blocked", "server_security_check", {
|
|
2419
|
+
reason: admission.reason,
|
|
2420
|
+
...admission.details
|
|
2421
|
+
});
|
|
2422
|
+
emitEvaluation(hookCtx, trackEvent, meta);
|
|
2423
|
+
throw new APIError("FORBIDDEN", { message: securityBlockMessage(admission.reason) });
|
|
2365
2424
|
}
|
|
2366
2425
|
/**
|
|
2367
|
-
*
|
|
2368
|
-
* one verdict — counts as one billable check.
|
|
2426
|
+
* Resolve + apply in one step (before-hook when rate-limit did not pre-decide).
|
|
2369
2427
|
*/
|
|
2370
2428
|
async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent, powSolution) {
|
|
2371
|
-
|
|
2372
|
-
await handleSecurityVerdict(await securityService.checkSecurity({
|
|
2429
|
+
applyAdmission(await resolveAdmission({
|
|
2373
2430
|
visitorId: checkCtx.visitorId,
|
|
2374
|
-
requestId: checkCtx.
|
|
2431
|
+
requestId: checkCtx.requestId,
|
|
2432
|
+
identification: checkCtx.identification,
|
|
2375
2433
|
ip: checkCtx.ip,
|
|
2376
2434
|
path: checkCtx.path,
|
|
2377
2435
|
identifier: checkCtx.identifier,
|
|
2378
|
-
powSolution
|
|
2379
|
-
}), hookCtx,
|
|
2436
|
+
powSolution
|
|
2437
|
+
}, securityService), hookCtx, {
|
|
2438
|
+
identifier: checkCtx.identifier,
|
|
2439
|
+
userAgent: checkCtx.userAgent
|
|
2440
|
+
}, trackEvent);
|
|
2380
2441
|
}
|
|
2381
2442
|
//#endregion
|
|
2382
2443
|
//#region src/sentinel/sentinel.ts
|
|
2444
|
+
/**
|
|
2445
|
+
* Strip basePath from a request URL for path matching.
|
|
2446
|
+
* Local copy of BA's normalizePathname — `@better-auth/core/utils/url` is not
|
|
2447
|
+
* exported on 1.4; `@better-auth/core/utils` barrel is gone on 1.7.
|
|
2448
|
+
*/
|
|
2449
|
+
function normalizeAuthPath(requestUrl, basePath) {
|
|
2450
|
+
try {
|
|
2451
|
+
const pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
|
|
2452
|
+
const base = basePath.replace(/\/+$/, "") || "";
|
|
2453
|
+
if (!base || base === "/") return pathname;
|
|
2454
|
+
if (pathname === base) return "/";
|
|
2455
|
+
if (pathname.startsWith(`${base}/`)) return pathname.slice(base.length).replace(/\/+$/, "") || "/";
|
|
2456
|
+
return pathname;
|
|
2457
|
+
} catch {
|
|
2458
|
+
return "/";
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
const SECURITY_PROTECTED_ROUTES = [
|
|
2462
|
+
routes.SIGN_IN_EMAIL,
|
|
2463
|
+
routes.SIGN_IN_USERNAME,
|
|
2464
|
+
routes.SIGN_IN_EMAIL_OTP,
|
|
2465
|
+
routes.SIGN_IN_SOCIAL,
|
|
2466
|
+
routes.SIGN_IN_PASSKEY,
|
|
2467
|
+
routes.SIGN_IN_MAGIC_LINK,
|
|
2468
|
+
routes.SIGN_IN_SSO,
|
|
2469
|
+
routes.SIGN_IN_ANONYMOUS,
|
|
2470
|
+
routes.SIGN_UP_EMAIL,
|
|
2471
|
+
routes.FORGET_PASSWORD,
|
|
2472
|
+
routes.REQUEST_PASSWORD_RESET,
|
|
2473
|
+
routes.RESET_PASSWORD,
|
|
2474
|
+
routes.TWO_FACTOR_VERIFY_TOTP,
|
|
2475
|
+
routes.TWO_FACTOR_VERIFY_BACKUP,
|
|
2476
|
+
routes.TWO_FACTOR_VERIFY_OTP,
|
|
2477
|
+
routes.EMAIL_OTP_SEND,
|
|
2478
|
+
routes.PHONE_SEND_OTP,
|
|
2479
|
+
routes.MAGIC_LINK_VERIFY,
|
|
2480
|
+
routes.ORG_CREATE,
|
|
2481
|
+
routes.CHANGE_EMAIL,
|
|
2482
|
+
routes.CHANGE_PASSWORD,
|
|
2483
|
+
routes.SET_PASSWORD,
|
|
2484
|
+
routes.LINK_SOCIAL,
|
|
2485
|
+
routes.PASSKEY_ADD
|
|
2486
|
+
];
|
|
2383
2487
|
const sentinel = (options) => {
|
|
2384
2488
|
const opts = resolveSentinelOptions(options);
|
|
2385
2489
|
const $api = createAPI(opts);
|
|
@@ -2402,6 +2506,12 @@ const sentinel = (options) => {
|
|
|
2402
2506
|
return typeof err.body === "object" && err.body !== null && "code" in err.body && err.body.code === STALE_ACCOUNT_BLOCK_ERROR.code;
|
|
2403
2507
|
}
|
|
2404
2508
|
let activityTrackingEnabled = false;
|
|
2509
|
+
/**
|
|
2510
|
+
* Admission from rateLimit.customRules for this Request. Reused by the
|
|
2511
|
+
* before-hook and by BA 1.5's second customRules pass on onResponse.
|
|
2512
|
+
* Do not delete — response-phase must still hit. GC cleans up with Request.
|
|
2513
|
+
*/
|
|
2514
|
+
const rateLimitAdmission = /* @__PURE__ */ new WeakMap();
|
|
2405
2515
|
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
2516
|
const securityService = createSecurityClient(opts, $apiThrowing, opts.security || {}, (event) => {
|
|
2407
2517
|
trackEvent({
|
|
@@ -2432,114 +2542,165 @@ const sentinel = (options) => {
|
|
|
2432
2542
|
id: "sentinel",
|
|
2433
2543
|
init(ctx) {
|
|
2434
2544
|
activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2545
|
+
const basePath = new URL(ctx.baseURL).pathname;
|
|
2546
|
+
const ipHeaders = ctx.options.advanced?.ipAddress?.ipAddressHeaders || null;
|
|
2547
|
+
const disableIpTracking = ctx.options.advanced?.ipAddress?.disableIpTracking === true;
|
|
2548
|
+
/**
|
|
2549
|
+
* After any user customRule resolves, run Sentinel admission so PoW
|
|
2550
|
+
* challenges skip BA consume (protocol round-trip) even when the app
|
|
2551
|
+
* owns path rules or `**`. Block/cooldown still consume.
|
|
2552
|
+
*/
|
|
2553
|
+
const applyCoordination = async (request, afterUser) => {
|
|
2554
|
+
const path = normalizeAuthPath(request.url, basePath);
|
|
2555
|
+
if (request.method === "GET" || isDashRoute(path) || !matchesAnyRoute(path, SECURITY_PROTECTED_ROUTES)) return afterUser;
|
|
2556
|
+
try {
|
|
2557
|
+
const cached = rateLimitAdmission.get(request);
|
|
2558
|
+
if (cached) return cached.type === "challenge" ? false : afterUser;
|
|
2559
|
+
const requestId = request.headers.get("X-Request-Id");
|
|
2560
|
+
const identification = requestId ? await getIdentification(requestId, $kv, opts.kvOptions.retry) : null;
|
|
2561
|
+
const ip = disableIpTracking ? identification?.ip ?? null : identification?.ip ?? resolveClientIpFromHeaders(request.headers, ipHeaders) ?? null;
|
|
2562
|
+
const admission = await resolveAdmission({
|
|
2563
|
+
visitorId: identification?.visitorId ?? null,
|
|
2564
|
+
requestId,
|
|
2565
|
+
identification,
|
|
2566
|
+
ip,
|
|
2567
|
+
path,
|
|
2568
|
+
powSolution: request.headers.get("X-PoW-Solution")
|
|
2569
|
+
}, securityService);
|
|
2570
|
+
rateLimitAdmission.set(request, admission);
|
|
2571
|
+
if (admission.type === "challenge") return false;
|
|
2572
|
+
return afterUser;
|
|
2573
|
+
} catch {
|
|
2574
|
+
return afterUser;
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
const wrapCustomRule = (userRule) => {
|
|
2578
|
+
return async (request, current) => {
|
|
2579
|
+
const userResult = typeof userRule === "function" ? await userRule(request, current) : userRule;
|
|
2580
|
+
return applyCoordination(request, userResult === false ? false : userResult || current);
|
|
2581
|
+
};
|
|
2582
|
+
};
|
|
2583
|
+
const customRules = {};
|
|
2584
|
+
for (const [key, rule] of Object.entries(ctx.rateLimit?.customRules ?? {})) {
|
|
2585
|
+
if (rule === void 0) continue;
|
|
2586
|
+
customRules[key] = wrapCustomRule(rule);
|
|
2587
|
+
}
|
|
2588
|
+
if (!Object.hasOwn(customRules, "**")) customRules["**"] = async (request, current) => applyCoordination(request, current);
|
|
2589
|
+
return {
|
|
2590
|
+
context: { rateLimit: {
|
|
2591
|
+
...ctx.rateLimit,
|
|
2592
|
+
customRules
|
|
2593
|
+
} },
|
|
2594
|
+
options: {
|
|
2595
|
+
emailValidation: opts.security?.emailValidation,
|
|
2596
|
+
emailNormalization: opts.security?.emailNormalization,
|
|
2597
|
+
rateLimit: { customRules },
|
|
2598
|
+
databaseHooks: {
|
|
2599
|
+
user: {
|
|
2600
|
+
create: {
|
|
2601
|
+
async before(user, ctx) {
|
|
2602
|
+
if (!ctx) return;
|
|
2603
|
+
if (isDashRoute(ctx.path)) {
|
|
2604
|
+
if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
|
|
2605
|
+
...user,
|
|
2606
|
+
email: normalizeEmail(user.email, ctx.context)
|
|
2607
|
+
} };
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const visitorId = ctx.context.visitorId;
|
|
2611
|
+
if (opts.security?.freeTrialAbuse?.enabled) {
|
|
2612
|
+
if (!visitorId) {
|
|
2613
|
+
recordCheck(ctx, "free_trial_abuse");
|
|
2614
|
+
setOutcome(ctx, "blocked", "free_trial_abuse", { reason: "identification_required" });
|
|
2615
|
+
emitEvaluation(ctx, trackEvent);
|
|
2616
|
+
throw new APIError("FORBIDDEN", { message: "Account creation is not allowed without device identification." });
|
|
2617
|
+
}
|
|
2618
|
+
recordCheck(ctx, "free_trial_abuse");
|
|
2619
|
+
const reservationId = crypto.randomUUID();
|
|
2620
|
+
ctx.context.freeTrialReservationId = reservationId;
|
|
2621
|
+
const abuseCheck = await securityService.reserveFreeTrialSignup(visitorId, reservationId, ctx.context.requestId ?? null);
|
|
2622
|
+
if (abuseCheck.isAbuse && abuseCheck.action === "block") {
|
|
2623
|
+
setOutcome(ctx, "blocked", "free_trial_abuse", {
|
|
2624
|
+
accountCount: abuseCheck.accountCount,
|
|
2625
|
+
maxAccounts: abuseCheck.maxAccounts
|
|
2626
|
+
});
|
|
2627
|
+
emitEvaluation(ctx, trackEvent);
|
|
2628
|
+
throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2444
2631
|
if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
|
|
2445
2632
|
...user,
|
|
2446
2633
|
email: normalizeEmail(user.email, ctx.context)
|
|
2447
2634
|
} };
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
-
}
|
|
2635
|
+
},
|
|
2636
|
+
async after(user, ctx) {
|
|
2637
|
+
if (!ctx || isDashRoute(ctx.path)) return;
|
|
2638
|
+
const visitorId = ctx.context.visitorId;
|
|
2639
|
+
const reservationId = ctx.context.freeTrialReservationId;
|
|
2640
|
+
if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
|
|
2470
2641
|
}
|
|
2642
|
+
},
|
|
2643
|
+
update: { async before(user, ctx) {
|
|
2644
|
+
if (!ctx) return;
|
|
2471
2645
|
if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
|
|
2472
2646
|
...user,
|
|
2473
2647
|
email: normalizeEmail(user.email, ctx.context)
|
|
2474
2648
|
} };
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2649
|
+
} }
|
|
2650
|
+
},
|
|
2651
|
+
session: { create: {
|
|
2652
|
+
async before(session, ctx) {
|
|
2477
2653
|
if (!ctx || isDashRoute(ctx.path)) return;
|
|
2478
2654
|
const visitorId = ctx.context.visitorId;
|
|
2479
|
-
const
|
|
2480
|
-
if (
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
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.");
|
|
2655
|
+
const identification = ctx.context.identification;
|
|
2656
|
+
if (session.userId && identification?.location && visitorId) {
|
|
2657
|
+
recordCheck(ctx, "impossible_travel");
|
|
2658
|
+
const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
|
|
2659
|
+
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution, identification.requestId);
|
|
2660
|
+
if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
|
|
2661
|
+
if (travelCheck?.isImpossible) {
|
|
2662
|
+
if (travelCheck.action === "block") {
|
|
2663
|
+
setOutcome(ctx, "blocked", "impossible_travel", {
|
|
2664
|
+
from: travelCheck.from,
|
|
2665
|
+
to: travelCheck.to,
|
|
2666
|
+
distance: travelCheck.distance,
|
|
2667
|
+
speedRequired: travelCheck.speedRequired
|
|
2668
|
+
});
|
|
2669
|
+
emitEvaluation(ctx, trackEvent);
|
|
2670
|
+
throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
|
|
2671
|
+
}
|
|
2672
|
+
if (travelCheck.action === "challenge" && travelCheck.challenge) {
|
|
2673
|
+
setOutcome(ctx, "challenged", "impossible_travel", {
|
|
2674
|
+
from: travelCheck.from,
|
|
2675
|
+
to: travelCheck.to,
|
|
2676
|
+
distance: travelCheck.distance,
|
|
2677
|
+
speedRequired: travelCheck.speedRequired
|
|
2678
|
+
});
|
|
2679
|
+
emitEvaluation(ctx, trackEvent);
|
|
2680
|
+
throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
|
|
2681
|
+
}
|
|
2521
2682
|
}
|
|
2522
2683
|
}
|
|
2684
|
+
},
|
|
2685
|
+
async after(session, ctx) {
|
|
2686
|
+
if (!ctx || !session.userId || isDashRoute(ctx.path)) return;
|
|
2687
|
+
const visitorId = ctx.context.visitorId;
|
|
2688
|
+
const identification = ctx.context.identification;
|
|
2689
|
+
let user = null;
|
|
2690
|
+
try {
|
|
2691
|
+
user = await getUserById(session.userId, ctx, { includeLastActiveAt: activityTrackingEnabled });
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
|
|
2694
|
+
}
|
|
2695
|
+
if (visitorId) {
|
|
2696
|
+
if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
|
|
2697
|
+
}
|
|
2698
|
+
if (opts.security?.impossibleTravel?.enabled && identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location, identification.ip));
|
|
2523
2699
|
}
|
|
2524
|
-
}
|
|
2525
|
-
|
|
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
|
-
} }
|
|
2700
|
+
} }
|
|
2701
|
+
}
|
|
2541
2702
|
}
|
|
2542
|
-
}
|
|
2703
|
+
};
|
|
2543
2704
|
},
|
|
2544
2705
|
hooks: {
|
|
2545
2706
|
before: [
|
|
@@ -2558,64 +2719,25 @@ const sentinel = (options) => {
|
|
|
2558
2719
|
const visitorId = ctx.context.visitorId;
|
|
2559
2720
|
const identification = ctx.context.identification;
|
|
2560
2721
|
const ip = ctx.context.ip;
|
|
2561
|
-
|
|
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;
|
|
2722
|
+
if (!matchesAnyRoute(ctx.path, SECURITY_PROTECTED_ROUTES)) return;
|
|
2593
2723
|
const requestBody = ctx.body;
|
|
2594
2724
|
const identifier = requestBody?.email || requestBody?.phone || requestBody?.username || void 0;
|
|
2595
2725
|
const powSolution = ctx.headers?.get?.("X-PoW-Solution");
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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,
|
|
2726
|
+
const userAgent = ctx.headers?.get?.("user-agent") || "";
|
|
2727
|
+
const meta = {
|
|
2728
|
+
identifier,
|
|
2729
|
+
userAgent
|
|
2730
|
+
};
|
|
2731
|
+
const cached = ctx.request ? rateLimitAdmission.get(ctx.request) : void 0;
|
|
2732
|
+
if (cached) applyAdmission(cached, ctx, meta, trackEvent);
|
|
2733
|
+
else await runSecurityChecks(ctx, {
|
|
2734
|
+
path: ctx.path ?? "",
|
|
2613
2735
|
identifier,
|
|
2614
2736
|
visitorId,
|
|
2615
2737
|
requestId: ctx.context.requestId ?? null,
|
|
2616
2738
|
identification,
|
|
2617
|
-
ip
|
|
2618
|
-
userAgent
|
|
2739
|
+
ip,
|
|
2740
|
+
userAgent
|
|
2619
2741
|
}, securityService, trackEvent, powSolution);
|
|
2620
2742
|
if (matchesAnyRoute(ctx.path, [
|
|
2621
2743
|
routes.SIGN_UP_EMAIL,
|
|
@@ -2633,7 +2755,7 @@ const sentinel = (options) => {
|
|
|
2633
2755
|
setOutcome(ctx, "blocked", "compromised_password", { breachCount: compromisedResult.breachCount });
|
|
2634
2756
|
emitEvaluation(ctx, trackEvent, {
|
|
2635
2757
|
identifier,
|
|
2636
|
-
userAgent
|
|
2758
|
+
userAgent
|
|
2637
2759
|
});
|
|
2638
2760
|
throw new APIError("BAD_REQUEST", {
|
|
2639
2761
|
message: "This password has been found in data breaches. Please choose a different password.",
|
|
@@ -4280,6 +4402,23 @@ const getConfig = (options) => {
|
|
|
4280
4402
|
emailVerification: { sendVerificationEmailEnabled: !!ctx.context.options.emailVerification?.sendVerificationEmail },
|
|
4281
4403
|
insights: {
|
|
4282
4404
|
hasDatabase: !!ctx.context.options.database,
|
|
4405
|
+
adapter: (() => {
|
|
4406
|
+
const adapter = ctx.context.adapter;
|
|
4407
|
+
if (!adapter) return null;
|
|
4408
|
+
const adapterConfig = adapter.options?.adapterConfig;
|
|
4409
|
+
return {
|
|
4410
|
+
id: adapter.id ?? adapterConfig?.adapterId ?? null,
|
|
4411
|
+
name: adapterConfig?.adapterName ?? adapter.id ?? adapterConfig?.adapterId ?? null,
|
|
4412
|
+
usePlural: adapterConfig?.usePlural ?? false,
|
|
4413
|
+
supportsTransactions: typeof adapterConfig?.transaction === "function",
|
|
4414
|
+
supportsJSON: typeof adapterConfig?.supportsJSON === "boolean" ? adapterConfig.supportsJSON : null,
|
|
4415
|
+
supportsDates: typeof adapterConfig?.supportsDates === "boolean" ? adapterConfig.supportsDates : null,
|
|
4416
|
+
supportsBooleans: typeof adapterConfig?.supportsBooleans === "boolean" ? adapterConfig.supportsBooleans : null,
|
|
4417
|
+
supportsArrays: typeof adapterConfig?.supportsArrays === "boolean" ? adapterConfig.supportsArrays : null,
|
|
4418
|
+
supportsNumericIds: typeof adapterConfig?.supportsNumericIds === "boolean" ? adapterConfig.supportsNumericIds : null,
|
|
4419
|
+
supportsUUIDs: typeof adapterConfig?.supportsUUIDs === "boolean" ? adapterConfig.supportsUUIDs : null
|
|
4420
|
+
};
|
|
4421
|
+
})(),
|
|
4283
4422
|
cookies: advancedOptions?.cookies ? Object.entries(advancedOptions.cookies).map(([key, value]) => ({
|
|
4284
4423
|
key,
|
|
4285
4424
|
name: value?.name,
|
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-
|
|
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-
|
|
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-
|
|
1
|
+
import { o as createKV, t as bytesToHex } from "./crypto-BhIjjZNe.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-CsInOtOL.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-
|
|
1
|
+
import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-BhIjjZNe.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.
|
|
3
|
+
"version": "0.4.5",
|
|
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": "^
|
|
77
|
+
"typescript": "^6.0.3",
|
|
78
78
|
"zod": "4.3.6"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|