@better-auth/infra 0.2.6 → 0.2.8
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 +6 -1
- package/dist/client.mjs +34 -20
- package/dist/{constants-DdWGfvz1.mjs → constants-CvriWQVc.mjs} +6 -2
- 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 +43 -10
- package/dist/index.mjs +305 -295
- package/dist/native.d.mts +6 -1
- package/dist/native.mjs +28 -15
- package/dist/{pow-CT9ehp8e.mjs → pow-retry-BTL4g3RP.mjs} +23 -21
- package/package.json +2 -2
- /package/dist/{dash-client-CVCHbtoB.d.mts → dash-client-B6U89e1S.d.mts} +0 -0
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
|
|
@@ -512,7 +535,7 @@ const initSessionEvents = (tracker) => {
|
|
|
512
535
|
eventData: {
|
|
513
536
|
userId: session.userId,
|
|
514
537
|
userName: user.name,
|
|
515
|
-
userEmail: user.email,
|
|
538
|
+
userEmail: user.email ?? "unknown",
|
|
516
539
|
sessionId: session.id,
|
|
517
540
|
triggeredBy: trigger.triggeredBy,
|
|
518
541
|
triggerContext: trigger.triggerContext
|
|
@@ -598,10 +621,10 @@ const initUserEvents = (tracker) => {
|
|
|
598
621
|
trackEvent({
|
|
599
622
|
eventKey: user.id,
|
|
600
623
|
eventType: EVENT_TYPES.USER_CREATED,
|
|
601
|
-
eventDisplayName: `${user.name || user.email} signed up`,
|
|
624
|
+
eventDisplayName: `${user.name || user.email || "unknown"} signed up`,
|
|
602
625
|
eventData: {
|
|
603
626
|
userId: user.id,
|
|
604
|
-
userEmail: user.email,
|
|
627
|
+
userEmail: user.email ?? "unknown",
|
|
605
628
|
userName: user.name,
|
|
606
629
|
triggeredBy: trigger.triggeredBy,
|
|
607
630
|
triggerContext: trigger.triggerContext
|
|
@@ -619,7 +642,7 @@ const initUserEvents = (tracker) => {
|
|
|
619
642
|
eventDisplayName: "User deleted",
|
|
620
643
|
eventData: {
|
|
621
644
|
userId: user.id,
|
|
622
|
-
userEmail: user.email,
|
|
645
|
+
userEmail: user.email ?? "unknown",
|
|
623
646
|
userName: user.name,
|
|
624
647
|
triggeredBy: trigger.triggeredBy,
|
|
625
648
|
triggerContext: trigger.triggerContext
|
|
@@ -637,7 +660,7 @@ const initUserEvents = (tracker) => {
|
|
|
637
660
|
eventDisplayName: "Profile updated",
|
|
638
661
|
eventData: {
|
|
639
662
|
userId: user.id,
|
|
640
|
-
userEmail: user.email,
|
|
663
|
+
userEmail: user.email ?? "unknown",
|
|
641
664
|
userName: user.name,
|
|
642
665
|
updatedFields: Object.keys(ctx?.body || {}),
|
|
643
666
|
triggeredBy: trigger.triggeredBy,
|
|
@@ -656,7 +679,7 @@ const initUserEvents = (tracker) => {
|
|
|
656
679
|
eventDisplayName: "Profile image updated",
|
|
657
680
|
eventData: {
|
|
658
681
|
userId: user.id,
|
|
659
|
-
userEmail: user.email,
|
|
682
|
+
userEmail: user.email ?? "unknown",
|
|
660
683
|
userName: user.name,
|
|
661
684
|
triggeredBy: trigger.triggeredBy,
|
|
662
685
|
triggerContext: trigger.triggerContext
|
|
@@ -676,7 +699,7 @@ const initUserEvents = (tracker) => {
|
|
|
676
699
|
eventDisplayName: `User banned${reasonSuffix}${expiresSuffix}`,
|
|
677
700
|
eventData: {
|
|
678
701
|
userId: user.id,
|
|
679
|
-
userEmail: user.email,
|
|
702
|
+
userEmail: user.email ?? "unknown",
|
|
680
703
|
userName: user.name,
|
|
681
704
|
banned: user.banned,
|
|
682
705
|
banReason: user.banReason,
|
|
@@ -697,7 +720,7 @@ const initUserEvents = (tracker) => {
|
|
|
697
720
|
eventDisplayName: "User unbanned",
|
|
698
721
|
eventData: {
|
|
699
722
|
userId: user.id,
|
|
700
|
-
userEmail: user.email,
|
|
723
|
+
userEmail: user.email ?? "unknown",
|
|
701
724
|
userName: user.name,
|
|
702
725
|
banned: user.banned,
|
|
703
726
|
triggeredBy: trigger.triggeredBy,
|
|
@@ -716,7 +739,7 @@ const initUserEvents = (tracker) => {
|
|
|
716
739
|
eventDisplayName: "Email verified",
|
|
717
740
|
eventData: {
|
|
718
741
|
userId: user.id,
|
|
719
|
-
userEmail: user.email,
|
|
742
|
+
userEmail: user.email ?? "unknown",
|
|
720
743
|
userName: user.name,
|
|
721
744
|
triggeredBy: trigger.triggeredBy,
|
|
722
745
|
triggerContext: trigger.triggerContext
|
|
@@ -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;
|
|
@@ -1064,23 +1078,19 @@ async function sha1Hash(input) {
|
|
|
1064
1078
|
/**
|
|
1065
1079
|
* Whether Sentinel should normalize emails for deduplication/sign-in consistency.
|
|
1066
1080
|
* If `emailNormalization` is set, it wins; otherwise legacy behavior uses `emailValidation.enabled`.
|
|
1067
|
-
* @internal Not part of the package public API; used by the sentinel plugin and email helpers.
|
|
1068
1081
|
*/
|
|
1069
1082
|
function isEmailNormalizationEnabled(security) {
|
|
1070
1083
|
const explicit = security?.emailNormalization;
|
|
1071
1084
|
if (explicit !== void 0) return explicit.enabled !== false;
|
|
1072
1085
|
return security?.emailValidation?.enabled !== false;
|
|
1073
1086
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
headers: { "x-api-key": apiKey },
|
|
1079
|
-
throw: true
|
|
1080
|
-
});
|
|
1087
|
+
/**
|
|
1088
|
+
* @param $api — Dash client from `createAPI(opts, { throw: true })`.
|
|
1089
|
+
*/
|
|
1090
|
+
function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
1081
1091
|
const emailSender = createEmailSender({
|
|
1082
|
-
apiUrl:
|
|
1083
|
-
apiKey
|
|
1092
|
+
apiUrl: conn.apiUrl || INFRA_API_URL,
|
|
1093
|
+
apiKey: conn.apiKey
|
|
1084
1094
|
});
|
|
1085
1095
|
function logEvent(event) {
|
|
1086
1096
|
const fullEvent = {
|
|
@@ -1485,7 +1495,9 @@ const all = new Set([
|
|
|
1485
1495
|
"/forget-password",
|
|
1486
1496
|
"/request-password-reset",
|
|
1487
1497
|
"/send-verification-email",
|
|
1488
|
-
"/change-email"
|
|
1498
|
+
"/change-email",
|
|
1499
|
+
"/organization/invite-member",
|
|
1500
|
+
"/dash/organization/invite-member"
|
|
1489
1501
|
]);
|
|
1490
1502
|
/**
|
|
1491
1503
|
* Path is one of `[
|
|
@@ -1502,7 +1514,9 @@ const all = new Set([
|
|
|
1502
1514
|
* '/forget-password',
|
|
1503
1515
|
* '/request-password-reset',
|
|
1504
1516
|
* '/send-verification-email',
|
|
1505
|
-
* '/change-email'
|
|
1517
|
+
* '/change-email',
|
|
1518
|
+
* '/organization/invite-member',
|
|
1519
|
+
* '/dash/organization/invite-member'
|
|
1506
1520
|
* ]`.
|
|
1507
1521
|
* @param context Request context
|
|
1508
1522
|
* @param context.path Request path
|
|
@@ -1560,17 +1574,11 @@ function normalizeEmail(email, context) {
|
|
|
1560
1574
|
if (GMAIL_LIKE_DOMAINS.has(domain)) localPart = localPart.replace(/\./g, "");
|
|
1561
1575
|
return `${localPart}@${domain}`;
|
|
1562
1576
|
}
|
|
1563
|
-
function
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
});
|
|
1569
|
-
const $kv = createFetch({
|
|
1570
|
-
baseURL: kvUrl,
|
|
1571
|
-
headers: { "x-api-key": apiKey },
|
|
1572
|
-
timeout: KV_TIMEOUT_MS
|
|
1573
|
-
});
|
|
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;
|
|
1574
1582
|
/**
|
|
1575
1583
|
* Fetch and resolve email validity policy from API with caching
|
|
1576
1584
|
* Sends client config to API which merges with user's dashboard settings
|
|
@@ -1744,18 +1752,10 @@ function createEmailValidationHook(validator, trackEvent) {
|
|
|
1744
1752
|
*
|
|
1745
1753
|
* @example
|
|
1746
1754
|
* // API-backed validation
|
|
1747
|
-
* createEmailHooks({
|
|
1748
|
-
*
|
|
1749
|
-
* @example
|
|
1750
|
-
* // High strictness + API
|
|
1751
|
-
* createEmailHooks({
|
|
1752
|
-
* emailValidationOptions: { strictness: "high" },
|
|
1753
|
-
* useApi: true,
|
|
1754
|
-
* apiKey: "your-api-key",
|
|
1755
|
-
* })
|
|
1755
|
+
* createEmailHooks({ $api, $kv })
|
|
1756
1756
|
*/
|
|
1757
1757
|
function createEmailHooks(options = {}) {
|
|
1758
|
-
const { emailValidationOptions, emailNormalizationOptions,
|
|
1758
|
+
const { emailValidationOptions, emailNormalizationOptions, trackEvent } = options;
|
|
1759
1759
|
const emailConfig = {
|
|
1760
1760
|
enabled: true,
|
|
1761
1761
|
strictness: "medium",
|
|
@@ -1765,10 +1765,9 @@ function createEmailHooks(options = {}) {
|
|
|
1765
1765
|
return { before: [...isEmailNormalizationEnabled({
|
|
1766
1766
|
emailValidation: emailValidationOptions,
|
|
1767
1767
|
emailNormalization: emailNormalizationOptions
|
|
1768
|
-
}) ? [createEmailNormalizationHook()] : [], ...emailConfig.enabled ? [createEmailValidationHook(
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
apiKey,
|
|
1768
|
+
}) ? [createEmailNormalizationHook()] : [], ...emailConfig.enabled ? [createEmailValidationHook(hasRemoteEmailClients(options) ? createEmailValidator({
|
|
1769
|
+
$api: options.$api,
|
|
1770
|
+
$kv: options.$kv,
|
|
1772
1771
|
emailValidationOptions: emailConfig
|
|
1773
1772
|
}) : void 0, trackEvent)] : []] };
|
|
1774
1773
|
}
|
|
@@ -2050,10 +2049,13 @@ async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent,
|
|
|
2050
2049
|
//#region src/sentinel/sentinel.ts
|
|
2051
2050
|
const sentinel = (options) => {
|
|
2052
2051
|
const opts = resolveSentinelOptions(options);
|
|
2053
|
-
const
|
|
2052
|
+
const $api = createAPI(opts);
|
|
2053
|
+
const $apiThrowing = createAPI(opts, { throw: true });
|
|
2054
|
+
const $kv = createKV(opts);
|
|
2055
|
+
const { tracker } = initTrackEvents($api);
|
|
2054
2056
|
const { trackEvent } = tracker;
|
|
2055
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.");
|
|
2056
|
-
const securityService = createSecurityClient(opts
|
|
2058
|
+
const securityService = createSecurityClient(opts, $apiThrowing, opts.security || {}, (event) => {
|
|
2057
2059
|
trackEvent({
|
|
2058
2060
|
eventKey: event.visitorId || event.userId || "unknown",
|
|
2059
2061
|
eventType: "security_signal",
|
|
@@ -2072,11 +2074,11 @@ const sentinel = (options) => {
|
|
|
2072
2074
|
const emailHooks = createEmailHooks({
|
|
2073
2075
|
emailValidationOptions: opts.security?.emailValidation,
|
|
2074
2076
|
emailNormalizationOptions: opts.security?.emailNormalization,
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2077
|
+
trackEvent,
|
|
2078
|
+
...opts.apiKey ? {
|
|
2079
|
+
$api,
|
|
2080
|
+
$kv
|
|
2081
|
+
} : {}
|
|
2080
2082
|
});
|
|
2081
2083
|
return {
|
|
2082
2084
|
id: "sentinel",
|
|
@@ -2209,7 +2211,7 @@ const sentinel = (options) => {
|
|
|
2209
2211
|
before: [
|
|
2210
2212
|
{
|
|
2211
2213
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2212
|
-
handler: createIdentificationMiddleware(
|
|
2214
|
+
handler: createIdentificationMiddleware($kv)
|
|
2213
2215
|
},
|
|
2214
2216
|
{
|
|
2215
2217
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
@@ -2354,7 +2356,7 @@ const initInvitationEvents = (tracker) => {
|
|
|
2354
2356
|
inviteeTeamId: invitation.teamId,
|
|
2355
2357
|
inviterId: inviter.id,
|
|
2356
2358
|
inviterName: inviter.name,
|
|
2357
|
-
inviterEmail: inviter.email,
|
|
2359
|
+
inviterEmail: inviter.email ?? "unknown",
|
|
2358
2360
|
triggeredBy: trigger.triggeredBy,
|
|
2359
2361
|
triggerContext: trigger.triggerContext
|
|
2360
2362
|
}
|
|
@@ -2374,7 +2376,7 @@ const initInvitationEvents = (tracker) => {
|
|
|
2374
2376
|
inviteeRole: invitation.role,
|
|
2375
2377
|
inviteeTeamId: invitation.teamId,
|
|
2376
2378
|
acceptedById: acceptedBy.id,
|
|
2377
|
-
acceptedByEmail: acceptedBy.email,
|
|
2379
|
+
acceptedByEmail: acceptedBy.email ?? "unknown",
|
|
2378
2380
|
acceptedByName: acceptedBy.name,
|
|
2379
2381
|
memberId: member.id,
|
|
2380
2382
|
memberRole: member.role,
|
|
@@ -2397,7 +2399,7 @@ const initInvitationEvents = (tracker) => {
|
|
|
2397
2399
|
inviteeRole: invitation.role,
|
|
2398
2400
|
inviteeTeamId: invitation.teamId,
|
|
2399
2401
|
rejectedById: rejectedBy.id,
|
|
2400
|
-
rejectedByEmail: rejectedBy.email,
|
|
2402
|
+
rejectedByEmail: rejectedBy.email ?? "unknown",
|
|
2401
2403
|
rejectedByName: rejectedBy.name,
|
|
2402
2404
|
triggeredBy: trigger.triggeredBy,
|
|
2403
2405
|
triggerContext: trigger.triggerContext
|
|
@@ -2419,7 +2421,7 @@ const initInvitationEvents = (tracker) => {
|
|
|
2419
2421
|
inviteeTeamId: invitation.teamId,
|
|
2420
2422
|
cancelledById: cancelledBy.id,
|
|
2421
2423
|
cancelledByName: cancelledBy.name,
|
|
2422
|
-
cancelledByEmail: cancelledBy.email,
|
|
2424
|
+
cancelledByEmail: cancelledBy.email ?? "unknown",
|
|
2423
2425
|
triggeredBy: trigger.triggeredBy,
|
|
2424
2426
|
triggerContext: trigger.triggerContext
|
|
2425
2427
|
}
|
|
@@ -2449,7 +2451,7 @@ const initMemberEvents = (tracker) => {
|
|
|
2449
2451
|
memberName: user.name,
|
|
2450
2452
|
role: member.role,
|
|
2451
2453
|
memberId: member.id,
|
|
2452
|
-
memberEmail: user.email,
|
|
2454
|
+
memberEmail: user.email ?? "unknown",
|
|
2453
2455
|
triggeredBy: trigger.triggeredBy,
|
|
2454
2456
|
triggerContext: trigger.triggerContext
|
|
2455
2457
|
}
|
|
@@ -2468,7 +2470,7 @@ const initMemberEvents = (tracker) => {
|
|
|
2468
2470
|
memberName: user.name,
|
|
2469
2471
|
role: member.role,
|
|
2470
2472
|
memberId: member.id,
|
|
2471
|
-
memberEmail: user.email,
|
|
2473
|
+
memberEmail: user.email ?? "unknown",
|
|
2472
2474
|
triggeredBy: trigger.triggeredBy,
|
|
2473
2475
|
triggerContext: trigger.triggerContext
|
|
2474
2476
|
}
|
|
@@ -2488,7 +2490,7 @@ const initMemberEvents = (tracker) => {
|
|
|
2488
2490
|
newRole: member.role,
|
|
2489
2491
|
oldRole: previousRole,
|
|
2490
2492
|
memberId: member.id,
|
|
2491
|
-
memberEmail: user.email,
|
|
2493
|
+
memberEmail: user.email ?? "unknown",
|
|
2492
2494
|
triggeredBy: trigger.triggeredBy,
|
|
2493
2495
|
triggerContext: trigger.triggerContext
|
|
2494
2496
|
}
|
|
@@ -2655,32 +2657,32 @@ const JTI_CHECK_GRACE_PERIOD_SECONDS = 30;
|
|
|
2655
2657
|
const JWKS_CACHE_TTL_MS = 900 * 1e3;
|
|
2656
2658
|
const jwksCache = /* @__PURE__ */ new Map();
|
|
2657
2659
|
const inflightRequests = /* @__PURE__ */ new Map();
|
|
2658
|
-
async function fetchJWKS(ctx,
|
|
2659
|
-
const { data, error } = await
|
|
2660
|
+
async function fetchJWKS(ctx, cacheKey, $api) {
|
|
2661
|
+
const { data, error } = await $api("/api/auth/jwks");
|
|
2660
2662
|
if (error || !data) {
|
|
2661
2663
|
ctx.context.logger.warn("[Dash] Failed to fetch JWKS", error);
|
|
2662
|
-
throw
|
|
2664
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2663
2665
|
}
|
|
2664
|
-
jwksCache.set(
|
|
2666
|
+
jwksCache.set(cacheKey, {
|
|
2665
2667
|
data,
|
|
2666
2668
|
expiresAt: Date.now() + JWKS_CACHE_TTL_MS
|
|
2667
2669
|
});
|
|
2668
2670
|
return createLocalJWKSet(data);
|
|
2669
2671
|
}
|
|
2670
|
-
async function prefetchJWKS(ctx,
|
|
2671
|
-
const fetchPromise = fetchJWKS(ctx,
|
|
2672
|
-
inflightRequests.set(
|
|
2673
|
-
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));
|
|
2674
2676
|
return fetchPromise;
|
|
2675
2677
|
}
|
|
2676
|
-
async function getJWKs(ctx,
|
|
2677
|
-
const cached = jwksCache.get(
|
|
2678
|
+
async function getJWKs(ctx, cacheKey, $api) {
|
|
2679
|
+
const cached = jwksCache.get(cacheKey);
|
|
2678
2680
|
if (cached && Date.now() < cached.expiresAt) return createLocalJWKSet(cached.data);
|
|
2679
2681
|
if (cached) {
|
|
2680
|
-
if (!inflightRequests.has(
|
|
2682
|
+
if (!inflightRequests.has(cacheKey)) prefetchJWKS(ctx, cacheKey, $api);
|
|
2681
2683
|
return createLocalJWKSet(cached.data);
|
|
2682
2684
|
}
|
|
2683
|
-
return inflightRequests.get(
|
|
2685
|
+
return inflightRequests.get(cacheKey) ?? prefetchJWKS(ctx, cacheKey, $api);
|
|
2684
2686
|
}
|
|
2685
2687
|
/**
|
|
2686
2688
|
* Check if JWT is recently issued and can skip JTI verification.
|
|
@@ -2692,86 +2694,92 @@ function isRecentlyIssued(payload) {
|
|
|
2692
2694
|
const issuedAt = payload.iat * 1e3;
|
|
2693
2695
|
return Date.now() - issuedAt < JTI_CHECK_GRACE_PERIOD_SECONDS * 1e3;
|
|
2694
2696
|
}
|
|
2695
|
-
const jwtMiddleware = (options, schema, getJWT) =>
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
ctx.context.logger.warn("[Dash] JWT verification failed:", e);
|
|
2703
|
-
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2704
|
-
});
|
|
2705
|
-
if (!isRecentlyIssued(payload)) {
|
|
2706
|
-
const { error, data } = await betterFetch("/api/auth/check-jti", {
|
|
2707
|
-
baseURL: options.apiUrl,
|
|
2708
|
-
method: "POST",
|
|
2709
|
-
headers: { "x-api-key": options.apiKey },
|
|
2710
|
-
body: {
|
|
2711
|
-
jti: payload.jti,
|
|
2712
|
-
expiresAt: payload.exp
|
|
2713
|
-
}
|
|
2714
|
-
});
|
|
2715
|
-
if (error || !data?.valid) {
|
|
2716
|
-
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");
|
|
2717
2704
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2718
2705
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2723
|
-
apiKeyHash,
|
|
2724
|
-
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" });
|
|
2725
2709
|
});
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
}
|
|
2733
|
-
if (schema) {
|
|
2734
|
-
const parsed = schema.safeParse(payload);
|
|
2735
|
-
if (!parsed.success) {
|
|
2736
|
-
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
|
+
});
|
|
2737
2716
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2738
2717
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
});
|
|
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
|
+
};
|
|
2743
2747
|
/**
|
|
2744
2748
|
* Lightweight JWT middleware for /dash/validate. Verifies JWT signature and
|
|
2745
2749
|
* apiKeyHash only—no JTI check. Used during onboarding when the org doesn't
|
|
2746
2750
|
* exist yet.
|
|
2747
2751
|
*/
|
|
2748
|
-
const jwtValidateMiddleware = (options) =>
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2761
|
-
apiKeyHash: typeof apiKeyHash,
|
|
2762
|
-
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" });
|
|
2763
2764
|
});
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
}
|
|
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
|
+
};
|
|
2772
2780
|
//#endregion
|
|
2773
2781
|
//#region src/version.ts
|
|
2774
|
-
const PLUGIN_VERSION = "0.2.
|
|
2782
|
+
const PLUGIN_VERSION = "0.2.8";
|
|
2775
2783
|
//#endregion
|
|
2776
2784
|
//#region src/routes/auth/config.ts
|
|
2777
2785
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
@@ -3257,10 +3265,7 @@ function transformEvent(raw) {
|
|
|
3257
3265
|
* ```
|
|
3258
3266
|
*/
|
|
3259
3267
|
const getUserEvents = (options) => {
|
|
3260
|
-
const $
|
|
3261
|
-
baseURL: options.apiUrl,
|
|
3262
|
-
headers: { "x-api-key": options.apiKey }
|
|
3263
|
-
});
|
|
3268
|
+
const { $api } = options;
|
|
3264
3269
|
return createAuthEndpoint("/events/list", {
|
|
3265
3270
|
method: "GET",
|
|
3266
3271
|
use: [sessionMiddleware],
|
|
@@ -3279,7 +3284,7 @@ const getUserEvents = (options) => {
|
|
|
3279
3284
|
const requestedLimit = ctx.query?.limit ?? 50;
|
|
3280
3285
|
const limit = Math.min(Math.max(1, requestedLimit), 100);
|
|
3281
3286
|
const offset = Math.max(0, ctx.query?.offset ?? 0);
|
|
3282
|
-
const { data, error } = await $
|
|
3287
|
+
const { data, error } = await $api("/events/user", {
|
|
3283
3288
|
method: "GET",
|
|
3284
3289
|
query: {
|
|
3285
3290
|
userId: session.user.id,
|
|
@@ -3329,10 +3334,7 @@ const getEventTypes = (options) => {
|
|
|
3329
3334
|
* results to the resolved user/identifier.
|
|
3330
3335
|
*/
|
|
3331
3336
|
const getAuditLogs = (options) => {
|
|
3332
|
-
const $
|
|
3333
|
-
baseURL: options.apiUrl,
|
|
3334
|
-
headers: { "x-api-key": options.apiKey }
|
|
3335
|
-
});
|
|
3337
|
+
const { $api } = options;
|
|
3336
3338
|
return createAuthEndpoint("/events/audit-logs", {
|
|
3337
3339
|
method: "GET",
|
|
3338
3340
|
use: [sessionMiddleware],
|
|
@@ -3364,7 +3366,7 @@ const getAuditLogs = (options) => {
|
|
|
3364
3366
|
if (!(sessionData.session?.activeOrganizationId === organizationId) && (!Array.isArray(sessionOrganizations) || !sessionOrganizations.some((org) => {
|
|
3365
3367
|
return org.id === organizationId;
|
|
3366
3368
|
}))) throw ctx.error("FORBIDDEN", { message: "Not allowed to access this organization" });
|
|
3367
|
-
const { data, error } = await $
|
|
3369
|
+
const { data, error } = await $api("/events/organization", {
|
|
3368
3370
|
method: "GET",
|
|
3369
3371
|
query: {
|
|
3370
3372
|
organizationId,
|
|
@@ -3381,7 +3383,7 @@ const getAuditLogs = (options) => {
|
|
|
3381
3383
|
responseLimit = data.limit;
|
|
3382
3384
|
responseOffset = data.offset;
|
|
3383
3385
|
} else {
|
|
3384
|
-
const { data, error } = await $
|
|
3386
|
+
const { data, error } = await $api("/events/user", {
|
|
3385
3387
|
method: "GET",
|
|
3386
3388
|
query: {
|
|
3387
3389
|
userId: resolvedUserId,
|
|
@@ -3428,10 +3430,7 @@ function isOwnerOrAdminRole(role) {
|
|
|
3428
3430
|
* @returns
|
|
3429
3431
|
*/
|
|
3430
3432
|
const getAllAuditLogs = (options) => {
|
|
3431
|
-
const $
|
|
3432
|
-
baseURL: options.apiUrl,
|
|
3433
|
-
headers: { "x-api-key": options.apiKey }
|
|
3434
|
-
});
|
|
3433
|
+
const { $api } = options;
|
|
3435
3434
|
const getUserOrganizationRole = async (adapter, orgId, userId) => {
|
|
3436
3435
|
return (await adapter.findOne({
|
|
3437
3436
|
model: "member",
|
|
@@ -3508,7 +3507,7 @@ const getAllAuditLogs = (options) => {
|
|
|
3508
3507
|
const identifierFilter = query.identifier?.trim();
|
|
3509
3508
|
if (eventTypeFilter) apiQuery.eventType = eventTypeFilter;
|
|
3510
3509
|
if (identifierFilter) apiQuery.identifier = identifierFilter;
|
|
3511
|
-
const { data, error } = await $
|
|
3510
|
+
const { data, error } = await $api("/events/activity", {
|
|
3512
3511
|
method: "GET",
|
|
3513
3512
|
query: apiQuery
|
|
3514
3513
|
});
|
|
@@ -3628,10 +3627,7 @@ const executeAdapter = (options) => {
|
|
|
3628
3627
|
* It creates the user in the auth database and sets up a session.
|
|
3629
3628
|
*/
|
|
3630
3629
|
const acceptInvitation = (options) => {
|
|
3631
|
-
const $api =
|
|
3632
|
-
baseURL: options.apiUrl || INFRA_API_URL,
|
|
3633
|
-
headers: { "x-api-key": options.apiKey }
|
|
3634
|
-
});
|
|
3630
|
+
const { $api } = options;
|
|
3635
3631
|
return createAuthEndpoint("/dash/accept-invitation", {
|
|
3636
3632
|
method: "GET",
|
|
3637
3633
|
query: z$1.object({ token: z$1.string() })
|
|
@@ -3711,10 +3707,7 @@ const acceptInvitation = (options) => {
|
|
|
3711
3707
|
* This creates the user in the auth database and sets up a session
|
|
3712
3708
|
*/
|
|
3713
3709
|
const completeInvitation = (options) => {
|
|
3714
|
-
const $api =
|
|
3715
|
-
baseURL: options.apiUrl || INFRA_API_URL,
|
|
3716
|
-
headers: { "x-api-key": options.apiKey }
|
|
3717
|
-
});
|
|
3710
|
+
const { $api } = options;
|
|
3718
3711
|
return createAuthEndpoint("/dash/complete-invitation", {
|
|
3719
3712
|
method: "POST",
|
|
3720
3713
|
body: z$1.object({
|
|
@@ -3844,7 +3837,7 @@ const listOrganizationInvitations = (options) => {
|
|
|
3844
3837
|
operator: "in"
|
|
3845
3838
|
}]
|
|
3846
3839
|
}) : [];
|
|
3847
|
-
const userByEmail = new Map(users.map((u) => [normalizeEmail(u.email, ctx.context), u]));
|
|
3840
|
+
const userByEmail = new Map(users.filter((u) => u.email != null).map((u) => [normalizeEmail(u.email, ctx.context), u]));
|
|
3848
3841
|
return invitations.map((invitation) => {
|
|
3849
3842
|
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3850
3843
|
const user = userByEmail.get(invitationEmail);
|
|
@@ -3853,7 +3846,7 @@ const listOrganizationInvitations = (options) => {
|
|
|
3853
3846
|
user: user ? {
|
|
3854
3847
|
id: user.id,
|
|
3855
3848
|
name: user.name,
|
|
3856
|
-
email: user.email,
|
|
3849
|
+
email: user.email ?? void 0,
|
|
3857
3850
|
image: user.image || null
|
|
3858
3851
|
} : null
|
|
3859
3852
|
};
|
|
@@ -3939,7 +3932,7 @@ const checkUserByEmail = (options) => {
|
|
|
3939
3932
|
user: {
|
|
3940
3933
|
id: user.id,
|
|
3941
3934
|
name: user.name,
|
|
3942
|
-
email: user.email,
|
|
3935
|
+
email: user.email ?? void 0,
|
|
3943
3936
|
image: user.image ?? null
|
|
3944
3937
|
},
|
|
3945
3938
|
isAlreadyMember: !!existingMember
|
|
@@ -4095,18 +4088,18 @@ const listOrganizationMembers = (options) => {
|
|
|
4095
4088
|
for (const m of members) {
|
|
4096
4089
|
const joinedUser = Array.isArray(m.user) ? m.user[0] : m.user;
|
|
4097
4090
|
if (!joinedUser) continue;
|
|
4098
|
-
const invitation = invitationByEmail.get(joinedUser.email.toLowerCase());
|
|
4091
|
+
const invitation = joinedUser.email ? invitationByEmail.get(joinedUser.email.toLowerCase()) : void 0;
|
|
4099
4092
|
const inviter = invitation ? inviterById.get(invitation.inviterId) : void 0;
|
|
4100
4093
|
const user = {
|
|
4101
4094
|
id: joinedUser.id,
|
|
4102
|
-
email: joinedUser.email,
|
|
4095
|
+
email: joinedUser.email ?? void 0,
|
|
4103
4096
|
name: joinedUser.name,
|
|
4104
4097
|
image: joinedUser.image ?? null
|
|
4105
4098
|
};
|
|
4106
4099
|
const invitedBy = inviter ? {
|
|
4107
4100
|
id: inviter.id,
|
|
4108
4101
|
name: inviter.name,
|
|
4109
|
-
email: inviter.email,
|
|
4102
|
+
email: inviter.email ?? void 0,
|
|
4110
4103
|
image: inviter.image ?? null
|
|
4111
4104
|
} : null;
|
|
4112
4105
|
result.push({
|
|
@@ -4549,8 +4542,8 @@ const listOrganizations = (options) => {
|
|
|
4549
4542
|
const members = organization.member.map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
|
|
4550
4543
|
id: u.id,
|
|
4551
4544
|
name: u.name,
|
|
4552
|
-
email: u.email,
|
|
4553
|
-
image: u.image
|
|
4545
|
+
email: u.email ?? void 0,
|
|
4546
|
+
image: u.image ?? null
|
|
4554
4547
|
}));
|
|
4555
4548
|
const { member: _members, ...org } = organization;
|
|
4556
4549
|
return {
|
|
@@ -5292,7 +5285,7 @@ const listTeamMembers = (options) => {
|
|
|
5292
5285
|
user: user ? {
|
|
5293
5286
|
id: user.id,
|
|
5294
5287
|
name: user.name,
|
|
5295
|
-
email: user.email,
|
|
5288
|
+
email: user.email ?? void 0,
|
|
5296
5289
|
image: user.image ?? null
|
|
5297
5290
|
} : null
|
|
5298
5291
|
};
|
|
@@ -7372,6 +7365,7 @@ const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verifi
|
|
|
7372
7365
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
7373
7366
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
7374
7367
|
if (user.emailVerified) throw ctx.error("BAD_REQUEST", { message: "Email is already verified" });
|
|
7368
|
+
if (!user.email) throw ctx.error("BAD_REQUEST", { message: "User has no associated email address" });
|
|
7375
7369
|
if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
|
|
7376
7370
|
await sendVerificationEmailFn({
|
|
7377
7371
|
...ctx,
|
|
@@ -7408,6 +7402,10 @@ const sendManyVerificationEmails = (options) => {
|
|
|
7408
7402
|
});
|
|
7409
7403
|
if (chunk.length - users.length > 0) for (const id of chunk.filter((id) => !users.some((u) => u.id === id))) skippedEmailUserIds.add(id);
|
|
7410
7404
|
for (const result of await Promise.allSettled(users.map(async (user) => {
|
|
7405
|
+
if (!user.email) return {
|
|
7406
|
+
success: false,
|
|
7407
|
+
id: user.id
|
|
7408
|
+
};
|
|
7411
7409
|
try {
|
|
7412
7410
|
await sendVerificationEmailFn({
|
|
7413
7411
|
...ctx,
|
|
@@ -7447,6 +7445,7 @@ const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset
|
|
|
7447
7445
|
const { userId } = ctx.context.payload;
|
|
7448
7446
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
7449
7447
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
7448
|
+
if (!user.email) throw ctx.error("BAD_REQUEST", { message: "User has no associated email address" });
|
|
7450
7449
|
ctx.body.redirectTo = ctx.body.callbackUrl;
|
|
7451
7450
|
ctx.body.email = user.email;
|
|
7452
7451
|
return await requestPasswordReset(ctx);
|
|
@@ -7541,6 +7540,11 @@ function createSMSSender(config) {
|
|
|
7541
7540
|
const apiUrl = baseUrl.endsWith("/api") ? baseUrl : `${baseUrl}/api`;
|
|
7542
7541
|
const apiKey = config?.apiKey || env.BETTER_AUTH_API_KEY || "";
|
|
7543
7542
|
if (!apiKey) logger.warn("[Dash] No API key provided for SMS sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
|
|
7543
|
+
const $api = createFetch({
|
|
7544
|
+
baseURL: apiUrl,
|
|
7545
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
7546
|
+
timeout: config?.apiTimeout ?? 3e3
|
|
7547
|
+
});
|
|
7544
7548
|
/**
|
|
7545
7549
|
* Send an SMS with OTP code
|
|
7546
7550
|
*/
|
|
@@ -7550,25 +7554,25 @@ function createSMSSender(config) {
|
|
|
7550
7554
|
error: "API key not configured"
|
|
7551
7555
|
};
|
|
7552
7556
|
try {
|
|
7553
|
-
const
|
|
7557
|
+
const { data, error } = await $api("/v1/sms/send", {
|
|
7554
7558
|
method: "POST",
|
|
7555
|
-
|
|
7556
|
-
"Content-Type": "application/json",
|
|
7557
|
-
Authorization: `Bearer ${apiKey}`
|
|
7558
|
-
},
|
|
7559
|
-
body: JSON.stringify({
|
|
7559
|
+
body: {
|
|
7560
7560
|
to: options.to,
|
|
7561
7561
|
code: options.code,
|
|
7562
7562
|
template: options.template
|
|
7563
|
-
}
|
|
7563
|
+
}
|
|
7564
7564
|
});
|
|
7565
|
-
if (
|
|
7565
|
+
if (error) return {
|
|
7566
7566
|
success: false,
|
|
7567
|
-
error:
|
|
7567
|
+
error: error.message || `HTTP ${error.status}`
|
|
7568
|
+
};
|
|
7569
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return {
|
|
7570
|
+
success: false,
|
|
7571
|
+
error: "Failed to parse JSON"
|
|
7568
7572
|
};
|
|
7569
7573
|
return {
|
|
7570
7574
|
success: true,
|
|
7571
|
-
messageId:
|
|
7575
|
+
messageId: data.messageId
|
|
7572
7576
|
};
|
|
7573
7577
|
} catch (error) {
|
|
7574
7578
|
logger.warn("[Dash] SMS send failed:", error);
|
|
@@ -7616,8 +7620,14 @@ async function sendSMS(options, config) {
|
|
|
7616
7620
|
const dash = (options) => {
|
|
7617
7621
|
const processedBulkOperationContexts = /* @__PURE__ */ new WeakSet();
|
|
7618
7622
|
const opts = resolveDashOptions(options);
|
|
7623
|
+
const $api = createAPI(opts);
|
|
7624
|
+
const settings = {
|
|
7625
|
+
...opts,
|
|
7626
|
+
$api
|
|
7627
|
+
};
|
|
7628
|
+
const $kv = createKV(opts);
|
|
7619
7629
|
const activityUpdateInterval = opts.activityTracking?.updateInterval ?? 3e5;
|
|
7620
|
-
const { tracker } = initTrackEvents(
|
|
7630
|
+
const { tracker } = initTrackEvents($api);
|
|
7621
7631
|
const { trackUserSignedUp, trackUserProfileUpdated, trackUserProfileImageUpdated, trackUserEmailVerified, trackUserBanned, trackUserUnBanned, trackUserDeleted } = initUserEvents(tracker);
|
|
7622
7632
|
const { trackEmailVerificationSent, trackEmailSignInAttempt, trackUserSignedIn, trackUserSignedOut, trackSessionCreated, trackSocialSignInAttempt, trackSocialSignInRedirectionAttempt, trackUserImpersonated, trackUserImpersonationStop, trackSessionRevoked, trackSessionRevokedAll } = initSessionEvents(tracker);
|
|
7623
7633
|
const { trackAccountLinking, trackAccountUnlink, trackAccountPasswordChange } = initAccountEvents(tracker);
|
|
@@ -7899,7 +7909,7 @@ const dash = (options) => {
|
|
|
7899
7909
|
const path = new URL(ctx.request.url).pathname;
|
|
7900
7910
|
return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
|
|
7901
7911
|
},
|
|
7902
|
-
handler: createIdentificationMiddleware(
|
|
7912
|
+
handler: createIdentificationMiddleware($kv)
|
|
7903
7913
|
}],
|
|
7904
7914
|
after: [{
|
|
7905
7915
|
matcher: (ctx) => {
|
|
@@ -7914,7 +7924,7 @@ const dash = (options) => {
|
|
|
7914
7924
|
const body = ctx.body;
|
|
7915
7925
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
|
|
7916
7926
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
|
|
7917
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.context.returned instanceof Error) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
7927
|
+
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
7918
7928
|
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
7919
7929
|
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
7920
7930
|
maxAge: 600,
|
|
@@ -7952,79 +7962,79 @@ const dash = (options) => {
|
|
|
7952
7962
|
}]
|
|
7953
7963
|
},
|
|
7954
7964
|
endpoints: {
|
|
7955
|
-
getDashConfig: getConfig(
|
|
7956
|
-
getDashValidate: getValidate(
|
|
7957
|
-
getDashUsers: getUsers(
|
|
7958
|
-
exportDashUsers: exportUsers(
|
|
7959
|
-
createDashUser: createUser(
|
|
7960
|
-
deleteDashUser: deleteUser(
|
|
7961
|
-
deleteManyDashUsers: deleteManyUsers(
|
|
7962
|
-
listDashOrganizations: listOrganizations(
|
|
7963
|
-
exportDashOrganizations: exportOrganizations(
|
|
7964
|
-
getDashOrganization: getOrganization(
|
|
7965
|
-
listDashOrganizationMembers: listOrganizationMembers(
|
|
7966
|
-
listDashOrganizationInvitations: listOrganizationInvitations(
|
|
7967
|
-
listDashOrganizationTeams: listOrganizationTeams(
|
|
7968
|
-
listDashOrganizationSsoProviders: listOrganizationSsoProviders(
|
|
7969
|
-
createDashSsoProvider: createSsoProvider(
|
|
7970
|
-
updateDashSsoProvider: updateSsoProvider(
|
|
7971
|
-
requestDashSsoVerificationToken: requestSsoVerificationToken(
|
|
7972
|
-
verifyDashSsoProviderDomain: verifySsoProviderDomain(
|
|
7973
|
-
deleteDashSsoProvider: deleteSsoProvider(
|
|
7974
|
-
markDashSsoProviderDomainVerified: markSsoProviderDomainVerified(
|
|
7975
|
-
listDashTeamMembers: listTeamMembers(
|
|
7976
|
-
createDashOrganization: createOrganization(
|
|
7977
|
-
deleteDashOrganization: deleteOrganization(
|
|
7978
|
-
deleteManyDashOrganizations: deleteManyOrganizations(
|
|
7979
|
-
getDashOrganizationOptions: getOrganizationOptions(
|
|
7980
|
-
getDashUser: getUserDetails(
|
|
7981
|
-
getDashUserOrganizations: getUserOrganizations(
|
|
7982
|
-
updateDashUser: updateUser(
|
|
7983
|
-
setDashPassword: setPassword(
|
|
7984
|
-
unlinkDashAccount: unlinkAccount(
|
|
7985
|
-
dashRevokeSession: revokeSession(
|
|
7986
|
-
dashRevokeAllSessions: revokeAllSessions(
|
|
7987
|
-
dashRevokeManySessions: revokeManySessions(
|
|
7988
|
-
dashImpersonateUser: impersonateUser(
|
|
7989
|
-
updateDashOrganization: updateOrganization(
|
|
7990
|
-
createDashTeam: createTeam(
|
|
7991
|
-
updateDashTeam: updateTeam(
|
|
7992
|
-
deleteDashTeam: deleteTeam(
|
|
7993
|
-
addDashTeamMember: addTeamMember(
|
|
7994
|
-
removeDashTeamMember: removeTeamMember(
|
|
7995
|
-
addDashMember: addMember(
|
|
7996
|
-
removeDashMember: removeMember(
|
|
7997
|
-
updateDashMemberRole: updateMemberRole(
|
|
7998
|
-
inviteDashMember: inviteMember(
|
|
7999
|
-
cancelDashInvitation: cancelInvitation(
|
|
8000
|
-
resendDashInvitation: resendInvitation(
|
|
8001
|
-
dashCheckUserByEmail: checkUserByEmail(
|
|
8002
|
-
dashGetUserStats: getUserStats(
|
|
8003
|
-
dashGetUserGraphData: getUserGraphData(
|
|
8004
|
-
dashGetUserRetentionData: getUserRetentionData(
|
|
8005
|
-
dashBanUser: banUser(
|
|
8006
|
-
dashBanManyUsers: banManyUsers(
|
|
8007
|
-
dashUnbanUser: unbanUser(
|
|
8008
|
-
dashSendVerificationEmail: sendVerificationEmail(
|
|
8009
|
-
dashSendManyVerificationEmails: sendManyVerificationEmails(
|
|
8010
|
-
dashSendResetPasswordEmail: sendResetPasswordEmail(
|
|
8011
|
-
dashEnableTwoFactor: enableTwoFactor(
|
|
8012
|
-
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(
|
|
8013
|
-
dashViewBackupCodes: viewBackupCodes(
|
|
8014
|
-
dashDisableTwoFactor: disableTwoFactor(
|
|
8015
|
-
dashGenerateBackupCodes: generateBackupCodes(
|
|
8016
|
-
getUserEvents: getUserEvents(
|
|
8017
|
-
getAuditLogs: getAuditLogs(
|
|
8018
|
-
getAllAuditLogs: getAllAuditLogs(
|
|
8019
|
-
getEventTypes: getEventTypes(
|
|
8020
|
-
dashAcceptInvitation: acceptInvitation(
|
|
8021
|
-
dashCompleteInvitation: completeInvitation(
|
|
8022
|
-
dashCheckUserExists: checkUserExists(
|
|
8023
|
-
listDashOrganizationDirectories: listOrganizationDirectories(
|
|
8024
|
-
createDashOrganizationDirectory: createOrganizationDirectory(
|
|
8025
|
-
deleteDashOrganizationDirectory: deleteOrganizationDirectory(
|
|
8026
|
-
regenerateDashDirectoryToken: regenerateDirectoryToken(
|
|
8027
|
-
dashExecuteAdapter: executeAdapter(
|
|
7965
|
+
getDashConfig: getConfig(settings),
|
|
7966
|
+
getDashValidate: getValidate(settings),
|
|
7967
|
+
getDashUsers: getUsers(settings),
|
|
7968
|
+
exportDashUsers: exportUsers(settings),
|
|
7969
|
+
createDashUser: createUser(settings),
|
|
7970
|
+
deleteDashUser: deleteUser(settings),
|
|
7971
|
+
deleteManyDashUsers: deleteManyUsers(settings),
|
|
7972
|
+
listDashOrganizations: listOrganizations(settings),
|
|
7973
|
+
exportDashOrganizations: exportOrganizations(settings),
|
|
7974
|
+
getDashOrganization: getOrganization(settings),
|
|
7975
|
+
listDashOrganizationMembers: listOrganizationMembers(settings),
|
|
7976
|
+
listDashOrganizationInvitations: listOrganizationInvitations(settings),
|
|
7977
|
+
listDashOrganizationTeams: listOrganizationTeams(settings),
|
|
7978
|
+
listDashOrganizationSsoProviders: listOrganizationSsoProviders(settings),
|
|
7979
|
+
createDashSsoProvider: createSsoProvider(settings),
|
|
7980
|
+
updateDashSsoProvider: updateSsoProvider(settings),
|
|
7981
|
+
requestDashSsoVerificationToken: requestSsoVerificationToken(settings),
|
|
7982
|
+
verifyDashSsoProviderDomain: verifySsoProviderDomain(settings),
|
|
7983
|
+
deleteDashSsoProvider: deleteSsoProvider(settings),
|
|
7984
|
+
markDashSsoProviderDomainVerified: markSsoProviderDomainVerified(settings),
|
|
7985
|
+
listDashTeamMembers: listTeamMembers(settings),
|
|
7986
|
+
createDashOrganization: createOrganization(settings),
|
|
7987
|
+
deleteDashOrganization: deleteOrganization(settings),
|
|
7988
|
+
deleteManyDashOrganizations: deleteManyOrganizations(settings),
|
|
7989
|
+
getDashOrganizationOptions: getOrganizationOptions(settings),
|
|
7990
|
+
getDashUser: getUserDetails(settings),
|
|
7991
|
+
getDashUserOrganizations: getUserOrganizations(settings),
|
|
7992
|
+
updateDashUser: updateUser(settings),
|
|
7993
|
+
setDashPassword: setPassword(settings),
|
|
7994
|
+
unlinkDashAccount: unlinkAccount(settings),
|
|
7995
|
+
dashRevokeSession: revokeSession(settings),
|
|
7996
|
+
dashRevokeAllSessions: revokeAllSessions(settings),
|
|
7997
|
+
dashRevokeManySessions: revokeManySessions(settings),
|
|
7998
|
+
dashImpersonateUser: impersonateUser(settings),
|
|
7999
|
+
updateDashOrganization: updateOrganization(settings),
|
|
8000
|
+
createDashTeam: createTeam(settings),
|
|
8001
|
+
updateDashTeam: updateTeam(settings),
|
|
8002
|
+
deleteDashTeam: deleteTeam(settings),
|
|
8003
|
+
addDashTeamMember: addTeamMember(settings),
|
|
8004
|
+
removeDashTeamMember: removeTeamMember(settings),
|
|
8005
|
+
addDashMember: addMember(settings),
|
|
8006
|
+
removeDashMember: removeMember(settings),
|
|
8007
|
+
updateDashMemberRole: updateMemberRole(settings),
|
|
8008
|
+
inviteDashMember: inviteMember(settings),
|
|
8009
|
+
cancelDashInvitation: cancelInvitation(settings),
|
|
8010
|
+
resendDashInvitation: resendInvitation(settings),
|
|
8011
|
+
dashCheckUserByEmail: checkUserByEmail(settings),
|
|
8012
|
+
dashGetUserStats: getUserStats(settings),
|
|
8013
|
+
dashGetUserGraphData: getUserGraphData(settings),
|
|
8014
|
+
dashGetUserRetentionData: getUserRetentionData(settings),
|
|
8015
|
+
dashBanUser: banUser(settings),
|
|
8016
|
+
dashBanManyUsers: banManyUsers(settings),
|
|
8017
|
+
dashUnbanUser: unbanUser(settings),
|
|
8018
|
+
dashSendVerificationEmail: sendVerificationEmail(settings),
|
|
8019
|
+
dashSendManyVerificationEmails: sendManyVerificationEmails(settings),
|
|
8020
|
+
dashSendResetPasswordEmail: sendResetPasswordEmail(settings),
|
|
8021
|
+
dashEnableTwoFactor: enableTwoFactor(settings),
|
|
8022
|
+
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(settings),
|
|
8023
|
+
dashViewBackupCodes: viewBackupCodes(settings),
|
|
8024
|
+
dashDisableTwoFactor: disableTwoFactor(settings),
|
|
8025
|
+
dashGenerateBackupCodes: generateBackupCodes(settings),
|
|
8026
|
+
getUserEvents: getUserEvents(settings),
|
|
8027
|
+
getAuditLogs: getAuditLogs(settings),
|
|
8028
|
+
getAllAuditLogs: getAllAuditLogs(settings),
|
|
8029
|
+
getEventTypes: getEventTypes(settings),
|
|
8030
|
+
dashAcceptInvitation: acceptInvitation(settings),
|
|
8031
|
+
dashCompleteInvitation: completeInvitation(settings),
|
|
8032
|
+
dashCheckUserExists: checkUserExists(settings),
|
|
8033
|
+
listDashOrganizationDirectories: listOrganizationDirectories(settings),
|
|
8034
|
+
createDashOrganizationDirectory: createOrganizationDirectory(settings),
|
|
8035
|
+
deleteDashOrganizationDirectory: deleteOrganizationDirectory(settings),
|
|
8036
|
+
regenerateDashDirectoryToken: regenerateDirectoryToken(settings),
|
|
8037
|
+
dashExecuteAdapter: executeAdapter(settings)
|
|
8028
8038
|
},
|
|
8029
8039
|
schema: opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: {
|
|
8030
8040
|
type: "date",
|