@better-auth/infra 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.mts +7 -2
- package/dist/client.mjs +34 -20
- package/dist/{constants-DdWGfvz1.mjs → constants-CvriWQVc.mjs} +6 -2
- package/dist/{dash-client-hJHp7l_X.d.mts → dash-client-B6U89e1S.d.mts} +22 -1
- package/dist/email.d.mts +5 -0
- package/dist/email.mjs +33 -31
- package/dist/fetch-DiAhoiKA.mjs +26 -0
- package/dist/index.d.mts +70 -9
- package/dist/index.mjs +607 -461
- package/dist/native.d.mts +7 -2
- package/dist/native.mjs +28 -15
- package/dist/{pow-BUuN_EKw.mjs → pow-retry-BTL4g3RP.mjs} +56 -36
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { n as INFRA_API_URL, r as INFRA_KV_URL } from "./constants-CvriWQVc.mjs";
|
|
2
|
+
import { n as createKV, t as createAPI } from "./fetch-DiAhoiKA.mjs";
|
|
2
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
3
4
|
import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
|
|
4
5
|
import { env } from "@better-auth/core/env";
|
|
5
6
|
import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
|
|
6
7
|
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
7
|
-
import {
|
|
8
|
+
import { createFetch } from "@better-fetch/fetch";
|
|
8
9
|
import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
|
|
9
10
|
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
10
11
|
import z$1, { z } from "zod";
|
|
@@ -14,7 +15,9 @@ function resolveConnectionOptions(options) {
|
|
|
14
15
|
return {
|
|
15
16
|
apiUrl: options?.apiUrl || INFRA_API_URL,
|
|
16
17
|
kvUrl: options?.kvUrl || INFRA_KV_URL,
|
|
17
|
-
apiKey: options?.apiKey || env.BETTER_AUTH_API_KEY || ""
|
|
18
|
+
apiKey: options?.apiKey || env.BETTER_AUTH_API_KEY || "",
|
|
19
|
+
apiTimeout: options?.apiTimeout ?? 3e3,
|
|
20
|
+
kvTimeout: options?.kvTimeout ?? 1e3
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
function resolveDashOptions(options) {
|
|
@@ -96,6 +99,8 @@ const routes = {
|
|
|
96
99
|
EMAIL_OTP_SEND: "/email-otp/send-verification-otp",
|
|
97
100
|
PHONE_SEND_OTP: "/phone-number/send-otp",
|
|
98
101
|
PHONE_VERIFY_OTP: "/phone-number/verify-phone-number",
|
|
102
|
+
DEVICE_TOKEN: "/device/token",
|
|
103
|
+
SIWE_VERIFY: "/siwe/verify",
|
|
99
104
|
ORG_CREATE: "/organization/create",
|
|
100
105
|
ORG_INVITE_MEMBER: "/organization/invite-member",
|
|
101
106
|
API_KEY_CREATE: "/api-key/create",
|
|
@@ -109,7 +114,8 @@ const routes = {
|
|
|
109
114
|
ADMIN_REVOKE_USER_SESSIONS: "/admin/revoke-user-sessions",
|
|
110
115
|
ADMIN_SET_PASSWORD: "/admin/set-user-password",
|
|
111
116
|
ADMIN_BAN_USER: "/admin/ban-user",
|
|
112
|
-
ADMIN_UNBAN_USER: "/admin/unban-user"
|
|
117
|
+
ADMIN_UNBAN_USER: "/admin/unban-user",
|
|
118
|
+
ADMIN_IMPERSONATE_USER: "/admin/impersonate-user"
|
|
113
119
|
};
|
|
114
120
|
const ORGANIZATION_EVENT_TYPES = {
|
|
115
121
|
ORGANIZATION_CREATED: "organization_created",
|
|
@@ -308,16 +314,33 @@ const matchesAnyRoute = (path, routes) => {
|
|
|
308
314
|
};
|
|
309
315
|
//#endregion
|
|
310
316
|
//#region src/events/login-methods.ts
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
routes.
|
|
314
|
-
routes.
|
|
315
|
-
routes.
|
|
316
|
-
routes.SIGN_IN_EMAIL_OTP,
|
|
317
|
-
routes.
|
|
317
|
+
const OAUTH_CALLBACK_PATHS = [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK];
|
|
318
|
+
const PATH_TO_METHOD = [
|
|
319
|
+
[routes.SIGN_IN_EMAIL, "email"],
|
|
320
|
+
[routes.SIGN_UP_EMAIL, "email"],
|
|
321
|
+
[routes.SIGN_IN_USERNAME, "username"],
|
|
322
|
+
[routes.SIGN_IN_EMAIL_OTP, "email-otp"],
|
|
323
|
+
[routes.SIGN_IN_SOCIAL, "social"],
|
|
324
|
+
[routes.SIGN_IN_ANONYMOUS, "anonymous"],
|
|
325
|
+
[routes.SIGN_IN_PASSKEY, "passkey"],
|
|
326
|
+
[routes.SIGN_IN_MAGIC_LINK, "magic-link"],
|
|
327
|
+
[routes.MAGIC_LINK_VERIFY, "magic-link"],
|
|
328
|
+
[routes.SIGN_IN_SSO, "sso"],
|
|
329
|
+
[routes.PHONE_VERIFY_OTP, "phone"],
|
|
330
|
+
[routes.TWO_FACTOR_VERIFY_TOTP, "totp"],
|
|
331
|
+
[routes.TWO_FACTOR_VERIFY_OTP, "two-factor-otp"],
|
|
332
|
+
[routes.TWO_FACTOR_VERIFY_BACKUP, "backup-code"],
|
|
333
|
+
[routes.ADMIN_IMPERSONATE_USER, "impersonation"],
|
|
334
|
+
[routes.DEVICE_TOKEN, "device-code"],
|
|
335
|
+
[routes.SIWE_VERIFY, "siwe"]
|
|
318
336
|
];
|
|
319
337
|
const getLoginMethod = (ctx) => {
|
|
320
|
-
if (matchesAnyRoute(ctx.path,
|
|
338
|
+
if (matchesAnyRoute(ctx.path, OAUTH_CALLBACK_PATHS)) {
|
|
339
|
+
const id = ctx.params?.id;
|
|
340
|
+
if (typeof id === "string" && id && !id.startsWith(":")) return id;
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
for (const [path, method] of PATH_TO_METHOD) if (matchesAnyRoute(ctx.path, [path])) return method;
|
|
321
344
|
return null;
|
|
322
345
|
};
|
|
323
346
|
//#endregion
|
|
@@ -812,18 +835,11 @@ const getOrganizationTriggerInfo = (user) => {
|
|
|
812
835
|
};
|
|
813
836
|
//#endregion
|
|
814
837
|
//#region src/events/index.ts
|
|
815
|
-
const initTrackEvents = (
|
|
816
|
-
const $fetch = createFetch({
|
|
817
|
-
baseURL: options.apiUrl,
|
|
818
|
-
headers: {
|
|
819
|
-
"user-agent": "better-auth",
|
|
820
|
-
"x-api-key": options.apiKey
|
|
821
|
-
}
|
|
822
|
-
});
|
|
838
|
+
const initTrackEvents = ($api) => {
|
|
823
839
|
const trackEvent = (data) => {
|
|
824
840
|
const track = async () => {
|
|
825
841
|
try {
|
|
826
|
-
await $
|
|
842
|
+
await $api("/events/track", {
|
|
827
843
|
method: "POST",
|
|
828
844
|
body: {
|
|
829
845
|
eventType: data.eventType,
|
|
@@ -874,11 +890,10 @@ function maybeCleanup() {
|
|
|
874
890
|
/**
|
|
875
891
|
* Fetch identification data from durable-kv by requestId
|
|
876
892
|
*/
|
|
877
|
-
async function getIdentification(requestId,
|
|
893
|
+
async function getIdentification(requestId, $kv) {
|
|
878
894
|
maybeCleanup();
|
|
879
895
|
const cached = identificationCache.get(requestId);
|
|
880
896
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
|
|
881
|
-
const baseUrl = kvUrl || INFRA_KV_URL;
|
|
882
897
|
const maxRetries = 3;
|
|
883
898
|
const retryDelays = [
|
|
884
899
|
50,
|
|
@@ -886,24 +901,20 @@ async function getIdentification(requestId, apiKey, kvUrl) {
|
|
|
886
901
|
200
|
|
887
902
|
];
|
|
888
903
|
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
headers: { "x-api-key": apiKey },
|
|
892
|
-
signal: AbortSignal.timeout(KV_TIMEOUT_MS)
|
|
893
|
-
});
|
|
894
|
-
if (response.ok) {
|
|
895
|
-
const data = await response.json();
|
|
904
|
+
const { data, error } = await $kv(`/identify/${requestId}`, { method: "GET" });
|
|
905
|
+
if (data && !error) {
|
|
896
906
|
identificationCache.set(requestId, {
|
|
897
907
|
data,
|
|
898
908
|
timestamp: Date.now()
|
|
899
909
|
});
|
|
900
910
|
return data;
|
|
901
911
|
}
|
|
902
|
-
|
|
912
|
+
const status = error?.status;
|
|
913
|
+
if (status === 404 && attempt < maxRetries) {
|
|
903
914
|
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
|
|
904
915
|
continue;
|
|
905
916
|
}
|
|
906
|
-
if (
|
|
917
|
+
if (status !== 404) identificationCache.set(requestId, {
|
|
907
918
|
data: null,
|
|
908
919
|
timestamp: Date.now()
|
|
909
920
|
});
|
|
@@ -931,16 +942,19 @@ function extractIdentificationHeaders(request) {
|
|
|
931
942
|
};
|
|
932
943
|
}
|
|
933
944
|
/**
|
|
934
|
-
* Early middleware that loads identification data
|
|
945
|
+
* Early middleware that loads identification data.
|
|
946
|
+
*
|
|
947
|
+
* @param $kv — KV client from {@link createKV} for the same plugin options (one instance per plugin).
|
|
935
948
|
*/
|
|
936
|
-
function createIdentificationMiddleware(
|
|
949
|
+
function createIdentificationMiddleware($kv) {
|
|
937
950
|
return createAuthMiddleware(async (ctx) => {
|
|
938
951
|
const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
|
|
939
952
|
const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
|
|
940
953
|
ctx.context.visitorId = visitorId;
|
|
941
954
|
ctx.context.requestId = requestId;
|
|
942
|
-
if (requestId)
|
|
943
|
-
|
|
955
|
+
if (requestId) {
|
|
956
|
+
if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv) ?? null;
|
|
957
|
+
} else ctx.context.identification = null;
|
|
944
958
|
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
945
959
|
if (ipConfig?.disableIpTracking === true) {
|
|
946
960
|
ctx.context.location = void 0;
|
|
@@ -997,6 +1011,59 @@ function getCountryCodeFromRequest(request) {
|
|
|
997
1011
|
return cc ? cc.toUpperCase() : void 0;
|
|
998
1012
|
}
|
|
999
1013
|
//#endregion
|
|
1014
|
+
//#region src/sentinel/evaluation.ts
|
|
1015
|
+
/** Lazily attach the buffer to the request context. */
|
|
1016
|
+
function getEvaluation(ctx) {
|
|
1017
|
+
if (!ctx.context.sentinelEvaluation) ctx.context.sentinelEvaluation = {
|
|
1018
|
+
checks: /* @__PURE__ */ new Set(),
|
|
1019
|
+
outcome: "passed",
|
|
1020
|
+
emitted: false
|
|
1021
|
+
};
|
|
1022
|
+
return ctx.context.sentinelEvaluation;
|
|
1023
|
+
}
|
|
1024
|
+
function recordCheck(ctx, name) {
|
|
1025
|
+
getEvaluation(ctx).checks.add(name);
|
|
1026
|
+
}
|
|
1027
|
+
/** Call immediately before throwing so the buffer reflects the final outcome. */
|
|
1028
|
+
function setOutcome(ctx, outcome, triggeredBy, details) {
|
|
1029
|
+
const ev = getEvaluation(ctx);
|
|
1030
|
+
ev.outcome = outcome;
|
|
1031
|
+
ev.triggeredBy = triggeredBy;
|
|
1032
|
+
if (details) ev.details = details;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Emit one `security_check` event for the request. Idempotent per request and
|
|
1036
|
+
* skipped when no checks ran (unprotected route, or short-circuit before any
|
|
1037
|
+
* sentinel hook). `evaluatedChecks` carries the per-type breakdown — all under
|
|
1038
|
+
* the same Stripe meter, so the array is for support inspection only.
|
|
1039
|
+
*/
|
|
1040
|
+
function emitEvaluation(ctx, trackEvent, options = {}) {
|
|
1041
|
+
const ev = ctx.context.sentinelEvaluation;
|
|
1042
|
+
if (!ev || ev.emitted || ev.checks.size === 0) return;
|
|
1043
|
+
ev.emitted = true;
|
|
1044
|
+
const identification = ctx.context.identification ?? null;
|
|
1045
|
+
const visitorId = ctx.context.visitorId ?? null;
|
|
1046
|
+
const path = ctx.path ?? "";
|
|
1047
|
+
trackEvent({
|
|
1048
|
+
eventKey: visitorId || identification?.ip || "unknown",
|
|
1049
|
+
eventType: "security_check",
|
|
1050
|
+
eventDisplayName: ev.outcome === "blocked" ? "Security: blocked" : ev.outcome === "challenged" ? "Security: challenged" : "Security: passed",
|
|
1051
|
+
eventData: {
|
|
1052
|
+
outcome: ev.outcome,
|
|
1053
|
+
evaluatedChecks: [...ev.checks].sort(),
|
|
1054
|
+
triggeredBy: ev.triggeredBy,
|
|
1055
|
+
identifier: options.identifier,
|
|
1056
|
+
path,
|
|
1057
|
+
userAgent: options.userAgent,
|
|
1058
|
+
details: ev.details
|
|
1059
|
+
},
|
|
1060
|
+
ipAddress: identification?.ip || void 0,
|
|
1061
|
+
city: identification?.location?.city || void 0,
|
|
1062
|
+
country: identification?.location?.country?.name || void 0,
|
|
1063
|
+
countryCode: identification?.location?.country?.code || void 0
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
//#endregion
|
|
1000
1067
|
//#region src/sentinel/security.ts
|
|
1001
1068
|
async function hashForFingerprint(input) {
|
|
1002
1069
|
const data = new TextEncoder().encode(input);
|
|
@@ -1011,23 +1078,19 @@ async function sha1Hash(input) {
|
|
|
1011
1078
|
/**
|
|
1012
1079
|
* Whether Sentinel should normalize emails for deduplication/sign-in consistency.
|
|
1013
1080
|
* If `emailNormalization` is set, it wins; otherwise legacy behavior uses `emailValidation.enabled`.
|
|
1014
|
-
* @internal Not part of the package public API; used by the sentinel plugin and email helpers.
|
|
1015
1081
|
*/
|
|
1016
1082
|
function isEmailNormalizationEnabled(security) {
|
|
1017
1083
|
const explicit = security?.emailNormalization;
|
|
1018
1084
|
if (explicit !== void 0) return explicit.enabled !== false;
|
|
1019
1085
|
return security?.emailValidation?.enabled !== false;
|
|
1020
1086
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
headers: { "x-api-key": apiKey },
|
|
1026
|
-
throw: true
|
|
1027
|
-
});
|
|
1087
|
+
/**
|
|
1088
|
+
* @param $api — Dash client from `createAPI(opts, { throw: true })`.
|
|
1089
|
+
*/
|
|
1090
|
+
function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
1028
1091
|
const emailSender = createEmailSender({
|
|
1029
|
-
apiUrl:
|
|
1030
|
-
apiKey
|
|
1092
|
+
apiUrl: conn.apiUrl || INFRA_API_URL,
|
|
1093
|
+
apiKey: conn.apiKey
|
|
1031
1094
|
});
|
|
1032
1095
|
function logEvent(event) {
|
|
1033
1096
|
const fullEvent = {
|
|
@@ -1183,6 +1246,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1183
1246
|
return null;
|
|
1184
1247
|
}
|
|
1185
1248
|
},
|
|
1249
|
+
/**
|
|
1250
|
+
* Store user's last known location for impossible travel detection
|
|
1251
|
+
*/
|
|
1186
1252
|
async storeLastLocation(userId, location) {
|
|
1187
1253
|
if (!location) return;
|
|
1188
1254
|
try {
|
|
@@ -1197,6 +1263,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1197
1263
|
logger.error("[Dash] Store last location error:", error);
|
|
1198
1264
|
}
|
|
1199
1265
|
},
|
|
1266
|
+
/**
|
|
1267
|
+
* Check if a visitor has exceeded the free trial signup limit.
|
|
1268
|
+
*/
|
|
1200
1269
|
async checkFreeTrialAbuse(visitorId) {
|
|
1201
1270
|
if (!options.freeTrialAbuse?.enabled) return {
|
|
1202
1271
|
isAbuse: false,
|
|
@@ -1235,6 +1304,10 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1235
1304
|
};
|
|
1236
1305
|
}
|
|
1237
1306
|
},
|
|
1307
|
+
/**
|
|
1308
|
+
* Track a new signup for free trial abuse detection.
|
|
1309
|
+
* Stores the userId for auditing purposes.
|
|
1310
|
+
*/
|
|
1238
1311
|
async trackFreeTrialSignup(visitorId, userId) {
|
|
1239
1312
|
if (!options.freeTrialAbuse?.enabled) return;
|
|
1240
1313
|
try {
|
|
@@ -1285,6 +1358,10 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1285
1358
|
return { compromised: false };
|
|
1286
1359
|
}
|
|
1287
1360
|
},
|
|
1361
|
+
/**
|
|
1362
|
+
* Check if a user account is stale (inactive for a configured period)
|
|
1363
|
+
* This helps detect potential account takeover when dormant accounts become active
|
|
1364
|
+
*/
|
|
1288
1365
|
async checkStaleUser(userId, lastActiveAt) {
|
|
1289
1366
|
if (!options.staleUsers?.enabled) return { isStale: false };
|
|
1290
1367
|
try {
|
|
@@ -1317,6 +1394,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1317
1394
|
return { isStale: false };
|
|
1318
1395
|
}
|
|
1319
1396
|
},
|
|
1397
|
+
/**
|
|
1398
|
+
* Send stale account reactivation notification to the user
|
|
1399
|
+
*/
|
|
1320
1400
|
async notifyStaleAccountUser(userEmail, userName, daysSinceLastActive, identification, appName) {
|
|
1321
1401
|
const loginTime = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
|
|
1322
1402
|
dateStyle: "long",
|
|
@@ -1344,6 +1424,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1344
1424
|
if (result.success) logger.info(`[Dash] Stale account notification sent to user: ${userEmail}`);
|
|
1345
1425
|
else logger.error(`[Dash] Failed to send stale account user notification: ${result.error}`);
|
|
1346
1426
|
},
|
|
1427
|
+
/**
|
|
1428
|
+
* Send stale account reactivation notification to the admin
|
|
1429
|
+
*/
|
|
1347
1430
|
async notifyStaleAccountAdmin(adminEmail, userId, userEmail, userName, daysSinceLastActive, identification, appName) {
|
|
1348
1431
|
const loginTime = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
|
|
1349
1432
|
dateStyle: "long",
|
|
@@ -1412,7 +1495,9 @@ const all = new Set([
|
|
|
1412
1495
|
"/forget-password",
|
|
1413
1496
|
"/request-password-reset",
|
|
1414
1497
|
"/send-verification-email",
|
|
1415
|
-
"/change-email"
|
|
1498
|
+
"/change-email",
|
|
1499
|
+
"/organization/invite-member",
|
|
1500
|
+
"/dash/organization/invite-member"
|
|
1416
1501
|
]);
|
|
1417
1502
|
/**
|
|
1418
1503
|
* Path is one of `[
|
|
@@ -1429,7 +1514,9 @@ const all = new Set([
|
|
|
1429
1514
|
* '/forget-password',
|
|
1430
1515
|
* '/request-password-reset',
|
|
1431
1516
|
* '/send-verification-email',
|
|
1432
|
-
* '/change-email'
|
|
1517
|
+
* '/change-email',
|
|
1518
|
+
* '/organization/invite-member',
|
|
1519
|
+
* '/dash/organization/invite-member'
|
|
1433
1520
|
* ]`.
|
|
1434
1521
|
* @param context Request context
|
|
1435
1522
|
* @param context.path Request path
|
|
@@ -1487,17 +1574,11 @@ function normalizeEmail(email, context) {
|
|
|
1487
1574
|
if (GMAIL_LIKE_DOMAINS.has(domain)) localPart = localPart.replace(/\./g, "");
|
|
1488
1575
|
return `${localPart}@${domain}`;
|
|
1489
1576
|
}
|
|
1490
|
-
function
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
});
|
|
1496
|
-
const $kv = createFetch({
|
|
1497
|
-
baseURL: kvUrl,
|
|
1498
|
-
headers: { "x-api-key": apiKey },
|
|
1499
|
-
timeout: KV_TIMEOUT_MS
|
|
1500
|
-
});
|
|
1577
|
+
function hasRemoteEmailClients(options) {
|
|
1578
|
+
return options.$api !== void 0 && options.$kv !== void 0;
|
|
1579
|
+
}
|
|
1580
|
+
function createEmailValidator(options) {
|
|
1581
|
+
const { $api, $kv, emailValidationOptions = {} } = options;
|
|
1501
1582
|
/**
|
|
1502
1583
|
* Fetch and resolve email validity policy from API with caching
|
|
1503
1584
|
* Sends client config to API which merges with user's dashboard settings
|
|
@@ -1522,7 +1603,12 @@ function createEmailValidator(options = {}) {
|
|
|
1522
1603
|
}
|
|
1523
1604
|
return null;
|
|
1524
1605
|
}
|
|
1525
|
-
return {
|
|
1606
|
+
return {
|
|
1607
|
+
/**
|
|
1608
|
+
* Validate an email address
|
|
1609
|
+
* Returns validation result - caller should use getPolicy() to determine action
|
|
1610
|
+
*/
|
|
1611
|
+
async validate(email, checkMx = true) {
|
|
1526
1612
|
const trimmed = email.trim();
|
|
1527
1613
|
const policy = await fetchPolicy();
|
|
1528
1614
|
if (!policy?.enabled) return {
|
|
@@ -1608,16 +1694,27 @@ function createEmailNormalizationHook() {
|
|
|
1608
1694
|
};
|
|
1609
1695
|
}
|
|
1610
1696
|
/**
|
|
1611
|
-
*
|
|
1697
|
+
* Records `email_validity` on every email-bearing request. On block, emits the
|
|
1698
|
+
* `security_check` event inline before throwing — the after-hook won't run on throw.
|
|
1612
1699
|
*/
|
|
1613
|
-
function createEmailValidationHook(validator,
|
|
1700
|
+
function createEmailValidationHook(validator, trackEvent) {
|
|
1614
1701
|
return {
|
|
1615
1702
|
matcher: allEmail,
|
|
1616
1703
|
handler: createAuthMiddleware(async (ctx) => {
|
|
1617
1704
|
const { email } = getEmail(ctx);
|
|
1618
1705
|
if (typeof email !== "string") return;
|
|
1619
1706
|
const trimmed = email.trim();
|
|
1620
|
-
|
|
1707
|
+
recordCheck(ctx, "email_validity");
|
|
1708
|
+
if (!isValidEmailFormatLocal(trimmed)) {
|
|
1709
|
+
if (trackEvent) {
|
|
1710
|
+
setOutcome(ctx, "blocked", "email_validity", {
|
|
1711
|
+
reason: "invalid_format",
|
|
1712
|
+
email: trimmed
|
|
1713
|
+
});
|
|
1714
|
+
emitEvaluation(ctx, trackEvent, { identifier: trimmed });
|
|
1715
|
+
}
|
|
1716
|
+
throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
|
|
1717
|
+
}
|
|
1621
1718
|
if (validator) {
|
|
1622
1719
|
const result = await validator.validate(trimmed);
|
|
1623
1720
|
const policy = result.policy;
|
|
@@ -1628,19 +1725,17 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1628
1725
|
}
|
|
1629
1726
|
const action = policy.action;
|
|
1630
1727
|
if (!result.valid) {
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
reason: result.reason
|
|
1728
|
+
if (action === "allow") return;
|
|
1729
|
+
const message = result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short" ? "This email address appears to be invalid" : "Invalid email";
|
|
1730
|
+
if (trackEvent) {
|
|
1731
|
+
setOutcome(ctx, "blocked", "email_validity", {
|
|
1732
|
+
reason: result.reason,
|
|
1636
1733
|
confidence: result.confidence,
|
|
1637
|
-
|
|
1638
|
-
path: ctx.path,
|
|
1639
|
-
action
|
|
1734
|
+
email: trimmed
|
|
1640
1735
|
});
|
|
1736
|
+
emitEvaluation(ctx, trackEvent, { identifier: trimmed });
|
|
1641
1737
|
}
|
|
1642
|
-
|
|
1643
|
-
throw new APIError$1("BAD_REQUEST", { message: result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short" ? "This email address appears to be invalid" : "Invalid email" });
|
|
1738
|
+
throw new APIError$1("BAD_REQUEST", { message });
|
|
1644
1739
|
}
|
|
1645
1740
|
}
|
|
1646
1741
|
})
|
|
@@ -1657,18 +1752,10 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1657
1752
|
*
|
|
1658
1753
|
* @example
|
|
1659
1754
|
* // API-backed validation
|
|
1660
|
-
* createEmailHooks({
|
|
1661
|
-
*
|
|
1662
|
-
* @example
|
|
1663
|
-
* // High strictness + API
|
|
1664
|
-
* createEmailHooks({
|
|
1665
|
-
* emailValidationOptions: { strictness: "high" },
|
|
1666
|
-
* useApi: true,
|
|
1667
|
-
* apiKey: "your-api-key",
|
|
1668
|
-
* })
|
|
1755
|
+
* createEmailHooks({ $api, $kv })
|
|
1669
1756
|
*/
|
|
1670
1757
|
function createEmailHooks(options = {}) {
|
|
1671
|
-
const { emailValidationOptions, emailNormalizationOptions,
|
|
1758
|
+
const { emailValidationOptions, emailNormalizationOptions, trackEvent } = options;
|
|
1672
1759
|
const emailConfig = {
|
|
1673
1760
|
enabled: true,
|
|
1674
1761
|
strictness: "medium",
|
|
@@ -1678,12 +1765,11 @@ function createEmailHooks(options = {}) {
|
|
|
1678
1765
|
return { before: [...isEmailNormalizationEnabled({
|
|
1679
1766
|
emailValidation: emailValidationOptions,
|
|
1680
1767
|
emailNormalization: emailNormalizationOptions
|
|
1681
|
-
}) ? [createEmailNormalizationHook()] : [], ...emailConfig.enabled ? [createEmailValidationHook(
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
apiKey,
|
|
1768
|
+
}) ? [createEmailNormalizationHook()] : [], ...emailConfig.enabled ? [createEmailValidationHook(hasRemoteEmailClients(options) ? createEmailValidator({
|
|
1769
|
+
$api: options.$api,
|
|
1770
|
+
$kv: options.$kv,
|
|
1685
1771
|
emailValidationOptions: emailConfig
|
|
1686
|
-
}) : void 0,
|
|
1772
|
+
}) : void 0, trackEvent)] : []] };
|
|
1687
1773
|
}
|
|
1688
1774
|
//#endregion
|
|
1689
1775
|
//#region src/validation/phone.ts
|
|
@@ -1898,32 +1984,6 @@ const ERROR_MESSAGES = {
|
|
|
1898
1984
|
compromised_password: "This password has been found in data breaches. Please choose a different password.",
|
|
1899
1985
|
impossible_travel: "Login blocked due to suspicious location change."
|
|
1900
1986
|
};
|
|
1901
|
-
const DISPLAY_NAMES = {
|
|
1902
|
-
geo_blocked: {
|
|
1903
|
-
challenge: "Security: geo challenge",
|
|
1904
|
-
block: "Security: geo blocked"
|
|
1905
|
-
},
|
|
1906
|
-
bot_detected: {
|
|
1907
|
-
challenge: "Security: bot challenge",
|
|
1908
|
-
block: "Security: bot blocked"
|
|
1909
|
-
},
|
|
1910
|
-
suspicious_ip_detected: {
|
|
1911
|
-
challenge: "Security: anonymous IP challenge",
|
|
1912
|
-
block: "Security: anonymous IP blocked"
|
|
1913
|
-
},
|
|
1914
|
-
rate_limited: {
|
|
1915
|
-
challenge: "Security: velocity challenge",
|
|
1916
|
-
block: "Security: velocity exceeded"
|
|
1917
|
-
},
|
|
1918
|
-
compromised_password: {
|
|
1919
|
-
challenge: "Security: breached password warning",
|
|
1920
|
-
block: "Security: breached password blocked"
|
|
1921
|
-
},
|
|
1922
|
-
impossible_travel: {
|
|
1923
|
-
challenge: "Security: impossible travel challenge",
|
|
1924
|
-
block: "Security: impossible travel blocked"
|
|
1925
|
-
}
|
|
1926
|
-
};
|
|
1927
1987
|
/**
|
|
1928
1988
|
* Throw a challenge error with appropriate headers
|
|
1929
1989
|
*/
|
|
@@ -1938,86 +1998,68 @@ function throwChallengeError(challenge, reason, message = "Please complete a sec
|
|
|
1938
1998
|
};
|
|
1939
1999
|
throw error;
|
|
1940
2000
|
}
|
|
1941
|
-
/**
|
|
1942
|
-
|
|
1943
|
-
*/
|
|
1944
|
-
function buildEventData(ctx, action, reason, confidence = 1, extraData) {
|
|
1945
|
-
const { visitorId, identification, path, identifier, userAgent } = ctx;
|
|
1946
|
-
const countryCode = identification?.location?.country?.code || void 0;
|
|
1947
|
-
return {
|
|
1948
|
-
eventKey: visitorId || identification?.ip || "unknown",
|
|
1949
|
-
eventType: action === "challenged" ? "security_challenged" : "security_blocked",
|
|
1950
|
-
eventDisplayName: DISPLAY_NAMES[reason]?.[action === "challenged" ? "challenge" : "block"] || `Security: ${reason}`,
|
|
1951
|
-
eventData: {
|
|
1952
|
-
action,
|
|
1953
|
-
reason,
|
|
1954
|
-
visitorId: visitorId || "",
|
|
1955
|
-
path,
|
|
1956
|
-
userAgent,
|
|
1957
|
-
identifier,
|
|
1958
|
-
confidence,
|
|
1959
|
-
...extraData
|
|
1960
|
-
},
|
|
1961
|
-
ipAddress: identification?.ip || void 0,
|
|
1962
|
-
city: identification?.location?.city || void 0,
|
|
1963
|
-
country: identification?.location?.country?.name || void 0,
|
|
1964
|
-
countryCode
|
|
1965
|
-
};
|
|
1966
|
-
}
|
|
1967
|
-
/**
|
|
1968
|
-
* Handle a security check result by tracking events and throwing appropriate errors
|
|
1969
|
-
*
|
|
1970
|
-
* @param verdict - The security verdict from the security service
|
|
1971
|
-
* @param ctx - Security check context with request information
|
|
1972
|
-
* @param trackEvent - Function to track security events
|
|
1973
|
-
* @param securityService - Security service for generating challenges
|
|
1974
|
-
* @returns True if the request should be blocked
|
|
1975
|
-
*/
|
|
1976
|
-
async function handleSecurityVerdict(verdict, ctx, trackEvent, securityService) {
|
|
2001
|
+
/** Emits the `security_check` event inline before throwing — the after-hook won't run on throw. */
|
|
2002
|
+
async function handleSecurityVerdict(verdict, hookCtx, checkCtx, trackEvent, securityService) {
|
|
1977
2003
|
if (verdict.action === "allow") return;
|
|
1978
2004
|
const reason = verdict.reason || "unknown";
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
const challenge = verdict.challenge || await securityService.generateChallenge(ctx.visitorId);
|
|
2005
|
+
if (verdict.action === "challenge" && checkCtx.visitorId) {
|
|
2006
|
+
const challenge = verdict.challenge || await securityService.generateChallenge(checkCtx.visitorId);
|
|
1982
2007
|
if (!challenge?.trim()) {
|
|
1983
2008
|
logger.warn("[Sentinel] Could not generate PoW challenge (service may be unavailable). Falling back to allow.");
|
|
1984
2009
|
return;
|
|
1985
2010
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2011
|
+
setOutcome(hookCtx, "challenged", "server_security_check", {
|
|
2012
|
+
reason,
|
|
2013
|
+
...verdict.details
|
|
2014
|
+
});
|
|
2015
|
+
emitEvaluation(hookCtx, trackEvent, {
|
|
2016
|
+
identifier: checkCtx.identifier,
|
|
2017
|
+
userAgent: checkCtx.userAgent
|
|
2018
|
+
});
|
|
2019
|
+
throwChallengeError(challenge, reason);
|
|
2020
|
+
}
|
|
2021
|
+
if (verdict.action === "block") {
|
|
2022
|
+
setOutcome(hookCtx, "blocked", "server_security_check", {
|
|
2023
|
+
reason,
|
|
2024
|
+
...verdict.details
|
|
2025
|
+
});
|
|
2026
|
+
emitEvaluation(hookCtx, trackEvent, {
|
|
2027
|
+
identifier: checkCtx.identifier,
|
|
2028
|
+
userAgent: checkCtx.userAgent
|
|
2029
|
+
});
|
|
1990
2030
|
throw new APIError("FORBIDDEN", { message: ERROR_MESSAGES[reason] || "Access denied." });
|
|
1991
2031
|
}
|
|
1992
2032
|
}
|
|
1993
2033
|
/**
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
* This replaces the multiple individual check calls with a single API call
|
|
1997
|
-
* that handles all security checks server-side.
|
|
2034
|
+
* Single API call from the plugin; server runs all configured rules and returns
|
|
2035
|
+
* one verdict — counts as one billable check.
|
|
1998
2036
|
*/
|
|
1999
|
-
async function runSecurityChecks(
|
|
2037
|
+
async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent, powVerified) {
|
|
2000
2038
|
if (powVerified) return;
|
|
2039
|
+
recordCheck(hookCtx, "server_security_check");
|
|
2001
2040
|
await handleSecurityVerdict(await securityService.checkSecurity({
|
|
2002
|
-
visitorId:
|
|
2003
|
-
requestId:
|
|
2004
|
-
ip:
|
|
2005
|
-
path:
|
|
2006
|
-
identifier:
|
|
2007
|
-
}),
|
|
2041
|
+
visitorId: checkCtx.visitorId,
|
|
2042
|
+
requestId: checkCtx.identification?.requestId || null,
|
|
2043
|
+
ip: checkCtx.identification?.ip || null,
|
|
2044
|
+
path: checkCtx.path,
|
|
2045
|
+
identifier: checkCtx.identifier
|
|
2046
|
+
}), hookCtx, checkCtx, trackEvent, securityService);
|
|
2008
2047
|
}
|
|
2009
2048
|
//#endregion
|
|
2010
2049
|
//#region src/sentinel/sentinel.ts
|
|
2011
2050
|
const sentinel = (options) => {
|
|
2012
2051
|
const opts = resolveSentinelOptions(options);
|
|
2013
|
-
const
|
|
2052
|
+
const $api = createAPI(opts);
|
|
2053
|
+
const $apiThrowing = createAPI(opts, { throw: true });
|
|
2054
|
+
const $kv = createKV(opts);
|
|
2055
|
+
const { tracker } = initTrackEvents($api);
|
|
2014
2056
|
const { trackEvent } = tracker;
|
|
2015
2057
|
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.");
|
|
2016
|
-
const securityService = createSecurityClient(opts
|
|
2058
|
+
const securityService = createSecurityClient(opts, $apiThrowing, opts.security || {}, (event) => {
|
|
2017
2059
|
trackEvent({
|
|
2018
2060
|
eventKey: event.visitorId || event.userId || "unknown",
|
|
2019
|
-
eventType:
|
|
2020
|
-
eventDisplayName: `Security: ${event.type.replace(/_/g, " ")}`,
|
|
2061
|
+
eventType: "security_signal",
|
|
2062
|
+
eventDisplayName: `Security signal: ${event.type.replace(/_/g, " ")}`,
|
|
2021
2063
|
eventData: {
|
|
2022
2064
|
type: event.type,
|
|
2023
2065
|
userId: event.userId || void 0,
|
|
@@ -2032,35 +2074,11 @@ const sentinel = (options) => {
|
|
|
2032
2074
|
const emailHooks = createEmailHooks({
|
|
2033
2075
|
emailValidationOptions: opts.security?.emailValidation,
|
|
2034
2076
|
emailNormalizationOptions: opts.security?.emailNormalization,
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const isNoMxRecord = data.reason === "no_mx_records";
|
|
2041
|
-
const reason = isNoMxRecord ? "no_mx_records" : "disposable_email";
|
|
2042
|
-
const label = isNoMxRecord ? "No MX Records" : "Disposable Email";
|
|
2043
|
-
const actionLabel = data.action === "allow" ? "allowed" : "blocked";
|
|
2044
|
-
const actionVerb = data.action === "allow" ? "detected" : "blocked";
|
|
2045
|
-
const eventType = data.action === "allow" ? "security_allowed" : "security_blocked";
|
|
2046
|
-
const displayName = isNoMxRecord ? `Security: invalid email domain ${actionVerb}` : `Security: disposable email ${actionVerb}`;
|
|
2047
|
-
const description = isNoMxRecord ? `${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)} signup attempt with invalid email domain (no MX records): ${data.email}` : `${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)} signup attempt with disposable email: ${data.email} (${data.reason}, ${data.confidence} confidence)`;
|
|
2048
|
-
logger.info(`[Sentinel] Tracking ${reason} event for email: ${data.email} (action: ${actionLabel})`);
|
|
2049
|
-
trackEvent({
|
|
2050
|
-
eventKey: data.email,
|
|
2051
|
-
eventType,
|
|
2052
|
-
eventDisplayName: displayName,
|
|
2053
|
-
eventData: {
|
|
2054
|
-
action: actionLabel,
|
|
2055
|
-
reason,
|
|
2056
|
-
identifier: data.email,
|
|
2057
|
-
detectionLabel: label,
|
|
2058
|
-
description,
|
|
2059
|
-
path: data.path
|
|
2060
|
-
},
|
|
2061
|
-
ipAddress: data.ip
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2077
|
+
trackEvent,
|
|
2078
|
+
...opts.apiKey ? {
|
|
2079
|
+
$api,
|
|
2080
|
+
$kv
|
|
2081
|
+
} : {}
|
|
2064
2082
|
});
|
|
2065
2083
|
return {
|
|
2066
2084
|
id: "sentinel",
|
|
@@ -2075,8 +2093,16 @@ const sentinel = (options) => {
|
|
|
2075
2093
|
if (!ctx) return;
|
|
2076
2094
|
const visitorId = ctx.context.visitorId;
|
|
2077
2095
|
if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
|
|
2096
|
+
recordCheck(ctx, "free_trial_abuse");
|
|
2078
2097
|
const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
|
|
2079
|
-
if (abuseCheck.isAbuse && abuseCheck.action === "block")
|
|
2098
|
+
if (abuseCheck.isAbuse && abuseCheck.action === "block") {
|
|
2099
|
+
setOutcome(ctx, "blocked", "free_trial_abuse", {
|
|
2100
|
+
accountCount: abuseCheck.accountCount,
|
|
2101
|
+
maxAccounts: abuseCheck.maxAccounts
|
|
2102
|
+
});
|
|
2103
|
+
emitEvaluation(ctx, trackEvent);
|
|
2104
|
+
throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
|
|
2105
|
+
}
|
|
2080
2106
|
}
|
|
2081
2107
|
if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
|
|
2082
2108
|
...user,
|
|
@@ -2095,10 +2121,29 @@ const sentinel = (options) => {
|
|
|
2095
2121
|
const visitorId = ctx.context.visitorId;
|
|
2096
2122
|
const identification = ctx.context.identification;
|
|
2097
2123
|
if (session.userId && identification?.location && visitorId) {
|
|
2124
|
+
recordCheck(ctx, "impossible_travel");
|
|
2098
2125
|
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
|
|
2099
2126
|
if (travelCheck?.isImpossible) {
|
|
2100
|
-
if (travelCheck.action === "block")
|
|
2101
|
-
|
|
2127
|
+
if (travelCheck.action === "block") {
|
|
2128
|
+
setOutcome(ctx, "blocked", "impossible_travel", {
|
|
2129
|
+
from: travelCheck.from,
|
|
2130
|
+
to: travelCheck.to,
|
|
2131
|
+
distance: travelCheck.distance,
|
|
2132
|
+
speedRequired: travelCheck.speedRequired
|
|
2133
|
+
});
|
|
2134
|
+
emitEvaluation(ctx, trackEvent);
|
|
2135
|
+
throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
|
|
2136
|
+
}
|
|
2137
|
+
if (travelCheck.action === "challenge" && travelCheck.challenge) {
|
|
2138
|
+
setOutcome(ctx, "challenged", "impossible_travel", {
|
|
2139
|
+
from: travelCheck.from,
|
|
2140
|
+
to: travelCheck.to,
|
|
2141
|
+
distance: travelCheck.distance,
|
|
2142
|
+
speedRequired: travelCheck.speedRequired
|
|
2143
|
+
});
|
|
2144
|
+
emitEvaluation(ctx, trackEvent);
|
|
2145
|
+
throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
|
|
2146
|
+
}
|
|
2102
2147
|
}
|
|
2103
2148
|
}
|
|
2104
2149
|
},
|
|
@@ -2128,6 +2173,7 @@ const sentinel = (options) => {
|
|
|
2128
2173
|
if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
|
|
2129
2174
|
}
|
|
2130
2175
|
if (opts.security?.staleUsers?.enabled && user) {
|
|
2176
|
+
recordCheck(ctx, "stale_users");
|
|
2131
2177
|
const lastActiveAtForStale = activityTrackingEnabled ? user.lastActiveAt ?? null : null;
|
|
2132
2178
|
const staleCheck = await securityService.checkStaleUser(session.userId, lastActiveAtForStale);
|
|
2133
2179
|
if (staleCheck.isStale) {
|
|
@@ -2138,31 +2184,21 @@ const sentinel = (options) => {
|
|
|
2138
2184
|
if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
|
|
2139
2185
|
logger.error("[Sentinel] Failed to send stale account notifications:", error);
|
|
2140
2186
|
});
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
eventType: "security_stale_account",
|
|
2144
|
-
eventDisplayName: "Security: stale account reactivated",
|
|
2145
|
-
eventData: {
|
|
2146
|
-
action: staleCheck.action === "block" ? "blocked" : staleCheck.action === "challenge" ? "challenged" : "logged",
|
|
2147
|
-
reason: "stale_account_reactivation",
|
|
2187
|
+
if (staleCheck.action === "block") {
|
|
2188
|
+
setOutcome(ctx, "blocked", "stale_users", {
|
|
2148
2189
|
userId: session.userId,
|
|
2149
2190
|
daysSinceLastActive: staleCheck.daysSinceLastActive,
|
|
2150
2191
|
staleDays: staleCheck.staleDays,
|
|
2151
2192
|
lastActiveAt: staleCheck.lastActiveAt,
|
|
2152
2193
|
notifyUser: staleCheck.notifyUser,
|
|
2153
|
-
notifyAdmin: staleCheck.notifyAdmin
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
});
|
|
2162
|
-
if (staleCheck.action === "block") throw new APIError("FORBIDDEN", {
|
|
2163
|
-
message: "This account has been inactive for an extended period. Please contact support to reactivate.",
|
|
2164
|
-
code: "STALE_ACCOUNT"
|
|
2165
|
-
});
|
|
2194
|
+
notifyAdmin: staleCheck.notifyAdmin
|
|
2195
|
+
});
|
|
2196
|
+
emitEvaluation(ctx, trackEvent);
|
|
2197
|
+
throw new APIError("FORBIDDEN", {
|
|
2198
|
+
message: "This account has been inactive for an extended period. Please contact support to reactivate.",
|
|
2199
|
+
code: "STALE_ACCOUNT"
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2166
2202
|
}
|
|
2167
2203
|
}
|
|
2168
2204
|
if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
|
|
@@ -2175,7 +2211,7 @@ const sentinel = (options) => {
|
|
|
2175
2211
|
before: [
|
|
2176
2212
|
{
|
|
2177
2213
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2178
|
-
handler: createIdentificationMiddleware(
|
|
2214
|
+
handler: createIdentificationMiddleware($kv)
|
|
2179
2215
|
},
|
|
2180
2216
|
{
|
|
2181
2217
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
@@ -2225,52 +2261,23 @@ const sentinel = (options) => {
|
|
|
2225
2261
|
const requestBody = ctx.body;
|
|
2226
2262
|
const identifier = requestBody?.email || requestBody?.phone || requestBody?.username || void 0;
|
|
2227
2263
|
const powVerified = ctx.context.powVerified === true;
|
|
2228
|
-
if (visitorId && powVerified)
|
|
2229
|
-
eventKey: visitorId,
|
|
2230
|
-
eventType: "security_allowed",
|
|
2231
|
-
eventDisplayName: "Security: challenge completed",
|
|
2232
|
-
eventData: {
|
|
2233
|
-
action: "allowed",
|
|
2234
|
-
reason: "pow_verified",
|
|
2235
|
-
visitorId,
|
|
2236
|
-
path: ctx.path,
|
|
2237
|
-
userAgent: ctx.headers?.get?.("user-agent") || "",
|
|
2238
|
-
identifier,
|
|
2239
|
-
detectionLabel: "Challenge Completed",
|
|
2240
|
-
description: identifier ? `Successfully completed security challenge for "${identifier}"` : "Successfully completed security challenge"
|
|
2241
|
-
},
|
|
2242
|
-
ipAddress: identification?.ip || void 0,
|
|
2243
|
-
city: identification?.location?.city || void 0,
|
|
2244
|
-
country: identification?.location?.country?.name || void 0,
|
|
2245
|
-
countryCode: identification?.location?.country?.code || void 0
|
|
2246
|
-
});
|
|
2264
|
+
if (visitorId && powVerified) recordCheck(ctx, "pow_challenge");
|
|
2247
2265
|
if (visitorId) {
|
|
2266
|
+
recordCheck(ctx, "credential_stuffing");
|
|
2248
2267
|
if (await securityService.isBlocked(visitorId)) {
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
path: ctx.path,
|
|
2258
|
-
userAgent: ctx.headers?.get?.("user-agent") || "",
|
|
2259
|
-
identifier,
|
|
2260
|
-
detectionType: "cooldown_active",
|
|
2261
|
-
detectionLabel: "Credential Stuffing",
|
|
2262
|
-
description: identifier ? `Visitor attempting "${identifier}" still in cooldown from prior detection` : "Visitor still in cooldown from prior detection",
|
|
2263
|
-
confidence: 1
|
|
2264
|
-
},
|
|
2265
|
-
ipAddress: identification?.ip || void 0,
|
|
2266
|
-
city: identification?.location?.city || void 0,
|
|
2267
|
-
country: identification?.location?.country?.name || void 0,
|
|
2268
|
-
countryCode: identification?.location?.country?.code || void 0
|
|
2268
|
+
setOutcome(ctx, "blocked", "credential_stuffing", {
|
|
2269
|
+
reason: "cooldown_active",
|
|
2270
|
+
visitorId,
|
|
2271
|
+
confidence: 1
|
|
2272
|
+
});
|
|
2273
|
+
emitEvaluation(ctx, trackEvent, {
|
|
2274
|
+
identifier,
|
|
2275
|
+
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2269
2276
|
});
|
|
2270
2277
|
throw new APIError(403, { message: "Too many failed attempts. Please try again later." });
|
|
2271
2278
|
}
|
|
2272
2279
|
}
|
|
2273
|
-
await runSecurityChecks({
|
|
2280
|
+
await runSecurityChecks(ctx, {
|
|
2274
2281
|
path: ctx.path,
|
|
2275
2282
|
identifier,
|
|
2276
2283
|
visitorId,
|
|
@@ -2286,12 +2293,20 @@ const sentinel = (options) => {
|
|
|
2286
2293
|
const body = ctx.body;
|
|
2287
2294
|
const passwordToCheck = body?.newPassword || body?.password;
|
|
2288
2295
|
if (passwordToCheck) {
|
|
2296
|
+
recordCheck(ctx, "compromised_password");
|
|
2289
2297
|
const compromisedResult = await securityService.checkCompromisedPassword(passwordToCheck);
|
|
2290
2298
|
if (compromisedResult.compromised && compromisedResult.action) {
|
|
2291
|
-
if (compromisedResult.action === "block")
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2299
|
+
if (compromisedResult.action === "block") {
|
|
2300
|
+
setOutcome(ctx, "blocked", "compromised_password", { breachCount: compromisedResult.breachCount });
|
|
2301
|
+
emitEvaluation(ctx, trackEvent, {
|
|
2302
|
+
identifier,
|
|
2303
|
+
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2304
|
+
});
|
|
2305
|
+
throw new APIError("BAD_REQUEST", {
|
|
2306
|
+
message: "This password has been found in data breaches. Please choose a different password.",
|
|
2307
|
+
code: "COMPROMISED_PASSWORD"
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2295
2310
|
}
|
|
2296
2311
|
}
|
|
2297
2312
|
}
|
|
@@ -2304,6 +2319,10 @@ const sentinel = (options) => {
|
|
|
2304
2319
|
const visitorId = ctx.context.visitorId;
|
|
2305
2320
|
const identification = ctx.context.identification;
|
|
2306
2321
|
const body = ctx.body;
|
|
2322
|
+
emitEvaluation(ctx, trackEvent, {
|
|
2323
|
+
identifier: body?.email,
|
|
2324
|
+
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2325
|
+
});
|
|
2307
2326
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email && body?.password && visitorId) {
|
|
2308
2327
|
const { email, password } = body;
|
|
2309
2328
|
const ip = identification?.ip || null;
|
|
@@ -2638,32 +2657,32 @@ const JTI_CHECK_GRACE_PERIOD_SECONDS = 30;
|
|
|
2638
2657
|
const JWKS_CACHE_TTL_MS = 900 * 1e3;
|
|
2639
2658
|
const jwksCache = /* @__PURE__ */ new Map();
|
|
2640
2659
|
const inflightRequests = /* @__PURE__ */ new Map();
|
|
2641
|
-
async function fetchJWKS(ctx,
|
|
2642
|
-
const { data, error } = await
|
|
2660
|
+
async function fetchJWKS(ctx, cacheKey, $api) {
|
|
2661
|
+
const { data, error } = await $api("/api/auth/jwks");
|
|
2643
2662
|
if (error || !data) {
|
|
2644
2663
|
ctx.context.logger.warn("[Dash] Failed to fetch JWKS", error);
|
|
2645
|
-
throw
|
|
2664
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2646
2665
|
}
|
|
2647
|
-
jwksCache.set(
|
|
2666
|
+
jwksCache.set(cacheKey, {
|
|
2648
2667
|
data,
|
|
2649
2668
|
expiresAt: Date.now() + JWKS_CACHE_TTL_MS
|
|
2650
2669
|
});
|
|
2651
2670
|
return createLocalJWKSet(data);
|
|
2652
2671
|
}
|
|
2653
|
-
async function prefetchJWKS(ctx,
|
|
2654
|
-
const fetchPromise = fetchJWKS(ctx,
|
|
2655
|
-
inflightRequests.set(
|
|
2656
|
-
fetchPromise.finally(() => inflightRequests.delete(
|
|
2672
|
+
async function prefetchJWKS(ctx, cacheKey, $api) {
|
|
2673
|
+
const fetchPromise = fetchJWKS(ctx, cacheKey, $api);
|
|
2674
|
+
inflightRequests.set(cacheKey, fetchPromise);
|
|
2675
|
+
fetchPromise.finally(() => inflightRequests.delete(cacheKey));
|
|
2657
2676
|
return fetchPromise;
|
|
2658
2677
|
}
|
|
2659
|
-
async function getJWKs(ctx,
|
|
2660
|
-
const cached = jwksCache.get(
|
|
2678
|
+
async function getJWKs(ctx, cacheKey, $api) {
|
|
2679
|
+
const cached = jwksCache.get(cacheKey);
|
|
2661
2680
|
if (cached && Date.now() < cached.expiresAt) return createLocalJWKSet(cached.data);
|
|
2662
2681
|
if (cached) {
|
|
2663
|
-
if (!inflightRequests.has(
|
|
2682
|
+
if (!inflightRequests.has(cacheKey)) prefetchJWKS(ctx, cacheKey, $api);
|
|
2664
2683
|
return createLocalJWKSet(cached.data);
|
|
2665
2684
|
}
|
|
2666
|
-
return inflightRequests.get(
|
|
2685
|
+
return inflightRequests.get(cacheKey) ?? prefetchJWKS(ctx, cacheKey, $api);
|
|
2667
2686
|
}
|
|
2668
2687
|
/**
|
|
2669
2688
|
* Check if JWT is recently issued and can skip JTI verification.
|
|
@@ -2675,86 +2694,92 @@ function isRecentlyIssued(payload) {
|
|
|
2675
2694
|
const issuedAt = payload.iat * 1e3;
|
|
2676
2695
|
return Date.now() - issuedAt < JTI_CHECK_GRACE_PERIOD_SECONDS * 1e3;
|
|
2677
2696
|
}
|
|
2678
|
-
const jwtMiddleware = (options, schema, getJWT) =>
|
|
2679
|
-
const
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
ctx.context.logger.warn("[Dash] JWT verification failed:", e);
|
|
2686
|
-
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2687
|
-
});
|
|
2688
|
-
if (!isRecentlyIssued(payload)) {
|
|
2689
|
-
const { error, data } = await betterFetch("/api/auth/check-jti", {
|
|
2690
|
-
baseURL: options.apiUrl,
|
|
2691
|
-
method: "POST",
|
|
2692
|
-
headers: { "x-api-key": options.apiKey },
|
|
2693
|
-
body: {
|
|
2694
|
-
jti: payload.jti,
|
|
2695
|
-
expiresAt: payload.exp
|
|
2696
|
-
}
|
|
2697
|
-
});
|
|
2698
|
-
if (error || !data?.valid) {
|
|
2699
|
-
ctx.context.logger.warn("[Dash] JTI check failed with error", error, data?.valid);
|
|
2697
|
+
const jwtMiddleware = (options, schema, getJWT) => {
|
|
2698
|
+
const { $api } = options;
|
|
2699
|
+
const cacheKey = options.apiUrl;
|
|
2700
|
+
return createAuthMiddleware(async (ctx) => {
|
|
2701
|
+
const jwsFromHeader = getJWT ? await getJWT(ctx) : ctx.headers?.get("Authorization")?.split(" ")[1];
|
|
2702
|
+
if (!jwsFromHeader) {
|
|
2703
|
+
ctx.context.logger.warn("[Dash] JWT is missing from header");
|
|
2700
2704
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2701
2705
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2706
|
-
apiKeyHash,
|
|
2707
|
-
apiKey: options.apiKey ? "present" : "missing"
|
|
2706
|
+
const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(ctx, cacheKey, $api), { maxTokenAge: "5m" }).catch((e) => {
|
|
2707
|
+
ctx.context.logger.warn("[Dash] JWT verification failed:", e);
|
|
2708
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2708
2709
|
});
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
}
|
|
2716
|
-
if (schema) {
|
|
2717
|
-
const parsed = schema.safeParse(payload);
|
|
2718
|
-
if (!parsed.success) {
|
|
2719
|
-
ctx.context.logger.warn("[Dash] JWT payload is invalid", parsed.error);
|
|
2710
|
+
const apiKeyHash = payload.apiKeyHash;
|
|
2711
|
+
if (typeof apiKeyHash !== "string" || !options.apiKey) {
|
|
2712
|
+
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2713
|
+
apiKeyHash,
|
|
2714
|
+
apiKey: options.apiKey ? "present" : "missing"
|
|
2715
|
+
});
|
|
2720
2716
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2721
2717
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
});
|
|
2718
|
+
const expectedHash = await hash(options.apiKey);
|
|
2719
|
+
if (apiKeyHash !== expectedHash) {
|
|
2720
|
+
ctx.context.logger.warn("[Dash] API key hash is invalid", apiKeyHash, expectedHash);
|
|
2721
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2722
|
+
}
|
|
2723
|
+
if (!isRecentlyIssued(payload)) {
|
|
2724
|
+
const { error, data } = await $api("/api/auth/check-jti", {
|
|
2725
|
+
method: "POST",
|
|
2726
|
+
body: {
|
|
2727
|
+
jti: payload.jti,
|
|
2728
|
+
expiresAt: payload.exp
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
if (error || !data?.valid) {
|
|
2732
|
+
ctx.context.logger.warn("[Dash] JTI check failed with error", error, data?.valid);
|
|
2733
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (schema) {
|
|
2737
|
+
const parsed = schema.safeParse(payload);
|
|
2738
|
+
if (!parsed.success) {
|
|
2739
|
+
ctx.context.logger.warn("[Dash] JWT payload is invalid", parsed.error);
|
|
2740
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2741
|
+
}
|
|
2742
|
+
return { payload: parsed.data };
|
|
2743
|
+
}
|
|
2744
|
+
return { payload };
|
|
2745
|
+
});
|
|
2746
|
+
};
|
|
2726
2747
|
/**
|
|
2727
2748
|
* Lightweight JWT middleware for /dash/validate. Verifies JWT signature and
|
|
2728
2749
|
* apiKeyHash only—no JTI check. Used during onboarding when the org doesn't
|
|
2729
2750
|
* exist yet.
|
|
2730
2751
|
*/
|
|
2731
|
-
const jwtValidateMiddleware = (options) =>
|
|
2732
|
-
const
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2744
|
-
apiKeyHash: typeof apiKeyHash,
|
|
2745
|
-
apiKey: options.apiKey ? "present" : "missing"
|
|
2752
|
+
const jwtValidateMiddleware = (options) => {
|
|
2753
|
+
const { $api } = options;
|
|
2754
|
+
const cacheKey = options.apiUrl;
|
|
2755
|
+
return createAuthMiddleware(async (ctx) => {
|
|
2756
|
+
const jwsFromHeader = ctx.headers?.get("Authorization")?.split(" ")[1];
|
|
2757
|
+
if (!jwsFromHeader) {
|
|
2758
|
+
ctx.context.logger.warn("[Dash] JWT is missing from header");
|
|
2759
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2760
|
+
}
|
|
2761
|
+
const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(ctx, cacheKey, $api), { maxTokenAge: "5m" }).catch((e) => {
|
|
2762
|
+
ctx.context.logger.error("[Dash] JWT verification failed:", e);
|
|
2763
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2746
2764
|
});
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
}
|
|
2765
|
+
const apiKeyHash = payload.apiKeyHash;
|
|
2766
|
+
if (typeof apiKeyHash !== "string" || !options.apiKey) {
|
|
2767
|
+
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2768
|
+
apiKeyHash: typeof apiKeyHash,
|
|
2769
|
+
apiKey: options.apiKey ? "present" : "missing"
|
|
2770
|
+
});
|
|
2771
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2772
|
+
}
|
|
2773
|
+
if (apiKeyHash !== await hash(options.apiKey)) {
|
|
2774
|
+
ctx.context.logger.warn("[Dash] API key hash mismatch");
|
|
2775
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2776
|
+
}
|
|
2777
|
+
return { payload };
|
|
2778
|
+
});
|
|
2779
|
+
};
|
|
2755
2780
|
//#endregion
|
|
2756
2781
|
//#region src/version.ts
|
|
2757
|
-
const PLUGIN_VERSION = "0.2.
|
|
2782
|
+
const PLUGIN_VERSION = "0.2.7";
|
|
2758
2783
|
//#endregion
|
|
2759
2784
|
//#region src/routes/auth/config.ts
|
|
2760
2785
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
@@ -3068,6 +3093,12 @@ function getScimEndpoint(baseUrl) {
|
|
|
3068
3093
|
function getSCIMPlugin(ctx) {
|
|
3069
3094
|
return ctx.context.getPlugin("scim");
|
|
3070
3095
|
}
|
|
3096
|
+
const DIRECTORY_SYNC_DUPLICATE_MESSAGE = "A directory sync connection with this provider ID already exists. Remove the existing connection or choose a different provider ID.";
|
|
3097
|
+
function isDuplicateDirectorySyncError(e) {
|
|
3098
|
+
if (e instanceof APIError$1) return e.status === "CONFLICT" || /duplicate|already exists|unique constraint/i.test(e.message ?? "");
|
|
3099
|
+
if (e instanceof Error) return /unique constraint|duplicate key|already exists|P2002/i.test(e.message);
|
|
3100
|
+
return false;
|
|
3101
|
+
}
|
|
3071
3102
|
const listOrganizationDirectories = (options) => {
|
|
3072
3103
|
return createAuthEndpoint("/dash/organization/:id/directories", {
|
|
3073
3104
|
method: "GET",
|
|
@@ -3118,14 +3149,22 @@ const createOrganizationDirectory = (options) => {
|
|
|
3118
3149
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3119
3150
|
if (!scimPlugin?.endpoints.generateSCIMToken || !scimPlugin?.options?.providerOwnership?.enabled) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3120
3151
|
const { providerId, ownerUserId } = ctx.body;
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3152
|
+
let scimToken;
|
|
3153
|
+
try {
|
|
3154
|
+
scimToken = (await scimPlugin.endpoints.generateSCIMToken({
|
|
3155
|
+
body: {
|
|
3156
|
+
providerId,
|
|
3157
|
+
organizationId
|
|
3158
|
+
},
|
|
3159
|
+
context: await buildSyntheticSession(ctx, ownerUserId),
|
|
3160
|
+
headers: ctx.request?.headers ?? new Headers()
|
|
3161
|
+
})).scimToken;
|
|
3162
|
+
} catch (e) {
|
|
3163
|
+
if (isDuplicateDirectorySyncError(e)) throw ctx.error("BAD_REQUEST", { message: DIRECTORY_SYNC_DUPLICATE_MESSAGE });
|
|
3164
|
+
ctx.context.logger.error("[Dash] Failed to create directory sync provider (organizationId=%s, providerId=%s)", organizationId, providerId, e);
|
|
3165
|
+
if (e instanceof APIError$1) throw e;
|
|
3166
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to create directory sync provider" });
|
|
3167
|
+
}
|
|
3129
3168
|
if (!scimToken) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to create directory sync provider" });
|
|
3130
3169
|
return {
|
|
3131
3170
|
organizationId,
|
|
@@ -3226,16 +3265,16 @@ function transformEvent(raw) {
|
|
|
3226
3265
|
* ```
|
|
3227
3266
|
*/
|
|
3228
3267
|
const getUserEvents = (options) => {
|
|
3229
|
-
const $
|
|
3230
|
-
baseURL: options.apiUrl,
|
|
3231
|
-
headers: { "x-api-key": options.apiKey }
|
|
3232
|
-
});
|
|
3268
|
+
const { $api } = options;
|
|
3233
3269
|
return createAuthEndpoint("/events/list", {
|
|
3234
3270
|
method: "GET",
|
|
3235
3271
|
use: [sessionMiddleware],
|
|
3236
3272
|
query: z$1.object({
|
|
3273
|
+
/** Maximum number of events to return (default: 50, max: 100) */
|
|
3237
3274
|
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
3275
|
+
/** Number of events to skip for pagination (default: 0) */
|
|
3238
3276
|
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
3277
|
+
/** Filter by event type (e.g., "user_signed_in") */
|
|
3239
3278
|
eventType: z$1.string().optional()
|
|
3240
3279
|
}).optional()
|
|
3241
3280
|
}, async (ctx) => {
|
|
@@ -3245,7 +3284,7 @@ const getUserEvents = (options) => {
|
|
|
3245
3284
|
const requestedLimit = ctx.query?.limit ?? 50;
|
|
3246
3285
|
const limit = Math.min(Math.max(1, requestedLimit), 100);
|
|
3247
3286
|
const offset = Math.max(0, ctx.query?.offset ?? 0);
|
|
3248
|
-
const { data, error } = await $
|
|
3287
|
+
const { data, error } = await $api("/events/user", {
|
|
3249
3288
|
method: "GET",
|
|
3250
3289
|
query: {
|
|
3251
3290
|
userId: session.user.id,
|
|
@@ -3295,10 +3334,7 @@ const getEventTypes = (options) => {
|
|
|
3295
3334
|
* results to the resolved user/identifier.
|
|
3296
3335
|
*/
|
|
3297
3336
|
const getAuditLogs = (options) => {
|
|
3298
|
-
const $
|
|
3299
|
-
baseURL: options.apiUrl,
|
|
3300
|
-
headers: { "x-api-key": options.apiKey }
|
|
3301
|
-
});
|
|
3337
|
+
const { $api } = options;
|
|
3302
3338
|
return createAuthEndpoint("/events/audit-logs", {
|
|
3303
3339
|
method: "GET",
|
|
3304
3340
|
use: [sessionMiddleware],
|
|
@@ -3330,7 +3366,7 @@ const getAuditLogs = (options) => {
|
|
|
3330
3366
|
if (!(sessionData.session?.activeOrganizationId === organizationId) && (!Array.isArray(sessionOrganizations) || !sessionOrganizations.some((org) => {
|
|
3331
3367
|
return org.id === organizationId;
|
|
3332
3368
|
}))) throw ctx.error("FORBIDDEN", { message: "Not allowed to access this organization" });
|
|
3333
|
-
const { data, error } = await $
|
|
3369
|
+
const { data, error } = await $api("/events/organization", {
|
|
3334
3370
|
method: "GET",
|
|
3335
3371
|
query: {
|
|
3336
3372
|
organizationId,
|
|
@@ -3347,7 +3383,7 @@ const getAuditLogs = (options) => {
|
|
|
3347
3383
|
responseLimit = data.limit;
|
|
3348
3384
|
responseOffset = data.offset;
|
|
3349
3385
|
} else {
|
|
3350
|
-
const { data, error } = await $
|
|
3386
|
+
const { data, error } = await $api("/events/user", {
|
|
3351
3387
|
method: "GET",
|
|
3352
3388
|
query: {
|
|
3353
3389
|
userId: resolvedUserId,
|
|
@@ -3383,6 +3419,110 @@ const getAuditLogs = (options) => {
|
|
|
3383
3419
|
};
|
|
3384
3420
|
});
|
|
3385
3421
|
};
|
|
3422
|
+
const OWNER_ADMIN_ROLES = new Set(["owner", "admin"]);
|
|
3423
|
+
function isOwnerOrAdminRole(role) {
|
|
3424
|
+
return role !== void 0 && OWNER_ADMIN_ROLES.has(role);
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Get all audit logs the current user has access to
|
|
3428
|
+
* Queries can be restricted to a single organization, a single user, or all organizations the user has access to
|
|
3429
|
+
* @param options
|
|
3430
|
+
* @returns
|
|
3431
|
+
*/
|
|
3432
|
+
const getAllAuditLogs = (options) => {
|
|
3433
|
+
const { $api } = options;
|
|
3434
|
+
const getUserOrganizationRole = async (adapter, orgId, userId) => {
|
|
3435
|
+
return (await adapter.findOne({
|
|
3436
|
+
model: "member",
|
|
3437
|
+
where: [{
|
|
3438
|
+
field: "organizationId",
|
|
3439
|
+
value: orgId
|
|
3440
|
+
}, {
|
|
3441
|
+
field: "userId",
|
|
3442
|
+
value: userId
|
|
3443
|
+
}],
|
|
3444
|
+
select: ["role"]
|
|
3445
|
+
}))?.role;
|
|
3446
|
+
};
|
|
3447
|
+
const listElevatedOrganizationIds = async (adapter, userId) => {
|
|
3448
|
+
const memberships = await adapter.findMany({
|
|
3449
|
+
model: "member",
|
|
3450
|
+
where: [{
|
|
3451
|
+
field: "userId",
|
|
3452
|
+
value: userId
|
|
3453
|
+
}],
|
|
3454
|
+
select: ["organizationId", "role"]
|
|
3455
|
+
});
|
|
3456
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3457
|
+
for (const m of memberships) if (isOwnerOrAdminRole(m.role)) ids.add(m.organizationId);
|
|
3458
|
+
return [...ids];
|
|
3459
|
+
};
|
|
3460
|
+
return createAuthEndpoint("/events/all-audit-logs", {
|
|
3461
|
+
method: "GET",
|
|
3462
|
+
use: [sessionMiddleware],
|
|
3463
|
+
query: z$1.object({
|
|
3464
|
+
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
3465
|
+
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
3466
|
+
userId: z$1.string().optional(),
|
|
3467
|
+
organizationId: z$1.string().optional(),
|
|
3468
|
+
/** Filter by event type (e.g. `organization_member_added`) */
|
|
3469
|
+
eventType: z$1.string().optional(),
|
|
3470
|
+
/** Match `eventData.identifier` (organization-scoped actor identity) */
|
|
3471
|
+
identifier: z$1.string().optional()
|
|
3472
|
+
}).refine((q) => {
|
|
3473
|
+
const u = q.userId?.trim();
|
|
3474
|
+
const o = q.organizationId?.trim();
|
|
3475
|
+
return !(u && o);
|
|
3476
|
+
}, { message: "Provide at most one of userId and organizationId." }).optional()
|
|
3477
|
+
}, async (ctx) => {
|
|
3478
|
+
const session = ctx.context.session;
|
|
3479
|
+
if (!session?.user?.id) throw ctx.error("UNAUTHORIZED", { message: "You must be signed in to view activity logs" });
|
|
3480
|
+
if (!options.apiKey) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Events API is not configured" });
|
|
3481
|
+
const query = ctx.query ?? {};
|
|
3482
|
+
const adapter = ctx.context.adapter;
|
|
3483
|
+
const sessionUserId = session.user.id;
|
|
3484
|
+
const limit = Math.min(Math.max(1, query.limit ?? 50), 100);
|
|
3485
|
+
const offset = Math.max(0, query.offset ?? 0);
|
|
3486
|
+
const organizationId = query.organizationId?.trim();
|
|
3487
|
+
const userId = query.userId?.trim();
|
|
3488
|
+
const forbidAccess = () => {
|
|
3489
|
+
return ctx.error("FORBIDDEN", { message: "Only organization owners and admins can view activity logs." });
|
|
3490
|
+
};
|
|
3491
|
+
const elevatedOrgIds = await listElevatedOrganizationIds(adapter, sessionUserId);
|
|
3492
|
+
if (organizationId) {
|
|
3493
|
+
if (!isOwnerOrAdminRole(await getUserOrganizationRole(adapter, organizationId, sessionUserId))) throw forbidAccess();
|
|
3494
|
+
} else if (userId) {
|
|
3495
|
+
if (elevatedOrgIds.length === 0) throw forbidAccess();
|
|
3496
|
+
} else if (elevatedOrgIds.length === 0) throw forbidAccess();
|
|
3497
|
+
const apiQuery = {
|
|
3498
|
+
limit: limit.toString(),
|
|
3499
|
+
offset: offset.toString()
|
|
3500
|
+
};
|
|
3501
|
+
if (organizationId) apiQuery.organizationIds = organizationId;
|
|
3502
|
+
else if (userId) {
|
|
3503
|
+
apiQuery.userId = userId;
|
|
3504
|
+
apiQuery.organizationIds = elevatedOrgIds.join(",");
|
|
3505
|
+
} else apiQuery.organizationIds = elevatedOrgIds.join(",");
|
|
3506
|
+
const eventTypeFilter = query.eventType?.trim();
|
|
3507
|
+
const identifierFilter = query.identifier?.trim();
|
|
3508
|
+
if (eventTypeFilter) apiQuery.eventType = eventTypeFilter;
|
|
3509
|
+
if (identifierFilter) apiQuery.identifier = identifierFilter;
|
|
3510
|
+
const { data, error } = await $api("/events/activity", {
|
|
3511
|
+
method: "GET",
|
|
3512
|
+
query: apiQuery
|
|
3513
|
+
});
|
|
3514
|
+
if (error || !data) {
|
|
3515
|
+
ctx.context.logger.error("[Dash] Failed to fetch activity logs:", error);
|
|
3516
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch activity logs" });
|
|
3517
|
+
}
|
|
3518
|
+
return {
|
|
3519
|
+
events: data.events.map(transformEvent),
|
|
3520
|
+
total: data.total,
|
|
3521
|
+
limit: data.limit,
|
|
3522
|
+
offset: data.offset
|
|
3523
|
+
};
|
|
3524
|
+
});
|
|
3525
|
+
};
|
|
3386
3526
|
//#endregion
|
|
3387
3527
|
//#region src/routes/execute-adapter/index.ts
|
|
3388
3528
|
const whereClause = z.object({
|
|
@@ -3487,10 +3627,7 @@ const executeAdapter = (options) => {
|
|
|
3487
3627
|
* It creates the user in the auth database and sets up a session.
|
|
3488
3628
|
*/
|
|
3489
3629
|
const acceptInvitation = (options) => {
|
|
3490
|
-
const $api =
|
|
3491
|
-
baseURL: options.apiUrl || INFRA_API_URL,
|
|
3492
|
-
headers: { "x-api-key": options.apiKey }
|
|
3493
|
-
});
|
|
3630
|
+
const { $api } = options;
|
|
3494
3631
|
return createAuthEndpoint("/dash/accept-invitation", {
|
|
3495
3632
|
method: "GET",
|
|
3496
3633
|
query: z$1.object({ token: z$1.string() })
|
|
@@ -3570,10 +3707,7 @@ const acceptInvitation = (options) => {
|
|
|
3570
3707
|
* This creates the user in the auth database and sets up a session
|
|
3571
3708
|
*/
|
|
3572
3709
|
const completeInvitation = (options) => {
|
|
3573
|
-
const $api =
|
|
3574
|
-
baseURL: options.apiUrl || INFRA_API_URL,
|
|
3575
|
-
headers: { "x-api-key": options.apiKey }
|
|
3576
|
-
});
|
|
3710
|
+
const { $api } = options;
|
|
3577
3711
|
return createAuthEndpoint("/dash/complete-invitation", {
|
|
3578
3712
|
method: "POST",
|
|
3579
3713
|
body: z$1.object({
|
|
@@ -6674,7 +6808,7 @@ const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
|
|
|
6674
6808
|
const updateData = ctx.body;
|
|
6675
6809
|
const userId = ctx.context.payload?.userId;
|
|
6676
6810
|
if (!userId) throw new APIError("FORBIDDEN", { message: "Invalid payload" });
|
|
6677
|
-
const filteredData = Object.fromEntries(Object.entries(updateData).filter(([
|
|
6811
|
+
const filteredData = Object.fromEntries(Object.entries(updateData).filter(([, value]) => value !== void 0));
|
|
6678
6812
|
if (Object.keys(filteredData).length === 0) throw new APIError("BAD_REQUEST", { message: "No valid fields to update" });
|
|
6679
6813
|
const user = await ctx.context.internalAdapter.updateUser(userId, {
|
|
6680
6814
|
...filteredData,
|
|
@@ -7400,6 +7534,11 @@ function createSMSSender(config) {
|
|
|
7400
7534
|
const apiUrl = baseUrl.endsWith("/api") ? baseUrl : `${baseUrl}/api`;
|
|
7401
7535
|
const apiKey = config?.apiKey || env.BETTER_AUTH_API_KEY || "";
|
|
7402
7536
|
if (!apiKey) logger.warn("[Dash] No API key provided for SMS sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
|
|
7537
|
+
const $api = createFetch({
|
|
7538
|
+
baseURL: apiUrl,
|
|
7539
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
7540
|
+
timeout: config?.apiTimeout ?? 3e3
|
|
7541
|
+
});
|
|
7403
7542
|
/**
|
|
7404
7543
|
* Send an SMS with OTP code
|
|
7405
7544
|
*/
|
|
@@ -7409,25 +7548,25 @@ function createSMSSender(config) {
|
|
|
7409
7548
|
error: "API key not configured"
|
|
7410
7549
|
};
|
|
7411
7550
|
try {
|
|
7412
|
-
const
|
|
7551
|
+
const { data, error } = await $api("/v1/sms/send", {
|
|
7413
7552
|
method: "POST",
|
|
7414
|
-
|
|
7415
|
-
"Content-Type": "application/json",
|
|
7416
|
-
Authorization: `Bearer ${apiKey}`
|
|
7417
|
-
},
|
|
7418
|
-
body: JSON.stringify({
|
|
7553
|
+
body: {
|
|
7419
7554
|
to: options.to,
|
|
7420
7555
|
code: options.code,
|
|
7421
7556
|
template: options.template
|
|
7422
|
-
}
|
|
7557
|
+
}
|
|
7423
7558
|
});
|
|
7424
|
-
if (
|
|
7559
|
+
if (error) return {
|
|
7425
7560
|
success: false,
|
|
7426
|
-
error:
|
|
7561
|
+
error: error.message || `HTTP ${error.status}`
|
|
7562
|
+
};
|
|
7563
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return {
|
|
7564
|
+
success: false,
|
|
7565
|
+
error: "Failed to parse JSON"
|
|
7427
7566
|
};
|
|
7428
7567
|
return {
|
|
7429
7568
|
success: true,
|
|
7430
|
-
messageId:
|
|
7569
|
+
messageId: data.messageId
|
|
7431
7570
|
};
|
|
7432
7571
|
} catch (error) {
|
|
7433
7572
|
logger.warn("[Dash] SMS send failed:", error);
|
|
@@ -7475,8 +7614,14 @@ async function sendSMS(options, config) {
|
|
|
7475
7614
|
const dash = (options) => {
|
|
7476
7615
|
const processedBulkOperationContexts = /* @__PURE__ */ new WeakSet();
|
|
7477
7616
|
const opts = resolveDashOptions(options);
|
|
7617
|
+
const $api = createAPI(opts);
|
|
7618
|
+
const settings = {
|
|
7619
|
+
...opts,
|
|
7620
|
+
$api
|
|
7621
|
+
};
|
|
7622
|
+
const $kv = createKV(opts);
|
|
7478
7623
|
const activityUpdateInterval = opts.activityTracking?.updateInterval ?? 3e5;
|
|
7479
|
-
const { tracker } = initTrackEvents(
|
|
7624
|
+
const { tracker } = initTrackEvents($api);
|
|
7480
7625
|
const { trackUserSignedUp, trackUserProfileUpdated, trackUserProfileImageUpdated, trackUserEmailVerified, trackUserBanned, trackUserUnBanned, trackUserDeleted } = initUserEvents(tracker);
|
|
7481
7626
|
const { trackEmailVerificationSent, trackEmailSignInAttempt, trackUserSignedIn, trackUserSignedOut, trackSessionCreated, trackSocialSignInAttempt, trackSocialSignInRedirectionAttempt, trackUserImpersonated, trackUserImpersonationStop, trackSessionRevoked, trackSessionRevokedAll } = initSessionEvents(tracker);
|
|
7482
7627
|
const { trackAccountLinking, trackAccountUnlink, trackAccountPasswordChange } = initAccountEvents(tracker);
|
|
@@ -7758,7 +7903,7 @@ const dash = (options) => {
|
|
|
7758
7903
|
const path = new URL(ctx.request.url).pathname;
|
|
7759
7904
|
return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
|
|
7760
7905
|
},
|
|
7761
|
-
handler: createIdentificationMiddleware(
|
|
7906
|
+
handler: createIdentificationMiddleware($kv)
|
|
7762
7907
|
}],
|
|
7763
7908
|
after: [{
|
|
7764
7909
|
matcher: (ctx) => {
|
|
@@ -7773,7 +7918,7 @@ const dash = (options) => {
|
|
|
7773
7918
|
const body = ctx.body;
|
|
7774
7919
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
|
|
7775
7920
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
|
|
7776
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.context.returned instanceof Error) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
7921
|
+
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
7777
7922
|
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
7778
7923
|
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
7779
7924
|
maxAge: 600,
|
|
@@ -7811,78 +7956,79 @@ const dash = (options) => {
|
|
|
7811
7956
|
}]
|
|
7812
7957
|
},
|
|
7813
7958
|
endpoints: {
|
|
7814
|
-
getDashConfig: getConfig(
|
|
7815
|
-
getDashValidate: getValidate(
|
|
7816
|
-
getDashUsers: getUsers(
|
|
7817
|
-
exportDashUsers: exportUsers(
|
|
7818
|
-
createDashUser: createUser(
|
|
7819
|
-
deleteDashUser: deleteUser(
|
|
7820
|
-
deleteManyDashUsers: deleteManyUsers(
|
|
7821
|
-
listDashOrganizations: listOrganizations(
|
|
7822
|
-
exportDashOrganizations: exportOrganizations(
|
|
7823
|
-
getDashOrganization: getOrganization(
|
|
7824
|
-
listDashOrganizationMembers: listOrganizationMembers(
|
|
7825
|
-
listDashOrganizationInvitations: listOrganizationInvitations(
|
|
7826
|
-
listDashOrganizationTeams: listOrganizationTeams(
|
|
7827
|
-
listDashOrganizationSsoProviders: listOrganizationSsoProviders(
|
|
7828
|
-
createDashSsoProvider: createSsoProvider(
|
|
7829
|
-
updateDashSsoProvider: updateSsoProvider(
|
|
7830
|
-
requestDashSsoVerificationToken: requestSsoVerificationToken(
|
|
7831
|
-
verifyDashSsoProviderDomain: verifySsoProviderDomain(
|
|
7832
|
-
deleteDashSsoProvider: deleteSsoProvider(
|
|
7833
|
-
markDashSsoProviderDomainVerified: markSsoProviderDomainVerified(
|
|
7834
|
-
listDashTeamMembers: listTeamMembers(
|
|
7835
|
-
createDashOrganization: createOrganization(
|
|
7836
|
-
deleteDashOrganization: deleteOrganization(
|
|
7837
|
-
deleteManyDashOrganizations: deleteManyOrganizations(
|
|
7838
|
-
getDashOrganizationOptions: getOrganizationOptions(
|
|
7839
|
-
getDashUser: getUserDetails(
|
|
7840
|
-
getDashUserOrganizations: getUserOrganizations(
|
|
7841
|
-
updateDashUser: updateUser(
|
|
7842
|
-
setDashPassword: setPassword(
|
|
7843
|
-
unlinkDashAccount: unlinkAccount(
|
|
7844
|
-
dashRevokeSession: revokeSession(
|
|
7845
|
-
dashRevokeAllSessions: revokeAllSessions(
|
|
7846
|
-
dashRevokeManySessions: revokeManySessions(
|
|
7847
|
-
dashImpersonateUser: impersonateUser(
|
|
7848
|
-
updateDashOrganization: updateOrganization(
|
|
7849
|
-
createDashTeam: createTeam(
|
|
7850
|
-
updateDashTeam: updateTeam(
|
|
7851
|
-
deleteDashTeam: deleteTeam(
|
|
7852
|
-
addDashTeamMember: addTeamMember(
|
|
7853
|
-
removeDashTeamMember: removeTeamMember(
|
|
7854
|
-
addDashMember: addMember(
|
|
7855
|
-
removeDashMember: removeMember(
|
|
7856
|
-
updateDashMemberRole: updateMemberRole(
|
|
7857
|
-
inviteDashMember: inviteMember(
|
|
7858
|
-
cancelDashInvitation: cancelInvitation(
|
|
7859
|
-
resendDashInvitation: resendInvitation(
|
|
7860
|
-
dashCheckUserByEmail: checkUserByEmail(
|
|
7861
|
-
dashGetUserStats: getUserStats(
|
|
7862
|
-
dashGetUserGraphData: getUserGraphData(
|
|
7863
|
-
dashGetUserRetentionData: getUserRetentionData(
|
|
7864
|
-
dashBanUser: banUser(
|
|
7865
|
-
dashBanManyUsers: banManyUsers(
|
|
7866
|
-
dashUnbanUser: unbanUser(
|
|
7867
|
-
dashSendVerificationEmail: sendVerificationEmail(
|
|
7868
|
-
dashSendManyVerificationEmails: sendManyVerificationEmails(
|
|
7869
|
-
dashSendResetPasswordEmail: sendResetPasswordEmail(
|
|
7870
|
-
dashEnableTwoFactor: enableTwoFactor(
|
|
7871
|
-
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(
|
|
7872
|
-
dashViewBackupCodes: viewBackupCodes(
|
|
7873
|
-
dashDisableTwoFactor: disableTwoFactor(
|
|
7874
|
-
dashGenerateBackupCodes: generateBackupCodes(
|
|
7875
|
-
getUserEvents: getUserEvents(
|
|
7876
|
-
getAuditLogs: getAuditLogs(
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7959
|
+
getDashConfig: getConfig(settings),
|
|
7960
|
+
getDashValidate: getValidate(settings),
|
|
7961
|
+
getDashUsers: getUsers(settings),
|
|
7962
|
+
exportDashUsers: exportUsers(settings),
|
|
7963
|
+
createDashUser: createUser(settings),
|
|
7964
|
+
deleteDashUser: deleteUser(settings),
|
|
7965
|
+
deleteManyDashUsers: deleteManyUsers(settings),
|
|
7966
|
+
listDashOrganizations: listOrganizations(settings),
|
|
7967
|
+
exportDashOrganizations: exportOrganizations(settings),
|
|
7968
|
+
getDashOrganization: getOrganization(settings),
|
|
7969
|
+
listDashOrganizationMembers: listOrganizationMembers(settings),
|
|
7970
|
+
listDashOrganizationInvitations: listOrganizationInvitations(settings),
|
|
7971
|
+
listDashOrganizationTeams: listOrganizationTeams(settings),
|
|
7972
|
+
listDashOrganizationSsoProviders: listOrganizationSsoProviders(settings),
|
|
7973
|
+
createDashSsoProvider: createSsoProvider(settings),
|
|
7974
|
+
updateDashSsoProvider: updateSsoProvider(settings),
|
|
7975
|
+
requestDashSsoVerificationToken: requestSsoVerificationToken(settings),
|
|
7976
|
+
verifyDashSsoProviderDomain: verifySsoProviderDomain(settings),
|
|
7977
|
+
deleteDashSsoProvider: deleteSsoProvider(settings),
|
|
7978
|
+
markDashSsoProviderDomainVerified: markSsoProviderDomainVerified(settings),
|
|
7979
|
+
listDashTeamMembers: listTeamMembers(settings),
|
|
7980
|
+
createDashOrganization: createOrganization(settings),
|
|
7981
|
+
deleteDashOrganization: deleteOrganization(settings),
|
|
7982
|
+
deleteManyDashOrganizations: deleteManyOrganizations(settings),
|
|
7983
|
+
getDashOrganizationOptions: getOrganizationOptions(settings),
|
|
7984
|
+
getDashUser: getUserDetails(settings),
|
|
7985
|
+
getDashUserOrganizations: getUserOrganizations(settings),
|
|
7986
|
+
updateDashUser: updateUser(settings),
|
|
7987
|
+
setDashPassword: setPassword(settings),
|
|
7988
|
+
unlinkDashAccount: unlinkAccount(settings),
|
|
7989
|
+
dashRevokeSession: revokeSession(settings),
|
|
7990
|
+
dashRevokeAllSessions: revokeAllSessions(settings),
|
|
7991
|
+
dashRevokeManySessions: revokeManySessions(settings),
|
|
7992
|
+
dashImpersonateUser: impersonateUser(settings),
|
|
7993
|
+
updateDashOrganization: updateOrganization(settings),
|
|
7994
|
+
createDashTeam: createTeam(settings),
|
|
7995
|
+
updateDashTeam: updateTeam(settings),
|
|
7996
|
+
deleteDashTeam: deleteTeam(settings),
|
|
7997
|
+
addDashTeamMember: addTeamMember(settings),
|
|
7998
|
+
removeDashTeamMember: removeTeamMember(settings),
|
|
7999
|
+
addDashMember: addMember(settings),
|
|
8000
|
+
removeDashMember: removeMember(settings),
|
|
8001
|
+
updateDashMemberRole: updateMemberRole(settings),
|
|
8002
|
+
inviteDashMember: inviteMember(settings),
|
|
8003
|
+
cancelDashInvitation: cancelInvitation(settings),
|
|
8004
|
+
resendDashInvitation: resendInvitation(settings),
|
|
8005
|
+
dashCheckUserByEmail: checkUserByEmail(settings),
|
|
8006
|
+
dashGetUserStats: getUserStats(settings),
|
|
8007
|
+
dashGetUserGraphData: getUserGraphData(settings),
|
|
8008
|
+
dashGetUserRetentionData: getUserRetentionData(settings),
|
|
8009
|
+
dashBanUser: banUser(settings),
|
|
8010
|
+
dashBanManyUsers: banManyUsers(settings),
|
|
8011
|
+
dashUnbanUser: unbanUser(settings),
|
|
8012
|
+
dashSendVerificationEmail: sendVerificationEmail(settings),
|
|
8013
|
+
dashSendManyVerificationEmails: sendManyVerificationEmails(settings),
|
|
8014
|
+
dashSendResetPasswordEmail: sendResetPasswordEmail(settings),
|
|
8015
|
+
dashEnableTwoFactor: enableTwoFactor(settings),
|
|
8016
|
+
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(settings),
|
|
8017
|
+
dashViewBackupCodes: viewBackupCodes(settings),
|
|
8018
|
+
dashDisableTwoFactor: disableTwoFactor(settings),
|
|
8019
|
+
dashGenerateBackupCodes: generateBackupCodes(settings),
|
|
8020
|
+
getUserEvents: getUserEvents(settings),
|
|
8021
|
+
getAuditLogs: getAuditLogs(settings),
|
|
8022
|
+
getAllAuditLogs: getAllAuditLogs(settings),
|
|
8023
|
+
getEventTypes: getEventTypes(settings),
|
|
8024
|
+
dashAcceptInvitation: acceptInvitation(settings),
|
|
8025
|
+
dashCompleteInvitation: completeInvitation(settings),
|
|
8026
|
+
dashCheckUserExists: checkUserExists(settings),
|
|
8027
|
+
listDashOrganizationDirectories: listOrganizationDirectories(settings),
|
|
8028
|
+
createDashOrganizationDirectory: createOrganizationDirectory(settings),
|
|
8029
|
+
deleteDashOrganizationDirectory: deleteOrganizationDirectory(settings),
|
|
8030
|
+
regenerateDashDirectoryToken: regenerateDirectoryToken(settings),
|
|
8031
|
+
dashExecuteAdapter: executeAdapter(settings)
|
|
7886
8032
|
},
|
|
7887
8033
|
schema: opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: {
|
|
7888
8034
|
type: "date",
|