@better-auth/infra 0.2.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/client.mjs +65 -49
- package/dist/{constants-CvriWQVc.mjs → constants-vBiGTmGf.mjs} +7 -1
- package/dist/crypto-DjtrPUlP.mjs +51 -0
- package/dist/email.mjs +5 -2
- package/dist/index.d.mts +1900 -103
- package/dist/index.mjs +955 -718
- package/dist/native.mjs +62 -43
- package/dist/{pow-retry-D2RRftJr.mjs → pow-retry-24lSgXId.mjs} +3 -17
- package/package.json +10 -9
- package/dist/fetch-DiAhoiKA.mjs +0 -26
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as INFRA_API_URL, r as INFRA_KV_URL } from "./constants-
|
|
2
|
-
import {
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-vBiGTmGf.mjs";
|
|
2
|
+
import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-DjtrPUlP.mjs";
|
|
3
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
4
|
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
5
|
-
import { APIError, generateId, getAuthTables, logger
|
|
5
|
+
import { APIError, generateId, getAuthTables, logger } from "better-auth";
|
|
6
6
|
import { env } from "@better-auth/core/env";
|
|
7
7
|
import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
|
|
8
8
|
import { createFetch } from "@better-fetch/fetch";
|
|
@@ -10,6 +10,8 @@ import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-j
|
|
|
10
10
|
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
11
11
|
import z$1, { z } from "zod";
|
|
12
12
|
import { setSessionCookie } from "better-auth/cookies";
|
|
13
|
+
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
|
|
14
|
+
import { createOTP } from "@better-auth/utils/otp";
|
|
13
15
|
//#region src/options.ts
|
|
14
16
|
function resolveConnectionOptions(options) {
|
|
15
17
|
return {
|
|
@@ -66,6 +68,7 @@ const EVENT_TYPES = {
|
|
|
66
68
|
USER_IMPERSONATED: "user_impersonated",
|
|
67
69
|
USER_IMPERSONATED_STOPPED: "user_impersonated_stopped"
|
|
68
70
|
};
|
|
71
|
+
const UNKNOWN_USER = "unknown";
|
|
69
72
|
const routes = {
|
|
70
73
|
SEND_VERIFICATION_EMAIL: "/send-verification-email",
|
|
71
74
|
SIGN_IN: "/sign-in",
|
|
@@ -75,7 +78,7 @@ const routes = {
|
|
|
75
78
|
SIGN_IN_SOCIAL: "/sign-in/social",
|
|
76
79
|
SIGN_IN_ANONYMOUS: "/sign-in/anonymous",
|
|
77
80
|
SIGN_IN_SOCIAL_CALLBACK: "/callback/:id",
|
|
78
|
-
SIGN_IN_OAUTH_CALLBACK: "/oauth2/callback/:
|
|
81
|
+
SIGN_IN_OAUTH_CALLBACK: "/oauth2/callback/:providerId",
|
|
79
82
|
SIGN_OUT: "/sign-out",
|
|
80
83
|
SIGN_UP: "/sign-up",
|
|
81
84
|
SIGN_UP_EMAIL: "/sign-up/email",
|
|
@@ -106,7 +109,11 @@ const routes = {
|
|
|
106
109
|
API_KEY_CREATE: "/api-key/create",
|
|
107
110
|
LINK_SOCIAL: "/link-social",
|
|
108
111
|
DASH_ROUTE: "/dash",
|
|
112
|
+
DASH_ACCEPT_INVITATION: "/dash/accept-invitation",
|
|
113
|
+
DASH_COMPLETE_INVITATION_SOCIAL: "/dash/complete-invitation-social",
|
|
109
114
|
DASH_UPDATE_USER: "/dash/update-user",
|
|
115
|
+
DASH_SET_PASSWORD: "/dash/set-password",
|
|
116
|
+
DASH_SEND_VERIFICATION_EMAIL: "/dash/send-verification-email",
|
|
110
117
|
DASH_REVOKE_SESSIONS_ALL: "/dash/sessions/revoke-all",
|
|
111
118
|
DASH_IMPERSONATE_USER: "/dash/impersonate-user",
|
|
112
119
|
DASH_BAN_USER: "/dash/ban-user",
|
|
@@ -181,34 +188,6 @@ async function getUserById(userId, ctx) {
|
|
|
181
188
|
}
|
|
182
189
|
return user;
|
|
183
190
|
}
|
|
184
|
-
const getUserByIdToken = async (providerId, idToken, ctx) => {
|
|
185
|
-
const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
|
|
186
|
-
let user = null;
|
|
187
|
-
if (provider) try {
|
|
188
|
-
user = await provider.getUserInfo(idToken);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
191
|
-
}
|
|
192
|
-
return user;
|
|
193
|
-
};
|
|
194
|
-
const getUserByAuthorizationCode = async (providerId, ctx) => {
|
|
195
|
-
let userInfo = null;
|
|
196
|
-
const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
|
|
197
|
-
if (provider) try {
|
|
198
|
-
const codeVerifier = (await parseState(ctx)).codeVerifier;
|
|
199
|
-
const { code, device_id } = ctx.query ?? {};
|
|
200
|
-
const tokens = await provider.validateAuthorizationCode({
|
|
201
|
-
code,
|
|
202
|
-
codeVerifier,
|
|
203
|
-
deviceId: device_id,
|
|
204
|
-
redirectURI: `${ctx.context.baseURL}/callback/${provider.id}`
|
|
205
|
-
});
|
|
206
|
-
userInfo = await provider.getUserInfo({ ...tokens }).then((res) => res?.user);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
209
|
-
}
|
|
210
|
-
return userInfo;
|
|
211
|
-
};
|
|
212
191
|
//#endregion
|
|
213
192
|
//#region src/events/core/events-account.ts
|
|
214
193
|
const initAccountEvents = (tracker) => {
|
|
@@ -307,13 +286,22 @@ const stripQuery = (value) => value.split("?")[0] || value;
|
|
|
307
286
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
308
287
|
const routeToRegex = (route) => {
|
|
309
288
|
const pattern = escapeRegex(stripQuery(route)).replace(/\/:([^/]+)/g, "/[^/]+");
|
|
310
|
-
return new RegExp(
|
|
289
|
+
return new RegExp(`^${pattern}(?:$|[/?])`);
|
|
311
290
|
};
|
|
312
291
|
const matchesAnyRoute = (path, routes) => {
|
|
292
|
+
if (!path) return false;
|
|
313
293
|
const cleanPath = stripQuery(path);
|
|
314
294
|
return routes.some((route) => routeToRegex(route).test(cleanPath));
|
|
315
295
|
};
|
|
316
296
|
//#endregion
|
|
297
|
+
//#region src/events/oauth-callback-provider.ts
|
|
298
|
+
/** Provider id from social (`/callback/:id`) or generic OAuth (`/oauth2/callback/:providerId`). */
|
|
299
|
+
function getOAuthCallbackProviderId(ctx) {
|
|
300
|
+
const id = ctx.params?.id ?? ctx.params?.providerId;
|
|
301
|
+
if (typeof id !== "string" || !id || id.startsWith(":")) return;
|
|
302
|
+
return id;
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
317
305
|
//#region src/events/login-methods.ts
|
|
318
306
|
const OAUTH_CALLBACK_PATHS = [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK];
|
|
319
307
|
const PATH_TO_METHOD = [
|
|
@@ -336,15 +324,58 @@ const PATH_TO_METHOD = [
|
|
|
336
324
|
[routes.SIWE_VERIFY, "siwe"]
|
|
337
325
|
];
|
|
338
326
|
const getLoginMethod = (ctx) => {
|
|
339
|
-
if (matchesAnyRoute(ctx.path, OAUTH_CALLBACK_PATHS))
|
|
340
|
-
const id = ctx.params?.id;
|
|
341
|
-
if (typeof id === "string" && id && !id.startsWith(":")) return id;
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
327
|
+
if (matchesAnyRoute(ctx.path, OAUTH_CALLBACK_PATHS)) return getOAuthCallbackProviderId(ctx) ?? null;
|
|
344
328
|
for (const [path, method] of PATH_TO_METHOD) if (matchesAnyRoute(ctx.path, [path])) return method;
|
|
345
329
|
return null;
|
|
346
330
|
};
|
|
347
331
|
//#endregion
|
|
332
|
+
//#region src/events/core/oauth-callback-user.ts
|
|
333
|
+
const OAUTH_CALLBACK_USER = Symbol.for("dash.oauthCallbackUser");
|
|
334
|
+
/**
|
|
335
|
+
* Stash OAuth profile on the request context when better-auth calls getUserInfo
|
|
336
|
+
* during the callback, before the authorization code is consumed.
|
|
337
|
+
*/
|
|
338
|
+
function instrumentSocialProviders(providers) {
|
|
339
|
+
for (const provider of providers) {
|
|
340
|
+
const originalGetUserInfo = provider.getUserInfo.bind(provider);
|
|
341
|
+
provider.getUserInfo = async (token) => {
|
|
342
|
+
const result = await originalGetUserInfo(token);
|
|
343
|
+
const user = result?.user;
|
|
344
|
+
if (user) try {
|
|
345
|
+
const endpointCtx = await getCurrentAuthContext();
|
|
346
|
+
endpointCtx.context[OAUTH_CALLBACK_USER] = user;
|
|
347
|
+
} catch {}
|
|
348
|
+
return result;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function resolveOAuthUser(providerId, ctx) {
|
|
353
|
+
const oauthUser = ctx.context[OAUTH_CALLBACK_USER];
|
|
354
|
+
if (!oauthUser) return null;
|
|
355
|
+
const email = oauthUser.email?.toLowerCase();
|
|
356
|
+
const accountId = oauthUser.id !== void 0 && oauthUser.id !== null ? String(oauthUser.id) : void 0;
|
|
357
|
+
if (email && accountId) try {
|
|
358
|
+
const result = await ctx.context.internalAdapter.findOAuthUser(email, accountId, providerId);
|
|
359
|
+
if (result?.user) return {
|
|
360
|
+
id: result.user.id,
|
|
361
|
+
email: result.user.email,
|
|
362
|
+
name: result.user.name
|
|
363
|
+
};
|
|
364
|
+
} catch (error) {
|
|
365
|
+
logger.debug("[Dash] Failed to find OAuth user:", error);
|
|
366
|
+
}
|
|
367
|
+
if (email) {
|
|
368
|
+
const user = await getUserByEmail(email, ctx);
|
|
369
|
+
if (user) return user;
|
|
370
|
+
}
|
|
371
|
+
if (!email && !oauthUser.name) return null;
|
|
372
|
+
return {
|
|
373
|
+
id: UNKNOWN_USER,
|
|
374
|
+
email: oauthUser.email ?? null,
|
|
375
|
+
name: oauthUser.name ?? "unknown"
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
348
379
|
//#region src/events/core/events-session.ts
|
|
349
380
|
const initSessionEvents = (tracker) => {
|
|
350
381
|
const { trackEvent } = tracker;
|
|
@@ -560,7 +591,7 @@ const initSessionEvents = (tracker) => {
|
|
|
560
591
|
eventDisplayName: "User sign-in attempt failed",
|
|
561
592
|
eventData: {
|
|
562
593
|
userId: user?.id ?? "unknown",
|
|
563
|
-
|
|
594
|
+
userName: user?.name ?? "unknown",
|
|
564
595
|
userEmail: ctx.body.email,
|
|
565
596
|
loginMethod: getLoginMethod(ctx),
|
|
566
597
|
triggeredBy: user?.id ?? trigger.triggeredBy,
|
|
@@ -574,19 +605,19 @@ const initSessionEvents = (tracker) => {
|
|
|
574
605
|
};
|
|
575
606
|
ctx.context.runInBackground(track());
|
|
576
607
|
};
|
|
577
|
-
const
|
|
608
|
+
const trackSocialSignInFailure = (ctx, providerId, trigger, location) => {
|
|
578
609
|
const track = async () => {
|
|
579
|
-
const user = await
|
|
610
|
+
const user = await resolveOAuthUser(providerId, ctx);
|
|
580
611
|
trackEvent({
|
|
581
|
-
eventKey: user?.
|
|
612
|
+
eventKey: user?.id ?? "unknown",
|
|
582
613
|
eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
|
|
583
614
|
eventDisplayName: "User sign-in attempt failed",
|
|
584
615
|
eventData: {
|
|
585
|
-
userId: user?.
|
|
586
|
-
userName: user?.
|
|
587
|
-
userEmail: user?.
|
|
616
|
+
userId: user?.id ?? "unknown",
|
|
617
|
+
userName: user?.name ?? "unknown",
|
|
618
|
+
userEmail: user?.email ?? "unknown",
|
|
588
619
|
loginMethod: getLoginMethod(ctx),
|
|
589
|
-
triggeredBy: user
|
|
620
|
+
triggeredBy: user && user.id !== "unknown" ? user.id : trigger.triggeredBy,
|
|
590
621
|
triggerContext: trigger.triggerContext
|
|
591
622
|
},
|
|
592
623
|
ipAddress: location?.ipAddress,
|
|
@@ -597,28 +628,13 @@ const initSessionEvents = (tracker) => {
|
|
|
597
628
|
};
|
|
598
629
|
ctx.context.runInBackground(track());
|
|
599
630
|
};
|
|
631
|
+
const trackSocialSignInAttempt = (ctx, trigger, location) => {
|
|
632
|
+
trackSocialSignInFailure(ctx, ctx.body.provider, trigger, location);
|
|
633
|
+
};
|
|
600
634
|
const trackSocialSignInRedirectionAttempt = (ctx, trigger, location) => {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
eventKey: user?.id.toString() ?? "unknown",
|
|
605
|
-
eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
|
|
606
|
-
eventDisplayName: "User sign-in attempt failed",
|
|
607
|
-
eventData: {
|
|
608
|
-
userId: user?.id.toString() ?? "unknown",
|
|
609
|
-
userName: user?.name ?? "unknown",
|
|
610
|
-
userEmail: user?.id ?? "unknown",
|
|
611
|
-
loginMethod: getLoginMethod(ctx),
|
|
612
|
-
triggeredBy: trigger.triggeredBy,
|
|
613
|
-
triggerContext: trigger.triggerContext
|
|
614
|
-
},
|
|
615
|
-
ipAddress: location?.ipAddress,
|
|
616
|
-
city: location?.city,
|
|
617
|
-
country: location?.country,
|
|
618
|
-
countryCode: location?.countryCode
|
|
619
|
-
});
|
|
620
|
-
};
|
|
621
|
-
ctx.context.runInBackground(track());
|
|
635
|
+
const providerId = getOAuthCallbackProviderId(ctx);
|
|
636
|
+
if (!providerId) return;
|
|
637
|
+
trackSocialSignInFailure(ctx, tryDecode(providerId), trigger, location);
|
|
622
638
|
};
|
|
623
639
|
return {
|
|
624
640
|
trackUserSignedIn,
|
|
@@ -888,6 +904,27 @@ const initTrackEvents = ($api) => {
|
|
|
888
904
|
return { tracker: { trackEvent } };
|
|
889
905
|
};
|
|
890
906
|
//#endregion
|
|
907
|
+
//#region ../utils/dist/ip.mjs
|
|
908
|
+
/** Platform-set client IP headers (not accepted from clients on the open internet). */
|
|
909
|
+
const DEFAULT_TRUSTED_IP_HEADERS = [
|
|
910
|
+
"cf-connecting-ip",
|
|
911
|
+
"true-client-ip",
|
|
912
|
+
"x-vercel-forwarded-for"
|
|
913
|
+
];
|
|
914
|
+
/**
|
|
915
|
+
* Resolves client IP from request headers.
|
|
916
|
+
* Defaults to platform-trusted headers only; set `ipAddressHeaders` for self-hosted proxies.
|
|
917
|
+
*/
|
|
918
|
+
function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
|
|
919
|
+
const headerNames = ipAddressHeaders?.length ? ipAddressHeaders : [...DEFAULT_TRUSTED_IP_HEADERS];
|
|
920
|
+
for (const headerName of headerNames) {
|
|
921
|
+
const value = headers.get(headerName);
|
|
922
|
+
if (!value) continue;
|
|
923
|
+
const ip = value.split(",")[0]?.trim();
|
|
924
|
+
if (ip) return ip;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
//#endregion
|
|
891
928
|
//#region src/identification.ts
|
|
892
929
|
/**
|
|
893
930
|
* Identification Service
|
|
@@ -908,6 +945,20 @@ function cleanupCache() {
|
|
|
908
945
|
function maybeCleanup() {
|
|
909
946
|
if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
|
|
910
947
|
}
|
|
948
|
+
/** Retries 404 races with exponential backoff (≤1s total wait across retries). */
|
|
949
|
+
const IDENTIFY_GET_RETRY = {
|
|
950
|
+
type: "exponential",
|
|
951
|
+
attempts: 2,
|
|
952
|
+
baseDelay: 400,
|
|
953
|
+
maxDelay: 600,
|
|
954
|
+
shouldRetry(response) {
|
|
955
|
+
if (response === null) return true;
|
|
956
|
+
return response.status === 404;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
function identifyGetRetryDelay(attempt) {
|
|
960
|
+
return Math.min(600, 400 * 2 ** attempt);
|
|
961
|
+
}
|
|
911
962
|
/**
|
|
912
963
|
* Fetch identification data from durable-kv by requestId
|
|
913
964
|
*/
|
|
@@ -915,14 +966,11 @@ async function getIdentification(requestId, $kv) {
|
|
|
915
966
|
maybeCleanup();
|
|
916
967
|
const cached = identificationCache.get(requestId);
|
|
917
968
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
];
|
|
924
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
925
|
-
const { data, error } = await $kv(`/identify/${requestId}`, { method: "GET" });
|
|
969
|
+
for (let networkAttempt = 0;; networkAttempt++) try {
|
|
970
|
+
const { data, error } = await $kv(`/identify/${requestId}`, {
|
|
971
|
+
method: "GET",
|
|
972
|
+
retry: IDENTIFY_GET_RETRY
|
|
973
|
+
});
|
|
926
974
|
if (data && !error) {
|
|
927
975
|
identificationCache.set(requestId, {
|
|
928
976
|
data,
|
|
@@ -930,24 +978,18 @@ async function getIdentification(requestId, $kv) {
|
|
|
930
978
|
});
|
|
931
979
|
return data;
|
|
932
980
|
}
|
|
933
|
-
|
|
934
|
-
if (status === 404 && attempt < maxRetries) {
|
|
935
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
|
|
936
|
-
continue;
|
|
937
|
-
}
|
|
938
|
-
if (status !== 404) identificationCache.set(requestId, {
|
|
981
|
+
if (error?.status !== 404) identificationCache.set(requestId, {
|
|
939
982
|
data: null,
|
|
940
983
|
timestamp: Date.now()
|
|
941
984
|
});
|
|
942
985
|
return null;
|
|
943
986
|
} catch (error) {
|
|
944
|
-
if (
|
|
987
|
+
if (networkAttempt >= IDENTIFY_GET_RETRY.attempts) {
|
|
945
988
|
logger.error("[Dash] Failed to fetch identification:", error);
|
|
946
989
|
return null;
|
|
947
990
|
}
|
|
948
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
991
|
+
await new Promise((resolve) => setTimeout(resolve, identifyGetRetryDelay(networkAttempt)));
|
|
949
992
|
}
|
|
950
|
-
return null;
|
|
951
993
|
}
|
|
952
994
|
/**
|
|
953
995
|
* Extract identification headers from a request
|
|
@@ -1004,23 +1046,22 @@ function createIdentificationMiddleware($kv) {
|
|
|
1004
1046
|
ctx.context.visitorId = visitorId;
|
|
1005
1047
|
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
1006
1048
|
let location;
|
|
1049
|
+
const requestIp = ctx.request ? resolveClientIpFromHeaders(ctx.request.headers, ipConfig?.ipAddressHeaders || null) : void 0;
|
|
1050
|
+
const requestCountryCode = getCountryCodeFromRequest(ctx.request);
|
|
1007
1051
|
if (ipConfig?.disableIpTracking === true) location = void 0;
|
|
1008
1052
|
else if (requestId && identification) {
|
|
1009
1053
|
const loc = getLocation(identification);
|
|
1010
1054
|
location = {
|
|
1011
|
-
ipAddress: identification.ip || void 0,
|
|
1055
|
+
ipAddress: identification.ip || requestIp || void 0,
|
|
1012
1056
|
city: loc?.city || void 0,
|
|
1013
1057
|
country: loc?.country?.name || void 0,
|
|
1014
|
-
countryCode: loc?.country?.code || void 0
|
|
1015
|
-
};
|
|
1016
|
-
} else {
|
|
1017
|
-
const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
|
|
1018
|
-
if (ipAddress) location = {
|
|
1019
|
-
ipAddress,
|
|
1020
|
-
countryCode: getCountryCodeFromRequest(ctx.request)
|
|
1058
|
+
countryCode: loc?.country?.code || requestCountryCode || void 0
|
|
1021
1059
|
};
|
|
1022
|
-
|
|
1023
|
-
|
|
1060
|
+
} else if (requestIp) location = {
|
|
1061
|
+
ipAddress: requestIp,
|
|
1062
|
+
countryCode: requestCountryCode
|
|
1063
|
+
};
|
|
1064
|
+
else location = void 0;
|
|
1024
1065
|
ctx.context.location = location;
|
|
1025
1066
|
ctx.context.ip = resolveSecurityIp(identification, location);
|
|
1026
1067
|
ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
|
|
@@ -1033,21 +1074,6 @@ function getLocation(identification) {
|
|
|
1033
1074
|
if (!identification) return null;
|
|
1034
1075
|
return identification.location;
|
|
1035
1076
|
}
|
|
1036
|
-
function getClientIpFromRequest(request, ipAddressHeaders) {
|
|
1037
|
-
if (!request) return void 0;
|
|
1038
|
-
const headers = ipAddressHeaders?.length ? ipAddressHeaders : [
|
|
1039
|
-
"cf-connecting-ip",
|
|
1040
|
-
"x-forwarded-for",
|
|
1041
|
-
"x-real-ip",
|
|
1042
|
-
"x-vercel-forwarded-for"
|
|
1043
|
-
];
|
|
1044
|
-
for (const headerName of headers) {
|
|
1045
|
-
const value = request.headers.get(headerName);
|
|
1046
|
-
if (!value) continue;
|
|
1047
|
-
const ip = value.split(",")[0]?.trim();
|
|
1048
|
-
if (ip) return ip;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
1077
|
function getCountryCodeFromRequest(request) {
|
|
1052
1078
|
if (!request) return void 0;
|
|
1053
1079
|
const cc = request.headers.get("cf-ipcountry") ?? request.headers.get("x-vercel-ip-country");
|
|
@@ -1092,9 +1118,12 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
|
|
|
1092
1118
|
ev.emitted = true;
|
|
1093
1119
|
const identification = ctx.context.identification ?? null;
|
|
1094
1120
|
const visitorId = ctx.context.visitorId ?? null;
|
|
1121
|
+
const untrustedVisitorId = ctx.context.untrustedVisitorId ?? null;
|
|
1122
|
+
const location = ctx.context.location;
|
|
1095
1123
|
const path = ctx.path ?? "";
|
|
1124
|
+
const ipAddress = location?.ipAddress || identification?.ip || ctx.context.ip || void 0;
|
|
1096
1125
|
trackEvent({
|
|
1097
|
-
eventKey: visitorId ||
|
|
1126
|
+
eventKey: visitorId || ipAddress || untrustedVisitorId || "unknown",
|
|
1098
1127
|
eventType: "security_check",
|
|
1099
1128
|
eventDisplayName: ev.outcome === "blocked" ? "Security: blocked" : ev.outcome === "challenged" ? "Security: challenged" : "Security: passed",
|
|
1100
1129
|
eventData: {
|
|
@@ -1107,19 +1136,14 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
|
|
|
1107
1136
|
userAgent: options.userAgent,
|
|
1108
1137
|
details: ev.details
|
|
1109
1138
|
},
|
|
1110
|
-
ipAddress
|
|
1111
|
-
city: identification?.location?.city || void 0,
|
|
1112
|
-
country: identification?.location?.country?.name || void 0,
|
|
1113
|
-
countryCode: identification?.location?.country?.code || void 0
|
|
1139
|
+
ipAddress,
|
|
1140
|
+
city: location?.city || identification?.location?.city || void 0,
|
|
1141
|
+
country: location?.country || identification?.location?.country?.name || void 0,
|
|
1142
|
+
countryCode: location?.countryCode || identification?.location?.country?.code || void 0
|
|
1114
1143
|
});
|
|
1115
1144
|
}
|
|
1116
1145
|
//#endregion
|
|
1117
1146
|
//#region src/sentinel/security.ts
|
|
1118
|
-
async function hashForFingerprint(input) {
|
|
1119
|
-
const data = new TextEncoder().encode(input);
|
|
1120
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1121
|
-
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1122
|
-
}
|
|
1123
1147
|
async function sha1Hash(input) {
|
|
1124
1148
|
const data = new TextEncoder().encode(input);
|
|
1125
1149
|
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
|
|
@@ -1184,15 +1208,20 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1184
1208
|
default: return "credential_stuffing";
|
|
1185
1209
|
}
|
|
1186
1210
|
},
|
|
1187
|
-
async trackFailedAttempt(identifier, visitorId, password, ip) {
|
|
1211
|
+
async trackFailedAttempt(identifier, visitorId, password, ip, requestId = null) {
|
|
1188
1212
|
try {
|
|
1213
|
+
if (!conn.apiKey) {
|
|
1214
|
+
logger.warn("[Sentinel] Missing apiKey; failed-login password fingerprint is skipped.");
|
|
1215
|
+
return { blocked: false };
|
|
1216
|
+
}
|
|
1189
1217
|
const data = await $api("/security/track-failed-login", {
|
|
1190
1218
|
method: "POST",
|
|
1191
1219
|
body: {
|
|
1192
1220
|
identifier,
|
|
1193
1221
|
visitorId,
|
|
1194
|
-
passwordHash: await
|
|
1222
|
+
passwordHash: await hmacSha256Hex(conn.apiKey, password),
|
|
1195
1223
|
ip,
|
|
1224
|
+
requestId,
|
|
1196
1225
|
config: options
|
|
1197
1226
|
}
|
|
1198
1227
|
});
|
|
@@ -1221,23 +1250,25 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1221
1250
|
logger.error("[Dash] Clear failed attempts error:", error);
|
|
1222
1251
|
}
|
|
1223
1252
|
},
|
|
1224
|
-
async isBlocked(visitorId, ip) {
|
|
1253
|
+
async isBlocked(visitorId, ip, requestId = null) {
|
|
1225
1254
|
try {
|
|
1226
1255
|
const params = new URLSearchParams({ visitorId });
|
|
1227
1256
|
if (ip) params.set("ip", ip);
|
|
1257
|
+
if (requestId) params.set("requestId", requestId);
|
|
1228
1258
|
return (await $api(`/security/is-blocked?${params.toString()}`, { method: "GET" })).blocked ?? false;
|
|
1229
1259
|
} catch (error) {
|
|
1230
1260
|
logger.warn("[Dash] Security is-blocked check failed:", error);
|
|
1231
1261
|
return false;
|
|
1232
1262
|
}
|
|
1233
1263
|
},
|
|
1234
|
-
async verifyPoWSolution(visitorId, solution) {
|
|
1264
|
+
async verifyPoWSolution(visitorId, solution, requestId = null) {
|
|
1235
1265
|
try {
|
|
1236
1266
|
return await $api("/security/pow/verify", {
|
|
1237
1267
|
method: "POST",
|
|
1238
1268
|
body: {
|
|
1239
1269
|
visitorId,
|
|
1240
|
-
solution
|
|
1270
|
+
solution,
|
|
1271
|
+
requestId
|
|
1241
1272
|
}
|
|
1242
1273
|
});
|
|
1243
1274
|
} catch (error) {
|
|
@@ -1248,12 +1279,13 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1248
1279
|
};
|
|
1249
1280
|
}
|
|
1250
1281
|
},
|
|
1251
|
-
async generateChallenge(visitorId) {
|
|
1282
|
+
async generateChallenge(visitorId, requestId = null) {
|
|
1252
1283
|
try {
|
|
1253
1284
|
return (await $api("/security/pow/generate", {
|
|
1254
1285
|
method: "POST",
|
|
1255
1286
|
body: {
|
|
1256
1287
|
visitorId,
|
|
1288
|
+
requestId,
|
|
1257
1289
|
difficulty: options.challengeDifficulty
|
|
1258
1290
|
}
|
|
1259
1291
|
})).challenge || "";
|
|
@@ -1262,7 +1294,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1262
1294
|
return "";
|
|
1263
1295
|
}
|
|
1264
1296
|
},
|
|
1265
|
-
async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null, powSolution) {
|
|
1297
|
+
async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null, powSolution, requestId = null) {
|
|
1266
1298
|
if (!options.impossibleTravel?.enabled || !currentLocation) return null;
|
|
1267
1299
|
try {
|
|
1268
1300
|
const data = await $api("/security/impossible-travel", {
|
|
@@ -1270,6 +1302,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1270
1302
|
body: {
|
|
1271
1303
|
userId,
|
|
1272
1304
|
visitorId,
|
|
1305
|
+
requestId,
|
|
1273
1306
|
location: currentLocation,
|
|
1274
1307
|
ip,
|
|
1275
1308
|
config: options,
|
|
@@ -1319,20 +1352,23 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1319
1352
|
}
|
|
1320
1353
|
},
|
|
1321
1354
|
/**
|
|
1322
|
-
*
|
|
1355
|
+
* Atomically reserve a signup slot before user creation.
|
|
1323
1356
|
*/
|
|
1324
|
-
async
|
|
1357
|
+
async reserveFreeTrialSignup(visitorId, reservationId, requestId = null) {
|
|
1325
1358
|
if (!options.freeTrialAbuse?.enabled) return {
|
|
1359
|
+
reserved: true,
|
|
1326
1360
|
isAbuse: false,
|
|
1327
1361
|
accountCount: 0,
|
|
1328
1362
|
maxAccounts: 0,
|
|
1329
1363
|
action: "log"
|
|
1330
1364
|
};
|
|
1331
1365
|
try {
|
|
1332
|
-
const data = await $api("/security/free-trial-abuse/
|
|
1366
|
+
const data = await $api("/security/free-trial-abuse/reserve", {
|
|
1333
1367
|
method: "POST",
|
|
1334
1368
|
body: {
|
|
1335
1369
|
visitorId,
|
|
1370
|
+
reservationId,
|
|
1371
|
+
requestId,
|
|
1336
1372
|
config: options
|
|
1337
1373
|
}
|
|
1338
1374
|
});
|
|
@@ -1350,8 +1386,9 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1350
1386
|
});
|
|
1351
1387
|
return data;
|
|
1352
1388
|
} catch (error) {
|
|
1353
|
-
logger.warn("[Dash] Free trial abuse
|
|
1389
|
+
logger.warn("[Dash] Free trial abuse reserve failed:", error);
|
|
1354
1390
|
return {
|
|
1391
|
+
reserved: true,
|
|
1355
1392
|
isAbuse: false,
|
|
1356
1393
|
accountCount: 0,
|
|
1357
1394
|
maxAccounts: 0,
|
|
@@ -1360,21 +1397,22 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
|
|
|
1360
1397
|
}
|
|
1361
1398
|
},
|
|
1362
1399
|
/**
|
|
1363
|
-
*
|
|
1364
|
-
* Stores the userId for auditing purposes.
|
|
1400
|
+
* Confirm a reserved signup slot with the created user id.
|
|
1365
1401
|
*/
|
|
1366
|
-
async
|
|
1402
|
+
async confirmFreeTrialSignup(visitorId, reservationId, userId, requestId = null) {
|
|
1367
1403
|
if (!options.freeTrialAbuse?.enabled) return;
|
|
1368
1404
|
try {
|
|
1369
|
-
await $api("/security/free-trial-abuse/
|
|
1405
|
+
await $api("/security/free-trial-abuse/confirm", {
|
|
1370
1406
|
method: "POST",
|
|
1371
1407
|
body: {
|
|
1372
1408
|
visitorId,
|
|
1373
|
-
|
|
1409
|
+
reservationId,
|
|
1410
|
+
userId,
|
|
1411
|
+
requestId
|
|
1374
1412
|
}
|
|
1375
1413
|
});
|
|
1376
1414
|
} catch (error) {
|
|
1377
|
-
logger.error("[Dash]
|
|
1415
|
+
logger.error("[Dash] Confirm free trial signup error:", error);
|
|
1378
1416
|
}
|
|
1379
1417
|
},
|
|
1380
1418
|
async checkCompromisedPassword(password) {
|
|
@@ -2084,7 +2122,7 @@ async function handleSecurityVerdict(verdict, hookCtx, checkCtx, trackEvent, sec
|
|
|
2084
2122
|
if (verdict.action === "allow") return;
|
|
2085
2123
|
const reason = verdict.reason || "unknown";
|
|
2086
2124
|
if (verdict.action === "challenge" && checkCtx.visitorId) {
|
|
2087
|
-
const challenge = verdict.challenge || await securityService.generateChallenge(checkCtx.visitorId);
|
|
2125
|
+
const challenge = verdict.challenge || await securityService.generateChallenge(checkCtx.visitorId, checkCtx.identification?.requestId ?? null);
|
|
2088
2126
|
if (!challenge?.trim()) {
|
|
2089
2127
|
logger.warn("[Sentinel] Could not generate PoW challenge (service may be unavailable). Falling back to allow.");
|
|
2090
2128
|
return;
|
|
@@ -2119,7 +2157,7 @@ async function runSecurityChecks(hookCtx, checkCtx, securityService, trackEvent,
|
|
|
2119
2157
|
recordCheck(hookCtx, "server_security_check");
|
|
2120
2158
|
await handleSecurityVerdict(await securityService.checkSecurity({
|
|
2121
2159
|
visitorId: checkCtx.visitorId,
|
|
2122
|
-
requestId: checkCtx.identification?.requestId
|
|
2160
|
+
requestId: checkCtx.identification?.requestId ?? checkCtx.requestId ?? null,
|
|
2123
2161
|
ip: checkCtx.ip,
|
|
2124
2162
|
path: checkCtx.path,
|
|
2125
2163
|
identifier: checkCtx.identifier,
|
|
@@ -2174,9 +2212,17 @@ const sentinel = (options) => {
|
|
|
2174
2212
|
async before(user, ctx) {
|
|
2175
2213
|
if (!ctx) return;
|
|
2176
2214
|
const visitorId = ctx.context.visitorId;
|
|
2177
|
-
if (
|
|
2215
|
+
if (opts.security?.freeTrialAbuse?.enabled) {
|
|
2216
|
+
if (!visitorId) {
|
|
2217
|
+
recordCheck(ctx, "free_trial_abuse");
|
|
2218
|
+
setOutcome(ctx, "blocked", "free_trial_abuse", { reason: "identification_required" });
|
|
2219
|
+
emitEvaluation(ctx, trackEvent);
|
|
2220
|
+
throw new APIError("FORBIDDEN", { message: "Account creation is not allowed without device identification." });
|
|
2221
|
+
}
|
|
2178
2222
|
recordCheck(ctx, "free_trial_abuse");
|
|
2179
|
-
const
|
|
2223
|
+
const reservationId = crypto.randomUUID();
|
|
2224
|
+
ctx.context.freeTrialReservationId = reservationId;
|
|
2225
|
+
const abuseCheck = await securityService.reserveFreeTrialSignup(visitorId, reservationId, ctx.context.requestId ?? null);
|
|
2180
2226
|
if (abuseCheck.isAbuse && abuseCheck.action === "block") {
|
|
2181
2227
|
setOutcome(ctx, "blocked", "free_trial_abuse", {
|
|
2182
2228
|
accountCount: abuseCheck.accountCount,
|
|
@@ -2194,7 +2240,8 @@ const sentinel = (options) => {
|
|
|
2194
2240
|
async after(user, ctx) {
|
|
2195
2241
|
if (!ctx) return;
|
|
2196
2242
|
const visitorId = ctx.context.visitorId;
|
|
2197
|
-
|
|
2243
|
+
const reservationId = ctx.context.freeTrialReservationId;
|
|
2244
|
+
if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
|
|
2198
2245
|
}
|
|
2199
2246
|
},
|
|
2200
2247
|
update: { async before(user, ctx) {
|
|
@@ -2213,7 +2260,7 @@ const sentinel = (options) => {
|
|
|
2213
2260
|
if (session.userId && identification?.location && visitorId) {
|
|
2214
2261
|
recordCheck(ctx, "impossible_travel");
|
|
2215
2262
|
const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
|
|
2216
|
-
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution);
|
|
2263
|
+
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution, identification.requestId);
|
|
2217
2264
|
if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
|
|
2218
2265
|
if (travelCheck?.isImpossible) {
|
|
2219
2266
|
if (travelCheck.action === "block") {
|
|
@@ -2324,7 +2371,11 @@ const sentinel = (options) => {
|
|
|
2324
2371
|
routes.SIGN_IN_ANONYMOUS
|
|
2325
2372
|
]);
|
|
2326
2373
|
const isSignUp = matchesAnyRoute(ctx.path, [routes.SIGN_UP_EMAIL]);
|
|
2327
|
-
const isPasswordReset = matchesAnyRoute(ctx.path, [
|
|
2374
|
+
const isPasswordReset = matchesAnyRoute(ctx.path, [
|
|
2375
|
+
routes.FORGET_PASSWORD,
|
|
2376
|
+
routes.REQUEST_PASSWORD_RESET,
|
|
2377
|
+
routes.RESET_PASSWORD
|
|
2378
|
+
]);
|
|
2328
2379
|
const isTwoFactor = matchesAnyRoute(ctx.path, [
|
|
2329
2380
|
routes.TWO_FACTOR_VERIFY_TOTP,
|
|
2330
2381
|
routes.TWO_FACTOR_VERIFY_BACKUP,
|
|
@@ -2346,7 +2397,7 @@ const sentinel = (options) => {
|
|
|
2346
2397
|
const powSolution = ctx.headers?.get?.("X-PoW-Solution");
|
|
2347
2398
|
if (visitorId || ip) {
|
|
2348
2399
|
recordCheck(ctx, "credential_stuffing");
|
|
2349
|
-
if (await securityService.isBlocked(visitorId ?? "", ip)) {
|
|
2400
|
+
if (await securityService.isBlocked(visitorId ?? "", ip, ctx.context.requestId ?? null)) {
|
|
2350
2401
|
setOutcome(ctx, "blocked", "credential_stuffing", {
|
|
2351
2402
|
phase: "cooldown_active",
|
|
2352
2403
|
visitorId: ctx.context.untrustedVisitorId ?? void 0,
|
|
@@ -2363,6 +2414,7 @@ const sentinel = (options) => {
|
|
|
2363
2414
|
path: ctx.path,
|
|
2364
2415
|
identifier,
|
|
2365
2416
|
visitorId,
|
|
2417
|
+
requestId: ctx.context.requestId ?? null,
|
|
2366
2418
|
identification,
|
|
2367
2419
|
ip: ctx.context.ip,
|
|
2368
2420
|
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
@@ -2412,7 +2464,7 @@ const sentinel = (options) => {
|
|
|
2412
2464
|
identifier: loginId,
|
|
2413
2465
|
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2414
2466
|
});
|
|
2415
|
-
if (isPasswordSignInRoute && ctx.context.returned instanceof Error && loginId && body?.password && untrustedVisitorId) await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(loginId, untrustedVisitorId, body.password, ip));
|
|
2467
|
+
if (isPasswordSignInRoute && ctx.context.returned instanceof Error && loginId && body?.password && untrustedVisitorId) await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(loginId, untrustedVisitorId, body.password, ip, ctx.context.requestId ?? null));
|
|
2416
2468
|
if (isPasswordSignInRoute && !(ctx.context.returned instanceof Error) && loginId) await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(loginId));
|
|
2417
2469
|
})
|
|
2418
2470
|
}]
|
|
@@ -2774,11 +2826,133 @@ const initTeamEvents = (tracker) => {
|
|
|
2774
2826
|
};
|
|
2775
2827
|
};
|
|
2776
2828
|
//#endregion
|
|
2777
|
-
//#region
|
|
2829
|
+
//#region ../utils/dist/redact.mjs
|
|
2830
|
+
const EXACT_SENSITIVE_STRING_KEYS_LOWER = new Set([...new Set([
|
|
2831
|
+
"accessToken",
|
|
2832
|
+
"apiKey",
|
|
2833
|
+
"apiSecret",
|
|
2834
|
+
"authorization",
|
|
2835
|
+
"authToken",
|
|
2836
|
+
"bearerToken",
|
|
2837
|
+
"backupCodes",
|
|
2838
|
+
"clientSecret",
|
|
2839
|
+
"consumerSecret",
|
|
2840
|
+
"credentialID",
|
|
2841
|
+
"deviceCode",
|
|
2842
|
+
"encPrivateKey",
|
|
2843
|
+
"encPrivateKeyPass",
|
|
2844
|
+
"encryptionKey",
|
|
2845
|
+
"encryptionSecret",
|
|
2846
|
+
"forwardHeaders",
|
|
2847
|
+
"idToken",
|
|
2848
|
+
"oidcConfig",
|
|
2849
|
+
"pass",
|
|
2850
|
+
"passwd",
|
|
2851
|
+
"password",
|
|
2852
|
+
"privateKey",
|
|
2853
|
+
"privateKeyPass",
|
|
2854
|
+
"pwd",
|
|
2855
|
+
"refreshToken",
|
|
2856
|
+
"samlConfig",
|
|
2857
|
+
"secret",
|
|
2858
|
+
"secretAccessKey",
|
|
2859
|
+
"secretKey",
|
|
2860
|
+
"signingSecret",
|
|
2861
|
+
"stripeWebhookSecret",
|
|
2862
|
+
"userCode",
|
|
2863
|
+
"webhookSecret"
|
|
2864
|
+
])].map((k) => k.toLowerCase()));
|
|
2865
|
+
/**
|
|
2866
|
+
* Case-insensitive suffixes for unknown property names.
|
|
2867
|
+
*/
|
|
2868
|
+
const SENSITIVE_KEY_SUFFIXES_LOWER = [
|
|
2869
|
+
"accesskey",
|
|
2870
|
+
"secret",
|
|
2871
|
+
"password",
|
|
2872
|
+
"passphrase",
|
|
2873
|
+
"privatekey",
|
|
2874
|
+
"keypass",
|
|
2875
|
+
"apikey",
|
|
2876
|
+
"token",
|
|
2877
|
+
"signingkey",
|
|
2878
|
+
"credentials",
|
|
2879
|
+
"authheader"
|
|
2880
|
+
];
|
|
2881
|
+
/**
|
|
2882
|
+
* Embedded secret substrings checked against the normalized (compact, lowercased) key.
|
|
2883
|
+
*/
|
|
2884
|
+
const SENSITIVE_KEY_SUBSTRINGS_LOWER = ["secretaccess", "accesskeysecret"];
|
|
2885
|
+
const REDACTED_SIMPLE_STRING = "[REDACTED]";
|
|
2886
|
+
function snakeCaseToCamelCase(key) {
|
|
2887
|
+
return key.replace(/_([a-zA-Z])/g, (_, ch) => ch.toUpperCase());
|
|
2888
|
+
}
|
|
2889
|
+
function keyVariantsForMatching(key) {
|
|
2890
|
+
const trimmed = key.replace(/^[\s._-]+/, "");
|
|
2891
|
+
if (!trimmed) return [key];
|
|
2892
|
+
const out = new Set([key, trimmed]);
|
|
2893
|
+
if (trimmed.includes("_")) out.add(snakeCaseToCamelCase(trimmed));
|
|
2894
|
+
return [...out];
|
|
2895
|
+
}
|
|
2896
|
+
/** Whether a property name should have string values redacted in `redact()`. */
|
|
2897
|
+
function isSensitiveStringKey(key) {
|
|
2898
|
+
const variants = keyVariantsForMatching(key);
|
|
2899
|
+
const compactLower = key.replace(/[\s._-]/g, "").toLowerCase();
|
|
2900
|
+
const forms = new Set([compactLower]);
|
|
2901
|
+
for (const raw of variants) {
|
|
2902
|
+
forms.add(raw);
|
|
2903
|
+
forms.add(raw.toLowerCase());
|
|
2904
|
+
}
|
|
2905
|
+
for (const form of forms) {
|
|
2906
|
+
const fl = form.toLowerCase();
|
|
2907
|
+
if (EXACT_SENSITIVE_STRING_KEYS_LOWER.has(fl)) return true;
|
|
2908
|
+
for (const suffix of SENSITIVE_KEY_SUFFIXES_LOWER) if (fl.endsWith(suffix)) return true;
|
|
2909
|
+
for (const substring of SENSITIVE_KEY_SUBSTRINGS_LOWER) if (compactLower.includes(substring)) return true;
|
|
2910
|
+
}
|
|
2911
|
+
return false;
|
|
2912
|
+
}
|
|
2913
|
+
function isPlainSerializable(value) {
|
|
2914
|
+
if (value === null || typeof value !== "object") return true;
|
|
2915
|
+
if (Array.isArray(value)) return true;
|
|
2916
|
+
if (value instanceof Date) return true;
|
|
2917
|
+
const constructor = Object.getPrototypeOf(value)?.constructor;
|
|
2918
|
+
if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2921
|
+
function redactInner(value, visiting, options) {
|
|
2922
|
+
if (value === null || value === void 0) return value;
|
|
2923
|
+
if (typeof value === "function") return void 0;
|
|
2924
|
+
if (typeof value !== "object") return value;
|
|
2925
|
+
const obj = value;
|
|
2926
|
+
if (visiting.has(obj)) return void 0;
|
|
2927
|
+
visiting.add(obj);
|
|
2928
|
+
try {
|
|
2929
|
+
if (Array.isArray(value)) return value.map((item) => redactInner(item, visiting, options)).filter((item) => item !== void 0);
|
|
2930
|
+
const result = {};
|
|
2931
|
+
for (const [key, val] of Object.entries(value)) {
|
|
2932
|
+
if (options?.excludeKeys?.has(key)) continue;
|
|
2933
|
+
if (typeof val === "function") continue;
|
|
2934
|
+
if (typeof val === "string" && isSensitiveStringKey(key)) {
|
|
2935
|
+
result[key] = REDACTED_SIMPLE_STRING;
|
|
2936
|
+
continue;
|
|
2937
|
+
}
|
|
2938
|
+
if (options?.skipNonPlainSerializable && val !== null && typeof val === "object" && !isPlainSerializable(val)) continue;
|
|
2939
|
+
const redacted = redactInner(val, visiting, options);
|
|
2940
|
+
if (redacted !== void 0) result[key] = redacted;
|
|
2941
|
+
}
|
|
2942
|
+
return result;
|
|
2943
|
+
} finally {
|
|
2944
|
+
visiting.delete(obj);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
/** Recursively redact sensitive string values in a JSON-like object tree. */
|
|
2948
|
+
function redact(value, options) {
|
|
2949
|
+
return redactInner(value, /* @__PURE__ */ new WeakSet(), options);
|
|
2950
|
+
}
|
|
2951
|
+
//#endregion
|
|
2952
|
+
//#region ../utils/dist/crypto/index.mjs
|
|
2778
2953
|
/**
|
|
2779
|
-
*
|
|
2780
|
-
*
|
|
2781
|
-
* @param value - The value to hash
|
|
2954
|
+
* One-way hash for storage and lookup. Returns hex-encoded digest.
|
|
2955
|
+
* Use to verify values without ever storing plaintext.
|
|
2782
2956
|
*/
|
|
2783
2957
|
async function hash(value) {
|
|
2784
2958
|
const data = new TextEncoder().encode(value);
|
|
@@ -2786,6 +2960,32 @@ async function hash(value) {
|
|
|
2786
2960
|
const hashArray = new Uint8Array(hashBuffer);
|
|
2787
2961
|
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2788
2962
|
}
|
|
2963
|
+
function hexToBytes(hex) {
|
|
2964
|
+
if (hex.length % 2 !== 0) return null;
|
|
2965
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
2966
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2967
|
+
const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
2968
|
+
if (Number.isNaN(byte)) return null;
|
|
2969
|
+
bytes[i] = byte;
|
|
2970
|
+
}
|
|
2971
|
+
return bytes;
|
|
2972
|
+
}
|
|
2973
|
+
function timingSafeEqualBytes(a, b) {
|
|
2974
|
+
if (a.length !== b.length) return false;
|
|
2975
|
+
let diff = 0;
|
|
2976
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
2977
|
+
return diff === 0;
|
|
2978
|
+
}
|
|
2979
|
+
/** Constant-time comparison for hex-encoded digests (e.g. SHA-256 hashes). */
|
|
2980
|
+
function timingSafeEqualHash(a, b) {
|
|
2981
|
+
if (a.length !== b.length) return false;
|
|
2982
|
+
const aBytes = hexToBytes(a);
|
|
2983
|
+
const bBytes = hexToBytes(b);
|
|
2984
|
+
if (!aBytes || !bBytes || aBytes.length !== bBytes.length) return false;
|
|
2985
|
+
return timingSafeEqualBytes(aBytes, bBytes);
|
|
2986
|
+
}
|
|
2987
|
+
//#endregion
|
|
2988
|
+
//#region src/jwt.ts
|
|
2789
2989
|
/**
|
|
2790
2990
|
* Skip JTI check for JWTs issued within this many seconds.
|
|
2791
2991
|
* This is safe because replay attacks require time for interception.
|
|
@@ -2853,9 +3053,8 @@ const jwtMiddleware = (options, schema, getJWT) => {
|
|
|
2853
3053
|
});
|
|
2854
3054
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2855
3055
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
ctx.context.logger.warn("[Dash] API key hash is invalid", apiKeyHash, expectedHash);
|
|
3056
|
+
if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
|
|
3057
|
+
ctx.context.logger.warn("[Dash] API key hash mismatch");
|
|
2859
3058
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2860
3059
|
}
|
|
2861
3060
|
if (!isRecentlyIssued(payload)) {
|
|
@@ -2908,7 +3107,7 @@ const jwtValidateMiddleware = (options) => {
|
|
|
2908
3107
|
});
|
|
2909
3108
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2910
3109
|
}
|
|
2911
|
-
if (apiKeyHash
|
|
3110
|
+
if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
|
|
2912
3111
|
ctx.context.logger.warn("[Dash] API key hash mismatch");
|
|
2913
3112
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2914
3113
|
}
|
|
@@ -2916,121 +3115,20 @@ const jwtValidateMiddleware = (options) => {
|
|
|
2916
3115
|
});
|
|
2917
3116
|
};
|
|
2918
3117
|
//#endregion
|
|
2919
|
-
//#region src/
|
|
2920
|
-
const PLUGIN_VERSION = "0.2.13";
|
|
2921
|
-
//#endregion
|
|
2922
|
-
//#region src/routes/auth/config.ts
|
|
3118
|
+
//#region src/lib/redact-plugin-options.ts
|
|
2923
3119
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
"authToken",
|
|
2930
|
-
"bearerToken",
|
|
2931
|
-
"clientSecret",
|
|
2932
|
-
"consumerSecret",
|
|
2933
|
-
"encPrivateKey",
|
|
2934
|
-
"encPrivateKeyPass",
|
|
2935
|
-
"encryptionKey",
|
|
2936
|
-
"encryptionSecret",
|
|
2937
|
-
"idToken",
|
|
2938
|
-
"password",
|
|
2939
|
-
"privateKey",
|
|
2940
|
-
"privateKeyPass",
|
|
2941
|
-
"refreshToken",
|
|
2942
|
-
"secret",
|
|
2943
|
-
"secretKey",
|
|
2944
|
-
"signingSecret",
|
|
2945
|
-
"stripeWebhookSecret",
|
|
2946
|
-
"webhookSecret"
|
|
2947
|
-
])].map((k) => k.toLowerCase()));
|
|
2948
|
-
/**
|
|
2949
|
-
* Case-insensitive suffixes for unknown plugin options
|
|
2950
|
-
* Matching is more permissive to account for unknown plugin options
|
|
2951
|
-
*/
|
|
2952
|
-
const SENSITIVE_KEY_SUFFIXES_LOWER = [
|
|
2953
|
-
"secret",
|
|
2954
|
-
"password",
|
|
2955
|
-
"passphrase",
|
|
2956
|
-
"privatekey",
|
|
2957
|
-
"keypass",
|
|
2958
|
-
"apikey",
|
|
2959
|
-
"token",
|
|
2960
|
-
"signingkey",
|
|
2961
|
-
"credentials",
|
|
2962
|
-
"authheader"
|
|
2963
|
-
];
|
|
2964
|
-
const REDACTED_SIMPLE_STRING = "[REDACTED]";
|
|
2965
|
-
function snakeCaseToCamelCase(key) {
|
|
2966
|
-
return key.replace(/_([a-zA-Z])/g, (_, ch) => ch.toUpperCase());
|
|
3120
|
+
function redactPluginOptions(pluginId, options, optionsConfig) {
|
|
3121
|
+
return redact(options, {
|
|
3122
|
+
...optionsConfig,
|
|
3123
|
+
excludeKeys: PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId]
|
|
3124
|
+
});
|
|
2967
3125
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
const
|
|
2972
|
-
if (
|
|
2973
|
-
return
|
|
2974
|
-
}
|
|
2975
|
-
function shouldRedactSensitiveStringKey(key) {
|
|
2976
|
-
const variants = keyVariantsForMatching(key);
|
|
2977
|
-
const compactLower = key.replace(/[\s._-]/g, "").toLowerCase();
|
|
2978
|
-
const forms = new Set([compactLower]);
|
|
2979
|
-
for (const raw of variants) {
|
|
2980
|
-
forms.add(raw);
|
|
2981
|
-
forms.add(raw.toLowerCase());
|
|
2982
|
-
}
|
|
2983
|
-
for (const form of forms) {
|
|
2984
|
-
const fl = form.toLowerCase();
|
|
2985
|
-
if (EXACT_SENSITIVE_STRING_KEYS_LOWER.has(fl)) return true;
|
|
2986
|
-
for (const suffix of SENSITIVE_KEY_SUFFIXES_LOWER) if (fl.endsWith(suffix)) return true;
|
|
2987
|
-
}
|
|
2988
|
-
return false;
|
|
2989
|
-
}
|
|
2990
|
-
function isPlainSerializable(value) {
|
|
2991
|
-
if (value === null || typeof value !== "object") return true;
|
|
2992
|
-
if (Array.isArray(value)) return true;
|
|
2993
|
-
if (value instanceof Date) return true;
|
|
2994
|
-
const constructor = Object.getPrototypeOf(value)?.constructor;
|
|
2995
|
-
if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
|
|
2996
|
-
return true;
|
|
2997
|
-
}
|
|
2998
|
-
/**
|
|
2999
|
-
* Sanitize any plain JSON-like subtree from Better Auth options
|
|
3000
|
-
*/
|
|
3001
|
-
function sanitizeConfig(value, visiting, excludeKeys) {
|
|
3002
|
-
if (value === null || value === void 0) return value;
|
|
3003
|
-
if (typeof value === "function") return void 0;
|
|
3004
|
-
if (typeof value !== "object") return value;
|
|
3005
|
-
const obj = value;
|
|
3006
|
-
if (visiting.has(obj)) return void 0;
|
|
3007
|
-
visiting.add(obj);
|
|
3008
|
-
try {
|
|
3009
|
-
if (Array.isArray(value)) return value.map((item) => sanitizeConfig(item, visiting, excludeKeys)).filter((item) => item !== void 0);
|
|
3010
|
-
const result = {};
|
|
3011
|
-
for (const [key, val] of Object.entries(value)) {
|
|
3012
|
-
if (excludeKeys?.has(key)) continue;
|
|
3013
|
-
if (typeof val === "function") continue;
|
|
3014
|
-
if (typeof val === "string" && shouldRedactSensitiveStringKey(key)) {
|
|
3015
|
-
result[key] = REDACTED_SIMPLE_STRING;
|
|
3016
|
-
continue;
|
|
3017
|
-
}
|
|
3018
|
-
if (val !== null && typeof val === "object" && !isPlainSerializable(val)) continue;
|
|
3019
|
-
const sanitized = sanitizeConfig(val, visiting, excludeKeys);
|
|
3020
|
-
if (sanitized !== void 0) result[key] = sanitized;
|
|
3021
|
-
}
|
|
3022
|
-
return result;
|
|
3023
|
-
} finally {
|
|
3024
|
-
visiting.delete(obj);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
function sanitizePluginOptions(pluginId, options) {
|
|
3028
|
-
return sanitizeConfig(options, /* @__PURE__ */ new WeakSet(), PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId]);
|
|
3029
|
-
}
|
|
3030
|
-
function estimateEntropy(str) {
|
|
3031
|
-
const unique = new Set(str).size;
|
|
3032
|
-
if (unique === 0) return 0;
|
|
3033
|
-
return Math.log2(Math.pow(unique, str.length));
|
|
3126
|
+
//#endregion
|
|
3127
|
+
//#region src/routes/auth/config.ts
|
|
3128
|
+
function estimateEntropy(str) {
|
|
3129
|
+
const unique = new Set(str).size;
|
|
3130
|
+
if (unique === 0) return 0;
|
|
3131
|
+
return Math.log2(Math.pow(unique, str.length));
|
|
3034
3132
|
}
|
|
3035
3133
|
const getConfig = (options) => {
|
|
3036
3134
|
return createAuthEndpoint("/dash/config", {
|
|
@@ -3042,13 +3140,13 @@ const getConfig = (options) => {
|
|
|
3042
3140
|
return {
|
|
3043
3141
|
version: ctx.context.version || null,
|
|
3044
3142
|
socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
|
|
3045
|
-
emailAndPassword:
|
|
3143
|
+
emailAndPassword: redact(ctx.context.options.emailAndPassword, { skipNonPlainSerializable: true }),
|
|
3046
3144
|
plugins: ctx.context.options.plugins?.map((plugin) => {
|
|
3047
3145
|
const base = {
|
|
3048
3146
|
id: plugin.id,
|
|
3049
3147
|
schema: plugin.schema,
|
|
3050
3148
|
version: plugin.version,
|
|
3051
|
-
options:
|
|
3149
|
+
options: redactPluginOptions(plugin.id, plugin.options, { skipNonPlainSerializable: true })
|
|
3052
3150
|
};
|
|
3053
3151
|
if (plugin.id === "dash" && !plugin.version) return {
|
|
3054
3152
|
...base,
|
|
@@ -3170,6 +3268,31 @@ const getValidate = (options) => {
|
|
|
3170
3268
|
});
|
|
3171
3269
|
};
|
|
3172
3270
|
//#endregion
|
|
3271
|
+
//#region src/semver.ts
|
|
3272
|
+
function parseVersion(version) {
|
|
3273
|
+
if (!version) return null;
|
|
3274
|
+
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
3275
|
+
if (!match) return null;
|
|
3276
|
+
return [
|
|
3277
|
+
Number(match[1]),
|
|
3278
|
+
Number(match[2]),
|
|
3279
|
+
Number(match[3])
|
|
3280
|
+
];
|
|
3281
|
+
}
|
|
3282
|
+
/** Compare semver strings (e.g. "1.7.0-beta.8" against "1.7" or "1.7.0"). */
|
|
3283
|
+
function isVersionAtLeast(version, target) {
|
|
3284
|
+
const parsed = parseVersion(version);
|
|
3285
|
+
if (!parsed) return false;
|
|
3286
|
+
const targetParts = target.split(".").map(Number);
|
|
3287
|
+
const major = targetParts[0] ?? 0;
|
|
3288
|
+
const minor = targetParts[1] ?? 0;
|
|
3289
|
+
const patch = targetParts[2] ?? 0;
|
|
3290
|
+
const [vMajor, vMinor, vPatch] = parsed;
|
|
3291
|
+
if (vMajor !== major) return vMajor > major;
|
|
3292
|
+
if (vMinor !== minor) return vMinor > minor;
|
|
3293
|
+
return vPatch >= patch;
|
|
3294
|
+
}
|
|
3295
|
+
//#endregion
|
|
3173
3296
|
//#region src/routes/organization-guards.ts
|
|
3174
3297
|
/** Returns true if organization plugin is enabled. */
|
|
3175
3298
|
function isOrganizationEnabled(ctx) {
|
|
@@ -3231,6 +3354,11 @@ function getScimEndpoint(baseUrl) {
|
|
|
3231
3354
|
function getSCIMPlugin(ctx) {
|
|
3232
3355
|
return ctx.context.getPlugin("scim");
|
|
3233
3356
|
}
|
|
3357
|
+
function isScimDirectorySyncEnabled(scimPlugin, authVersion) {
|
|
3358
|
+
if (!scimPlugin) return false;
|
|
3359
|
+
if (isVersionAtLeast(authVersion, "1.7.0")) return true;
|
|
3360
|
+
return scimPlugin.options?.providerOwnership?.enabled === true;
|
|
3361
|
+
}
|
|
3234
3362
|
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.";
|
|
3235
3363
|
function isDuplicateDirectorySyncError(e) {
|
|
3236
3364
|
if (e instanceof APIError$1) return e.status === "CONFLICT" || /duplicate|already exists|unique constraint/i.test(e.message ?? "");
|
|
@@ -3248,7 +3376,7 @@ const listOrganizationDirectories = (options) => {
|
|
|
3248
3376
|
}
|
|
3249
3377
|
const organizationId = tryDecode(ctx.params.id);
|
|
3250
3378
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3251
|
-
if (!scimPlugin?.endpoints.listSCIMProviderConnections || !scimPlugin
|
|
3379
|
+
if (!scimPlugin?.endpoints.listSCIMProviderConnections || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) {
|
|
3252
3380
|
ctx.context.logger.warn("[Dash] SCIM plugin not available or provider ownership disabled, returning empty directories list", { organizationId });
|
|
3253
3381
|
return [];
|
|
3254
3382
|
}
|
|
@@ -3285,7 +3413,7 @@ const createOrganizationDirectory = (options) => {
|
|
|
3285
3413
|
requireOrganizationPlugin(ctx);
|
|
3286
3414
|
const { organizationId } = ctx.context.payload;
|
|
3287
3415
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3288
|
-
if (!scimPlugin?.endpoints.generateSCIMToken || !scimPlugin
|
|
3416
|
+
if (!scimPlugin?.endpoints.generateSCIMToken || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3289
3417
|
const { providerId, ownerUserId } = ctx.body;
|
|
3290
3418
|
let scimToken;
|
|
3291
3419
|
try {
|
|
@@ -3320,7 +3448,7 @@ const deleteOrganizationDirectory = (options) => {
|
|
|
3320
3448
|
}, async (ctx) => {
|
|
3321
3449
|
requireOrganizationPlugin(ctx);
|
|
3322
3450
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3323
|
-
if (!scimPlugin?.endpoints.deleteSCIMProviderConnection || !scimPlugin
|
|
3451
|
+
if (!scimPlugin?.endpoints.deleteSCIMProviderConnection || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3324
3452
|
const { organizationId } = ctx.context.payload;
|
|
3325
3453
|
const { providerId } = ctx.body;
|
|
3326
3454
|
const ownerUserId = await getScimProviderOwner(ctx, organizationId, providerId);
|
|
@@ -3341,7 +3469,7 @@ const regenerateDirectoryToken = (options) => {
|
|
|
3341
3469
|
}, async (ctx) => {
|
|
3342
3470
|
requireOrganizationPlugin(ctx);
|
|
3343
3471
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3344
|
-
if (!scimPlugin?.endpoints.generateSCIMToken || !scimPlugin
|
|
3472
|
+
if (!scimPlugin?.endpoints.generateSCIMToken || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3345
3473
|
const { organizationId } = ctx.context.payload;
|
|
3346
3474
|
const { providerId } = ctx.body;
|
|
3347
3475
|
const ownerUserId = await getScimProviderOwner(ctx, organizationId, providerId);
|
|
@@ -3758,6 +3886,36 @@ const executeAdapter = (options) => {
|
|
|
3758
3886
|
});
|
|
3759
3887
|
};
|
|
3760
3888
|
//#endregion
|
|
3889
|
+
//#region src/lib/invitation-auth-mode.ts
|
|
3890
|
+
/** Route new users through the platform /invite/accept flow. */
|
|
3891
|
+
function usesPlatformInviteAcceptFlow(authMode) {
|
|
3892
|
+
return authMode === "auth" || authMode === "credential_setup" || authMode === "create_with_session" || authMode === "create_no_session";
|
|
3893
|
+
}
|
|
3894
|
+
/** Whether complete-invitation should mint a session cookie. */
|
|
3895
|
+
function shouldCreateSessionOnInviteComplete(authMode) {
|
|
3896
|
+
return authMode !== "create_no_session";
|
|
3897
|
+
}
|
|
3898
|
+
//#endregion
|
|
3899
|
+
//#region src/lib/safe-url.ts
|
|
3900
|
+
function isSafeHttpUrl(value) {
|
|
3901
|
+
try {
|
|
3902
|
+
const url = new URL(value);
|
|
3903
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
|
|
3904
|
+
} catch {
|
|
3905
|
+
return false;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
/** http(s) absolute URL without embedded credentials. */
|
|
3909
|
+
const safeUrlSchema = z$1.string().trim().min(1).pipe(z$1.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
|
|
3910
|
+
const optionalSafeUrlSchema = z$1.union([safeUrlSchema, z$1.literal("")]).optional();
|
|
3911
|
+
/** Validates http(s) redirect targets at use time; falls back when invalid. */
|
|
3912
|
+
function resolveSafeRedirectUrl(raw, fallback) {
|
|
3913
|
+
if (raw == null || raw.trim() === "") return fallback;
|
|
3914
|
+
const parsed = safeUrlSchema.safeParse(raw);
|
|
3915
|
+
if (!parsed.success) return fallback;
|
|
3916
|
+
return parsed.data;
|
|
3917
|
+
}
|
|
3918
|
+
//#endregion
|
|
3761
3919
|
//#region src/routes/invitations/index.ts
|
|
3762
3920
|
async function verifyPendingInvitation(token, $api, ctx) {
|
|
3763
3921
|
const { data: invitation, error } = await $api("/api/internal/invitations/verify", {
|
|
@@ -3784,6 +3942,9 @@ async function verifyPendingInvitation(token, $api, ctx) {
|
|
|
3784
3942
|
function getFallbackRedirect(ctx) {
|
|
3785
3943
|
return typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : "/";
|
|
3786
3944
|
}
|
|
3945
|
+
function resolveInvitationRedirect(ctx, raw) {
|
|
3946
|
+
return resolveSafeRedirectUrl(raw, getFallbackRedirect(ctx));
|
|
3947
|
+
}
|
|
3787
3948
|
/**
|
|
3788
3949
|
* Accept invitation endpoint
|
|
3789
3950
|
* This is called when a user clicks the invitation link.
|
|
@@ -3808,14 +3969,14 @@ const acceptInvitation = (options) => {
|
|
|
3808
3969
|
}
|
|
3809
3970
|
});
|
|
3810
3971
|
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted (existing user)", markError);
|
|
3811
|
-
await setSessionCookie(ctx, {
|
|
3972
|
+
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
3812
3973
|
session: await ctx.context.internalAdapter.createSession(existingUser.id),
|
|
3813
3974
|
user: existingUser
|
|
3814
3975
|
});
|
|
3815
|
-
const redirectUrl = invitation.redirectUrl
|
|
3816
|
-
return ctx.redirect(redirectUrl
|
|
3976
|
+
const redirectUrl = resolveInvitationRedirect(ctx, invitation.redirectUrl);
|
|
3977
|
+
return ctx.redirect(redirectUrl);
|
|
3817
3978
|
}
|
|
3818
|
-
if (invitation.authMode
|
|
3979
|
+
if (usesPlatformInviteAcceptFlow(invitation.authMode)) {
|
|
3819
3980
|
const platformUrl = options.apiUrl || INFRA_API_URL;
|
|
3820
3981
|
const acceptPageUrl = new URL("/invite/accept", platformUrl);
|
|
3821
3982
|
acceptPageUrl.searchParams.set("token", token);
|
|
@@ -3823,13 +3984,15 @@ const acceptInvitation = (options) => {
|
|
|
3823
3984
|
acceptPageUrl.searchParams.set("callback", callbackUrl);
|
|
3824
3985
|
return ctx.redirect(acceptPageUrl.toString());
|
|
3825
3986
|
}
|
|
3826
|
-
const
|
|
3987
|
+
const userPayload = {
|
|
3827
3988
|
email: invitationEmail,
|
|
3828
3989
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
3829
3990
|
emailVerified: true,
|
|
3830
3991
|
createdAt: /* @__PURE__ */ new Date(),
|
|
3831
3992
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3832
|
-
}
|
|
3993
|
+
};
|
|
3994
|
+
const adapter = ctx.context.internalAdapter;
|
|
3995
|
+
const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
|
|
3833
3996
|
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
3834
3997
|
method: "POST",
|
|
3835
3998
|
body: {
|
|
@@ -3838,12 +4001,12 @@ const acceptInvitation = (options) => {
|
|
|
3838
4001
|
}
|
|
3839
4002
|
});
|
|
3840
4003
|
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
3841
|
-
await setSessionCookie(ctx, {
|
|
4004
|
+
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
3842
4005
|
session: await ctx.context.internalAdapter.createSession(user.id),
|
|
3843
4006
|
user
|
|
3844
4007
|
});
|
|
3845
|
-
const redirectUrl = invitation.redirectUrl
|
|
3846
|
-
return ctx.redirect(redirectUrl
|
|
4008
|
+
const redirectUrl = resolveInvitationRedirect(ctx, invitation.redirectUrl);
|
|
4009
|
+
return ctx.redirect(redirectUrl);
|
|
3847
4010
|
});
|
|
3848
4011
|
};
|
|
3849
4012
|
/**
|
|
@@ -3861,7 +4024,6 @@ const completeInvitation = (options) => {
|
|
|
3861
4024
|
})
|
|
3862
4025
|
}, async (ctx) => {
|
|
3863
4026
|
const { token, password } = ctx.body;
|
|
3864
|
-
const fallbackRedirect = getFallbackRedirect(ctx);
|
|
3865
4027
|
const invitation = await verifyPendingInvitation(token, $api, ctx);
|
|
3866
4028
|
if (!ctx.context) throw new APIError("BAD_REQUEST", { message: "Context is required" });
|
|
3867
4029
|
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
@@ -3875,23 +4037,25 @@ const completeInvitation = (options) => {
|
|
|
3875
4037
|
}
|
|
3876
4038
|
});
|
|
3877
4039
|
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
3878
|
-
await setSessionCookie(ctx, {
|
|
4040
|
+
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
3879
4041
|
session: await ctx.context.internalAdapter.createSession(existingUser.id),
|
|
3880
4042
|
user: existingUser
|
|
3881
4043
|
});
|
|
3882
4044
|
return {
|
|
3883
4045
|
success: true,
|
|
3884
|
-
redirectUrl: invitation.redirectUrl
|
|
4046
|
+
redirectUrl: resolveInvitationRedirect(ctx, invitation.redirectUrl)
|
|
3885
4047
|
};
|
|
3886
4048
|
}
|
|
3887
4049
|
if (!password) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
|
|
3888
|
-
const
|
|
4050
|
+
const userPayload = {
|
|
3889
4051
|
email: invitationEmail,
|
|
3890
4052
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
3891
4053
|
emailVerified: true,
|
|
3892
4054
|
createdAt: /* @__PURE__ */ new Date(),
|
|
3893
4055
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3894
|
-
}
|
|
4056
|
+
};
|
|
4057
|
+
const adapter = ctx.context.internalAdapter;
|
|
4058
|
+
const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
|
|
3895
4059
|
await ctx.context.internalAdapter.createAccount({
|
|
3896
4060
|
userId: user.id,
|
|
3897
4061
|
providerId: "credential",
|
|
@@ -3906,13 +4070,13 @@ const completeInvitation = (options) => {
|
|
|
3906
4070
|
}
|
|
3907
4071
|
});
|
|
3908
4072
|
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
3909
|
-
await setSessionCookie(ctx, {
|
|
4073
|
+
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
3910
4074
|
session: await ctx.context.internalAdapter.createSession(user.id),
|
|
3911
4075
|
user
|
|
3912
4076
|
});
|
|
3913
4077
|
return {
|
|
3914
4078
|
success: true,
|
|
3915
|
-
redirectUrl: invitation.redirectUrl
|
|
4079
|
+
redirectUrl: resolveInvitationRedirect(ctx, invitation.redirectUrl)
|
|
3916
4080
|
};
|
|
3917
4081
|
});
|
|
3918
4082
|
};
|
|
@@ -3937,7 +4101,7 @@ const completeInvitationSocial = (options) => {
|
|
|
3937
4101
|
}
|
|
3938
4102
|
});
|
|
3939
4103
|
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
3940
|
-
return ctx.redirect((invitation.redirectUrl
|
|
4104
|
+
return ctx.redirect(resolveInvitationRedirect(ctx, invitation.redirectUrl));
|
|
3941
4105
|
});
|
|
3942
4106
|
};
|
|
3943
4107
|
/**
|
|
@@ -4119,11 +4283,15 @@ const cancelInvitation = (options) => {
|
|
|
4119
4283
|
}, async (ctx) => {
|
|
4120
4284
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
4121
4285
|
const { invitationId, organizationId } = ctx.context.payload;
|
|
4286
|
+
if (ctx.body.invitationId !== invitationId) throw ctx.error("BAD_REQUEST", { message: "Invitation ID mismatch" });
|
|
4122
4287
|
const invitation = await ctx.context.adapter.findOne({
|
|
4123
4288
|
model: "invitation",
|
|
4124
4289
|
where: [{
|
|
4125
4290
|
field: "id",
|
|
4126
4291
|
value: invitationId
|
|
4292
|
+
}, {
|
|
4293
|
+
field: "organizationId",
|
|
4294
|
+
value: organizationId
|
|
4127
4295
|
}]
|
|
4128
4296
|
});
|
|
4129
4297
|
if (!invitation) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
|
|
@@ -4152,6 +4320,9 @@ const cancelInvitation = (options) => {
|
|
|
4152
4320
|
where: [{
|
|
4153
4321
|
field: "id",
|
|
4154
4322
|
value: invitationId
|
|
4323
|
+
}, {
|
|
4324
|
+
field: "organizationId",
|
|
4325
|
+
value: organizationId
|
|
4155
4326
|
}],
|
|
4156
4327
|
update: { status: "canceled" }
|
|
4157
4328
|
});
|
|
@@ -4177,11 +4348,15 @@ const resendInvitation = (options) => {
|
|
|
4177
4348
|
}, async (ctx) => {
|
|
4178
4349
|
const organizationPlugin = requireOrganizationPlugin(ctx);
|
|
4179
4350
|
const { invitationId, organizationId } = ctx.context.payload;
|
|
4351
|
+
if (ctx.body.invitationId !== invitationId) throw ctx.error("BAD_REQUEST", { message: "Invitation ID mismatch" });
|
|
4180
4352
|
const invitation = await ctx.context.adapter.findOne({
|
|
4181
4353
|
model: "invitation",
|
|
4182
4354
|
where: [{
|
|
4183
4355
|
field: "id",
|
|
4184
4356
|
value: invitationId
|
|
4357
|
+
}, {
|
|
4358
|
+
field: "organizationId",
|
|
4359
|
+
value: organizationId
|
|
4185
4360
|
}]
|
|
4186
4361
|
});
|
|
4187
4362
|
if (!invitation) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
|
|
@@ -4581,6 +4756,8 @@ async function withConcurrency(items, fn, options) {
|
|
|
4581
4756
|
if (executing.length > 0) await run();
|
|
4582
4757
|
return results;
|
|
4583
4758
|
}
|
|
4759
|
+
/** Zod schema for validating organization/project slug format. */
|
|
4760
|
+
const slugSchema = z.string().min(1, "Slug is required").regex(/^[a-z0-9-]+$/, "Slug can only contain lowercase letters, numbers, and hyphens");
|
|
4584
4761
|
//#endregion
|
|
4585
4762
|
//#region src/routes/organizations/schemas.ts
|
|
4586
4763
|
const DENIED_ORG_WRITE_KEYS = new Set([
|
|
@@ -4589,13 +4766,13 @@ const DENIED_ORG_WRITE_KEYS = new Set([
|
|
|
4589
4766
|
"updatedAt"
|
|
4590
4767
|
]);
|
|
4591
4768
|
const CREATE_NON_PERSISTED_KEYS = new Set(["defaultTeamName"]);
|
|
4592
|
-
const CREATE_REQUEST_ONLY_KEYS = new Set(["userId", "skipDefaultTeam"]);
|
|
4593
|
-
const CORE_CREATE_KEYS = [
|
|
4769
|
+
const CREATE_REQUEST_ONLY_KEYS$1 = new Set(["userId", "skipDefaultTeam"]);
|
|
4770
|
+
const CORE_CREATE_KEYS$1 = [
|
|
4594
4771
|
"name",
|
|
4595
4772
|
"slug",
|
|
4596
4773
|
"logo"
|
|
4597
4774
|
];
|
|
4598
|
-
const CORE_UPDATE_KEYS = [
|
|
4775
|
+
const CORE_UPDATE_KEYS$1 = [
|
|
4599
4776
|
"name",
|
|
4600
4777
|
"slug",
|
|
4601
4778
|
"logo",
|
|
@@ -4611,7 +4788,7 @@ function getWritableOrganizationAdditionalFieldNames(orgOptions) {
|
|
|
4611
4788
|
}).map(([name]) => name);
|
|
4612
4789
|
}
|
|
4613
4790
|
function getWritableOrganizationFieldNames(orgOptions, mode) {
|
|
4614
|
-
const keys = new Set(mode === "create" ? CORE_CREATE_KEYS : CORE_UPDATE_KEYS);
|
|
4791
|
+
const keys = new Set(mode === "create" ? CORE_CREATE_KEYS$1 : CORE_UPDATE_KEYS$1);
|
|
4615
4792
|
for (const name of getWritableOrganizationAdditionalFieldNames(orgOptions)) keys.add(name);
|
|
4616
4793
|
return keys;
|
|
4617
4794
|
}
|
|
@@ -4624,7 +4801,7 @@ function pickWritableOrganizationFields(body, orgOptions, mode) {
|
|
|
4624
4801
|
const skip = new Set([
|
|
4625
4802
|
...DENIED_ORG_WRITE_KEYS,
|
|
4626
4803
|
...mode === "create" ? CREATE_NON_PERSISTED_KEYS : [],
|
|
4627
|
-
...mode === "create" ? CREATE_REQUEST_ONLY_KEYS : []
|
|
4804
|
+
...mode === "create" ? CREATE_REQUEST_ONLY_KEYS$1 : []
|
|
4628
4805
|
]);
|
|
4629
4806
|
const result = {};
|
|
4630
4807
|
for (const [key, value] of Object.entries(body)) {
|
|
@@ -4635,19 +4812,19 @@ function pickWritableOrganizationFields(body, orgOptions, mode) {
|
|
|
4635
4812
|
}
|
|
4636
4813
|
const BaseCreateOrgCoreBodySchema = z$1.object({
|
|
4637
4814
|
name: z$1.string(),
|
|
4638
|
-
slug:
|
|
4815
|
+
slug: slugSchema,
|
|
4639
4816
|
logo: z$1.string().optional(),
|
|
4640
4817
|
defaultTeamName: z$1.string().optional()
|
|
4641
4818
|
});
|
|
4642
4819
|
const BaseUpdateOrgCoreBodySchema = z$1.object({
|
|
4643
|
-
logo:
|
|
4820
|
+
logo: optionalSafeUrlSchema,
|
|
4644
4821
|
name: z$1.string().optional(),
|
|
4645
|
-
slug:
|
|
4822
|
+
slug: slugSchema.optional(),
|
|
4646
4823
|
metadata: z$1.string().optional()
|
|
4647
4824
|
});
|
|
4648
4825
|
const CreateOrganizationBodySchema = BaseCreateOrgCoreBodySchema.catchall(z$1.unknown());
|
|
4649
4826
|
const UpdateOrganizationBodySchema = BaseUpdateOrgCoreBodySchema.catchall(z$1.unknown());
|
|
4650
|
-
function createSchemaForDBField(field) {
|
|
4827
|
+
function createSchemaForDBField$1(field) {
|
|
4651
4828
|
switch (field.type) {
|
|
4652
4829
|
case "number": return field.required ? z$1.coerce.number() : z$1.coerce.number().optional();
|
|
4653
4830
|
case "boolean": return field.required ? z$1.coerce.boolean() : z$1.coerce.boolean().optional();
|
|
@@ -4662,13 +4839,13 @@ function createSchemaForDBField(field) {
|
|
|
4662
4839
|
function validateWritableCreateOrganizationFields(data, options) {
|
|
4663
4840
|
const shape = {
|
|
4664
4841
|
name: z$1.string().min(1),
|
|
4665
|
-
slug:
|
|
4842
|
+
slug: slugSchema,
|
|
4666
4843
|
logo: z$1.string().optional()
|
|
4667
4844
|
};
|
|
4668
4845
|
const additional = getOrganizationAdditionalFields(options);
|
|
4669
4846
|
for (const [name, field] of Object.entries(additional)) {
|
|
4670
4847
|
if (field.input === false) continue;
|
|
4671
|
-
shape[name] = createSchemaForDBField(field);
|
|
4848
|
+
shape[name] = createSchemaForDBField$1(field);
|
|
4672
4849
|
}
|
|
4673
4850
|
const result = z$1.object(shape).strict().safeParse(data);
|
|
4674
4851
|
if (!result.success) throw result.error;
|
|
@@ -4684,14 +4861,14 @@ function validateWritableOrganizationUpdateFields(data, orgOptions) {
|
|
|
4684
4861
|
}]);
|
|
4685
4862
|
const shape = {
|
|
4686
4863
|
name: z$1.string().min(1).optional(),
|
|
4687
|
-
slug:
|
|
4688
|
-
logo:
|
|
4864
|
+
slug: slugSchema.optional(),
|
|
4865
|
+
logo: optionalSafeUrlSchema,
|
|
4689
4866
|
metadata: z$1.string().optional()
|
|
4690
4867
|
};
|
|
4691
4868
|
const additional = getOrganizationAdditionalFields(orgOptions);
|
|
4692
4869
|
for (const [name, field] of Object.entries(additional)) {
|
|
4693
4870
|
if (field.input === false) continue;
|
|
4694
|
-
shape[name] = createSchemaForDBField(field);
|
|
4871
|
+
shape[name] = createSchemaForDBField$1(field);
|
|
4695
4872
|
}
|
|
4696
4873
|
const result = z$1.object(shape).partial().strict().safeParse(data);
|
|
4697
4874
|
if (!result.success) throw result.error;
|
|
@@ -5760,6 +5937,17 @@ const removeTeamMember = (options) => {
|
|
|
5760
5937
|
});
|
|
5761
5938
|
};
|
|
5762
5939
|
//#endregion
|
|
5940
|
+
//#region src/compat/internal-adapter.ts
|
|
5941
|
+
/** Deletes all sessions for a user across better-auth 1.4–1.6 internal adapter APIs. */
|
|
5942
|
+
async function deleteUserSessionsCompat(adapter, userId) {
|
|
5943
|
+
if (typeof adapter.deleteUserSessions === "function") {
|
|
5944
|
+
await adapter.deleteUserSessions(userId);
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
if (typeof adapter.deleteSessions !== "function") throw new Error("Internal adapter does not support deleting user sessions");
|
|
5948
|
+
await adapter.deleteSessions(userId);
|
|
5949
|
+
}
|
|
5950
|
+
//#endregion
|
|
5763
5951
|
//#region src/routes/sessions/index.ts
|
|
5764
5952
|
const revokeSession = (options) => createAuthEndpoint("/dash/sessions/revoke", {
|
|
5765
5953
|
method: "POST",
|
|
@@ -5797,7 +5985,7 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
|
|
|
5797
5985
|
}, async (ctx) => {
|
|
5798
5986
|
const { userId } = ctx.body;
|
|
5799
5987
|
if (!await ctx.context.internalAdapter.findUserById(userId)) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
5800
|
-
await ctx.context.internalAdapter
|
|
5988
|
+
await deleteUserSessionsCompat(ctx.context.internalAdapter, userId);
|
|
5801
5989
|
return ctx.json({ success: true });
|
|
5802
5990
|
});
|
|
5803
5991
|
const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
|
|
@@ -5806,7 +5994,7 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
|
|
|
5806
5994
|
}, async (ctx) => {
|
|
5807
5995
|
const { userIds } = ctx.context.payload;
|
|
5808
5996
|
await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
|
|
5809
|
-
for (const userId of chunk) await ctx.context.internalAdapter
|
|
5997
|
+
for (const userId of chunk) await deleteUserSessionsCompat(ctx.context.internalAdapter, userId);
|
|
5810
5998
|
}, { concurrency: 3 });
|
|
5811
5999
|
return ctx.json({
|
|
5812
6000
|
success: true,
|
|
@@ -5814,248 +6002,6 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
|
|
|
5814
6002
|
});
|
|
5815
6003
|
});
|
|
5816
6004
|
//#endregion
|
|
5817
|
-
//#region ../utils/dist/ip.mjs
|
|
5818
|
-
/**
|
|
5819
|
-
* SSRF (Server-Side Request Forgery) Protection
|
|
5820
|
-
*
|
|
5821
|
-
* IP and URL utilities to validate hostnames and URLs before server-side fetches.
|
|
5822
|
-
* Blocks requests to private/reserved networks. Covers IPv4 private ranges,
|
|
5823
|
-
* IPv6 private ranges, and IPv4-mapped IPv6 bypass vectors.
|
|
5824
|
-
*/
|
|
5825
|
-
function isPrivateIPv4(a, b) {
|
|
5826
|
-
if (a === 10) return true;
|
|
5827
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
5828
|
-
if (a === 192 && b === 168) return true;
|
|
5829
|
-
if (a === 127) return true;
|
|
5830
|
-
if (a === 169 && b === 254) return true;
|
|
5831
|
-
if (a === 0) return true;
|
|
5832
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
5833
|
-
if (a >= 224) return true;
|
|
5834
|
-
return false;
|
|
5835
|
-
}
|
|
5836
|
-
function parseIPv4Quad(quad) {
|
|
5837
|
-
const v4parts = quad.split(".").map(Number);
|
|
5838
|
-
if (v4parts.length === 4 && v4parts.every((p) => !isNaN(p) && p >= 0 && p <= 255)) return [v4parts[0], v4parts[1]];
|
|
5839
|
-
return null;
|
|
5840
|
-
}
|
|
5841
|
-
/**
|
|
5842
|
-
* Loopback, RFC1918, link-local, CGNAT, ULA, documentation, NAT64 well-known,
|
|
5843
|
-
* IPv4-mapped private, unspecified, and multicast (RFC 4291 section 2.7).
|
|
5844
|
-
*/
|
|
5845
|
-
function ipv6GroupsAreNonPublic(groups) {
|
|
5846
|
-
if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) return true;
|
|
5847
|
-
if (groups.every((g) => g === 0)) return true;
|
|
5848
|
-
if ((groups[0] & 65472) === 65152) return true;
|
|
5849
|
-
if ((groups[0] & 65024) === 64512) return true;
|
|
5850
|
-
if ((groups[0] & 65280) === 65280) return true;
|
|
5851
|
-
if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
|
|
5852
|
-
if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 65535 && groups[5] === 0) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
|
|
5853
|
-
if (groups[0] === 100 && groups[1] === 65435 && groups.slice(2, 6).every((g) => g === 0)) return isPrivateIPv4(groups[6] >> 8 & 255, groups[6] & 255);
|
|
5854
|
-
if (groups[0] === 8193 && groups[1] === 3512) return true;
|
|
5855
|
-
if (groups[0] === 256 && groups.slice(1, 4).every((g) => g === 0)) return true;
|
|
5856
|
-
return false;
|
|
5857
|
-
}
|
|
5858
|
-
function parseIPv6(addr) {
|
|
5859
|
-
const sides = addr.split("::");
|
|
5860
|
-
if (sides.length > 2) return null;
|
|
5861
|
-
const parseGroups = (s) => s === "" ? [] : s.split(":").map((g) => parseInt(g, 16));
|
|
5862
|
-
const left = parseGroups(sides[0]);
|
|
5863
|
-
const right = sides.length === 2 ? parseGroups(sides[1]) : [];
|
|
5864
|
-
if (left.some(isNaN) || right.some(isNaN)) return null;
|
|
5865
|
-
const missing = 8 - left.length - right.length;
|
|
5866
|
-
if (sides.length === 2) {
|
|
5867
|
-
if (missing < 0) return null;
|
|
5868
|
-
return [
|
|
5869
|
-
...left,
|
|
5870
|
-
...Array(missing).fill(0),
|
|
5871
|
-
...right
|
|
5872
|
-
];
|
|
5873
|
-
}
|
|
5874
|
-
if (left.length !== 8) return null;
|
|
5875
|
-
return left;
|
|
5876
|
-
}
|
|
5877
|
-
/** True when the hostname is an IPv4 or IPv6 literal (not a DNS name). */
|
|
5878
|
-
function isIpLiteralHostname(hostname) {
|
|
5879
|
-
const bare = hostname.replace(/^\[|\]$/g, "");
|
|
5880
|
-
if (parseIPv4Quad(bare)) return true;
|
|
5881
|
-
return parseIPv6(bare)?.length === 8;
|
|
5882
|
-
}
|
|
5883
|
-
/** Returns true if the hostname resolves to a private/reserved IP or is a local hostname. */
|
|
5884
|
-
function isPrivateHost(hostname) {
|
|
5885
|
-
if (hostname === "localhost" || hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) return true;
|
|
5886
|
-
const bare = hostname.replace(/^\[|\]$/g, "");
|
|
5887
|
-
const v4 = parseIPv4Quad(bare);
|
|
5888
|
-
if (v4) return isPrivateIPv4(v4[0], v4[1]);
|
|
5889
|
-
const mappedTail = bare.match(/(?:^|:)([\d.]+)$/);
|
|
5890
|
-
if (mappedTail && mappedTail[1].includes(".")) {
|
|
5891
|
-
const mappedV4 = parseIPv4Quad(mappedTail[1]);
|
|
5892
|
-
if (mappedV4) return isPrivateIPv4(mappedV4[0], mappedV4[1]);
|
|
5893
|
-
}
|
|
5894
|
-
const groups = parseIPv6(bare);
|
|
5895
|
-
if (groups && groups.length === 8) return ipv6GroupsAreNonPublic(groups);
|
|
5896
|
-
return false;
|
|
5897
|
-
}
|
|
5898
|
-
//#endregion
|
|
5899
|
-
//#region ../utils/dist/url.mjs
|
|
5900
|
-
/**
|
|
5901
|
-
* URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
|
|
5902
|
-
*/
|
|
5903
|
-
const HTTP_PROTOCOLS = ["http:", "https:"];
|
|
5904
|
-
function isAllowedProtocol(protocol, allowedProtocols) {
|
|
5905
|
-
return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
|
|
5906
|
-
}
|
|
5907
|
-
function toAllowedOriginSet(allowedOrigins) {
|
|
5908
|
-
const set = /* @__PURE__ */ new Set();
|
|
5909
|
-
for (const raw of allowedOrigins) try {
|
|
5910
|
-
const parsed = new URL(raw.trim());
|
|
5911
|
-
if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
|
|
5912
|
-
} catch {}
|
|
5913
|
-
return set;
|
|
5914
|
-
}
|
|
5915
|
-
function isAllowedOrigin(origin, allowedOrigins) {
|
|
5916
|
-
if (!allowedOrigins.length) return false;
|
|
5917
|
-
return toAllowedOriginSet(allowedOrigins).has(origin);
|
|
5918
|
-
}
|
|
5919
|
-
async function resolveHostnameAddresses(hostname) {
|
|
5920
|
-
try {
|
|
5921
|
-
const dns = await import("node:dns").catch((e) => {
|
|
5922
|
-
console.warn("Failed to load node:dns for DNS resolution", e);
|
|
5923
|
-
return null;
|
|
5924
|
-
});
|
|
5925
|
-
if (!dns?.promises?.resolve) return null;
|
|
5926
|
-
return await dns.promises.resolve(hostname);
|
|
5927
|
-
} catch {
|
|
5928
|
-
return null;
|
|
5929
|
-
}
|
|
5930
|
-
}
|
|
5931
|
-
function validateUrlSync(input, options = {}) {
|
|
5932
|
-
let parsed;
|
|
5933
|
-
if (input instanceof URL) parsed = input;
|
|
5934
|
-
else {
|
|
5935
|
-
const trimmed = input.trim();
|
|
5936
|
-
try {
|
|
5937
|
-
parsed = new URL(trimmed);
|
|
5938
|
-
} catch {
|
|
5939
|
-
return {
|
|
5940
|
-
ok: false,
|
|
5941
|
-
reason: "invalid_url"
|
|
5942
|
-
};
|
|
5943
|
-
}
|
|
5944
|
-
}
|
|
5945
|
-
if (!isAllowedProtocol(parsed.protocol, options.allowedProtocols)) return {
|
|
5946
|
-
ok: false,
|
|
5947
|
-
reason: "disallowed_protocol",
|
|
5948
|
-
url: parsed,
|
|
5949
|
-
protocol: parsed.protocol
|
|
5950
|
-
};
|
|
5951
|
-
if (parsed.username !== "" || parsed.password !== "") return {
|
|
5952
|
-
ok: false,
|
|
5953
|
-
reason: "embedded_credentials",
|
|
5954
|
-
url: parsed
|
|
5955
|
-
};
|
|
5956
|
-
if (isPrivateHost(parsed.hostname)) return {
|
|
5957
|
-
ok: false,
|
|
5958
|
-
reason: "private_host",
|
|
5959
|
-
url: parsed
|
|
5960
|
-
};
|
|
5961
|
-
if (options.allowedOrigins?.length) {
|
|
5962
|
-
if (!isAllowedOrigin(parsed.origin, options.allowedOrigins)) return {
|
|
5963
|
-
ok: false,
|
|
5964
|
-
reason: "disallowed_origin",
|
|
5965
|
-
url: parsed,
|
|
5966
|
-
hostname: parsed.hostname,
|
|
5967
|
-
origin: parsed.origin
|
|
5968
|
-
};
|
|
5969
|
-
}
|
|
5970
|
-
return {
|
|
5971
|
-
ok: true,
|
|
5972
|
-
url: parsed
|
|
5973
|
-
};
|
|
5974
|
-
}
|
|
5975
|
-
/** SSRF checks with DNS resolution when available. */
|
|
5976
|
-
async function validateUrl(input, options = {}) {
|
|
5977
|
-
const sync = validateUrlSync(input, options);
|
|
5978
|
-
if (!sync.ok) return sync;
|
|
5979
|
-
if (options.dns === false) return sync;
|
|
5980
|
-
const hostname = sync.url.hostname;
|
|
5981
|
-
const addresses = await resolveHostnameAddresses(hostname);
|
|
5982
|
-
if (!isIpLiteralHostname(hostname) && !addresses?.length) return {
|
|
5983
|
-
ok: false,
|
|
5984
|
-
reason: "unresolved_host",
|
|
5985
|
-
url: sync.url,
|
|
5986
|
-
hostname
|
|
5987
|
-
};
|
|
5988
|
-
if (addresses) {
|
|
5989
|
-
for (const addr of addresses) if (isPrivateHost(addr)) return {
|
|
5990
|
-
ok: false,
|
|
5991
|
-
reason: "private_dns",
|
|
5992
|
-
url: sync.url,
|
|
5993
|
-
hostname,
|
|
5994
|
-
address: addr
|
|
5995
|
-
};
|
|
5996
|
-
}
|
|
5997
|
-
return {
|
|
5998
|
-
ok: true,
|
|
5999
|
-
url: sync.url,
|
|
6000
|
-
...addresses ? { addresses } : {}
|
|
6001
|
-
};
|
|
6002
|
-
}
|
|
6003
|
-
function safeResolveUrl(raw, baseURL) {
|
|
6004
|
-
const trim = raw.trim();
|
|
6005
|
-
if (!trim) return null;
|
|
6006
|
-
if (trim.startsWith("//") || trim.includes("\\")) return null;
|
|
6007
|
-
if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
|
|
6008
|
-
try {
|
|
6009
|
-
if (trim.startsWith("/")) return new URL(trim, baseURL);
|
|
6010
|
-
return new URL(trim);
|
|
6011
|
-
} catch {
|
|
6012
|
-
return null;
|
|
6013
|
-
}
|
|
6014
|
-
}
|
|
6015
|
-
//#endregion
|
|
6016
|
-
//#region src/validation/ssrf.ts
|
|
6017
|
-
const REDIRECT_STATUSES = new Set([
|
|
6018
|
-
301,
|
|
6019
|
-
302,
|
|
6020
|
-
303,
|
|
6021
|
-
307,
|
|
6022
|
-
308
|
|
6023
|
-
]);
|
|
6024
|
-
const MAX_REDIRECTS = 10;
|
|
6025
|
-
var SsrfBlockedError = class extends Error {
|
|
6026
|
-
name = "SsrfBlockedError";
|
|
6027
|
-
};
|
|
6028
|
-
function isSsrfBlockedError(error) {
|
|
6029
|
-
return error instanceof SsrfBlockedError;
|
|
6030
|
-
}
|
|
6031
|
-
/**
|
|
6032
|
-
* Fetch with SSRF checks on the initial URL and every redirect target.
|
|
6033
|
-
*/
|
|
6034
|
-
async function $outbound(url, options = {}) {
|
|
6035
|
-
const { timeout, signal: callerSignal, ...fetchInit } = options;
|
|
6036
|
-
let currentUrl = url;
|
|
6037
|
-
for (let hop = 0;; hop++) {
|
|
6038
|
-
const result = await validateUrl(currentUrl);
|
|
6039
|
-
if (!result.ok) throw new SsrfBlockedError("Invalid or blocked URL");
|
|
6040
|
-
const validated = result.url;
|
|
6041
|
-
const signals = [];
|
|
6042
|
-
if (callerSignal) signals.push(callerSignal);
|
|
6043
|
-
if (timeout !== void 0 && timeout > 0) signals.push(AbortSignal.timeout(timeout));
|
|
6044
|
-
const response = await fetch(validated.href, {
|
|
6045
|
-
...fetchInit,
|
|
6046
|
-
redirect: "manual",
|
|
6047
|
-
signal: signals.length === 0 ? void 0 : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
|
|
6048
|
-
});
|
|
6049
|
-
if (!REDIRECT_STATUSES.has(response.status)) return response;
|
|
6050
|
-
if (hop >= MAX_REDIRECTS) throw new SsrfBlockedError("Too many redirects");
|
|
6051
|
-
await response.body?.cancel().catch(() => {});
|
|
6052
|
-
const location = response.headers.get("location");
|
|
6053
|
-
const next = location ? safeResolveUrl(location, validated.href) : null;
|
|
6054
|
-
if (!next) throw new SsrfBlockedError("Invalid redirect location");
|
|
6055
|
-
currentUrl = next;
|
|
6056
|
-
}
|
|
6057
|
-
}
|
|
6058
|
-
//#endregion
|
|
6059
6005
|
//#region src/routes/plugin-session.ts
|
|
6060
6006
|
/**
|
|
6061
6007
|
* Builds an in-memory session context for calling plugin endpoints
|
|
@@ -6115,6 +6061,16 @@ function extractAlgorithmsFromSAMLMetadata(metadataXml) {
|
|
|
6115
6061
|
digestAlgorithms
|
|
6116
6062
|
};
|
|
6117
6063
|
}
|
|
6064
|
+
/** Extract the first IdP SSO URL from SAML metadata XML, if present. */
|
|
6065
|
+
function extractEntryPointFromSAMLMetadata(metadataXml) {
|
|
6066
|
+
const match = metadataXml.match(/<(?:[\w-]+:)?SingleSignOnService\b[^>]*\bLocation=["']([^"']+)["']/i);
|
|
6067
|
+
if (!match?.[1]) return void 0;
|
|
6068
|
+
try {
|
|
6069
|
+
return new URL(match[1]).toString();
|
|
6070
|
+
} catch {
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6118
6074
|
function validateSAMLMetadataAlgorithms(metadataXml) {
|
|
6119
6075
|
const warnings = [];
|
|
6120
6076
|
const { signatureAlgorithms, digestAlgorithms } = extractAlgorithmsFromSAMLMetadata(metadataXml);
|
|
@@ -6124,10 +6080,6 @@ function validateSAMLMetadataAlgorithms(metadataXml) {
|
|
|
6124
6080
|
}
|
|
6125
6081
|
//#endregion
|
|
6126
6082
|
//#region src/routes/sso/index.ts
|
|
6127
|
-
let ssoRuntimeModule;
|
|
6128
|
-
function loadSsoRuntime() {
|
|
6129
|
-
return ssoRuntimeModule ??= import("@better-auth/sso");
|
|
6130
|
-
}
|
|
6131
6083
|
function requireOrganizationAccess(ctx) {
|
|
6132
6084
|
const orgIdFromUrl = tryDecode(ctx.params.id);
|
|
6133
6085
|
const orgIdFromToken = ctx.context.payload?.organizationId;
|
|
@@ -6156,6 +6108,12 @@ const oidcConfigSchema = z$1.object({
|
|
|
6156
6108
|
clientSecret: z$1.string().optional(),
|
|
6157
6109
|
discoveryUrl: z$1.string().optional(),
|
|
6158
6110
|
issuer: z$1.string().optional(),
|
|
6111
|
+
discoveryEndpoint: z$1.string().optional(),
|
|
6112
|
+
authorizationEndpoint: z$1.string().optional(),
|
|
6113
|
+
tokenEndpoint: z$1.string().optional(),
|
|
6114
|
+
jwksEndpoint: z$1.string().optional(),
|
|
6115
|
+
userInfoEndpoint: z$1.string().optional(),
|
|
6116
|
+
tokenEndpointAuthentication: z$1.enum(["client_secret_post", "client_secret_basic"]).optional(),
|
|
6159
6117
|
mapping: z$1.object({
|
|
6160
6118
|
id: z$1.string().optional(),
|
|
6161
6119
|
email: z$1.string().optional(),
|
|
@@ -6166,21 +6124,8 @@ const oidcConfigSchema = z$1.object({
|
|
|
6166
6124
|
}).optional()
|
|
6167
6125
|
});
|
|
6168
6126
|
async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
6169
|
-
|
|
6170
|
-
if (
|
|
6171
|
-
const metadataResponse = await $outbound(samlConfig.idpMetadata.metadataUrl, {
|
|
6172
|
-
method: "GET",
|
|
6173
|
-
headers: { Accept: "application/xml, text/xml" },
|
|
6174
|
-
timeout: 15e3
|
|
6175
|
-
});
|
|
6176
|
-
if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
|
|
6177
|
-
idpMetadataXml = await metadataResponse.text();
|
|
6178
|
-
} catch (e) {
|
|
6179
|
-
if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
|
|
6180
|
-
if (e instanceof APIError$1) throw e;
|
|
6181
|
-
ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
|
|
6182
|
-
throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
|
|
6183
|
-
}
|
|
6127
|
+
const idpMetadataXml = samlConfig.idpMetadata?.metadata;
|
|
6128
|
+
if (samlConfig.idpMetadata?.metadataUrl && !idpMetadataXml) throw ctx.error("BAD_REQUEST", { message: "IdP metadata URL must be resolved before submitting; provide idpMetadata.metadata" });
|
|
6184
6129
|
const metadataAlgorithmWarnings = [];
|
|
6185
6130
|
if (idpMetadataXml) {
|
|
6186
6131
|
try {
|
|
@@ -6203,14 +6148,22 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
6203
6148
|
...samlConfig.cert ? { cert: samlConfig.cert } : {}
|
|
6204
6149
|
};
|
|
6205
6150
|
const m = samlConfig.mapping;
|
|
6151
|
+
const resolvedEntryPoint = samlConfig.entryPoint?.trim() || (idpMetadataXml ? extractEntryPointFromSAMLMetadata(idpMetadataXml) : void 0);
|
|
6152
|
+
const saml17OrNewer = isVersionAtLeast(ctx.context.version, "1.7.0");
|
|
6153
|
+
if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
|
|
6206
6154
|
return {
|
|
6207
6155
|
config: {
|
|
6208
6156
|
issuer: samlConfig.entityId ?? `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`,
|
|
6209
|
-
callbackUrl: `${baseURL}/sso/saml2/sp/acs/${providerId}`,
|
|
6210
6157
|
idpMetadata,
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6158
|
+
...saml17OrNewer ? {
|
|
6159
|
+
entryPoint: resolvedEntryPoint,
|
|
6160
|
+
...samlConfig.cert ? { cert: samlConfig.cert } : {}
|
|
6161
|
+
} : {
|
|
6162
|
+
callbackUrl: `${baseURL}/sso/saml2/sp/acs/${providerId}`,
|
|
6163
|
+
spMetadata: {},
|
|
6164
|
+
entryPoint: resolvedEntryPoint ?? "",
|
|
6165
|
+
cert: samlConfig.cert ?? ""
|
|
6166
|
+
},
|
|
6214
6167
|
...m ? { mapping: {
|
|
6215
6168
|
id: m.id ?? "nameID",
|
|
6216
6169
|
email: m.email ?? "email",
|
|
@@ -6224,67 +6177,33 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
6224
6177
|
...metadataAlgorithmWarnings.length > 0 ? { warnings: metadataAlgorithmWarnings } : {}
|
|
6225
6178
|
};
|
|
6226
6179
|
}
|
|
6227
|
-
async function resolveOIDCConfig(oidcConfig,
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
clientSecret: oidcConfig.clientSecret,
|
|
6255
|
-
issuer: hydratedConfig.issuer,
|
|
6256
|
-
discoveryEndpoint: hydratedConfig.discoveryEndpoint,
|
|
6257
|
-
authorizationEndpoint: hydratedConfig.authorizationEndpoint,
|
|
6258
|
-
tokenEndpoint: hydratedConfig.tokenEndpoint,
|
|
6259
|
-
jwksEndpoint: hydratedConfig.jwksEndpoint,
|
|
6260
|
-
userInfoEndpoint: hydratedConfig.userInfoEndpoint,
|
|
6261
|
-
tokenEndpointAuthentication: hydratedConfig.tokenEndpointAuthentication,
|
|
6262
|
-
pkce: true,
|
|
6263
|
-
...om ? { mapping: {
|
|
6264
|
-
id: om.id ?? "sub",
|
|
6265
|
-
email: om.email ?? "email",
|
|
6266
|
-
name: om.name ?? "name",
|
|
6267
|
-
emailVerified: om.emailVerified,
|
|
6268
|
-
image: om.image,
|
|
6269
|
-
extraFields: om.extraFields
|
|
6270
|
-
} } : {}
|
|
6271
|
-
},
|
|
6272
|
-
issuer: hydratedConfig.issuer
|
|
6273
|
-
};
|
|
6274
|
-
} catch (e) {
|
|
6275
|
-
if (e instanceof sso.DiscoveryError) {
|
|
6276
|
-
ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
|
|
6277
|
-
throw ctx.error("BAD_REQUEST", {
|
|
6278
|
-
message: `OIDC discovery failed: ${e.message}`,
|
|
6279
|
-
code: e.code
|
|
6280
|
-
});
|
|
6281
|
-
}
|
|
6282
|
-
ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
|
|
6283
|
-
throw ctx.error("BAD_REQUEST", {
|
|
6284
|
-
message: `OIDC discovery failed: Unable to discover configuration from ${issuer}`,
|
|
6285
|
-
code: "OIDC_DISCOVERY_FAILED"
|
|
6286
|
-
});
|
|
6287
|
-
}
|
|
6180
|
+
async function resolveOIDCConfig(oidcConfig, _domain, ctx) {
|
|
6181
|
+
if (!oidcConfig.issuer || !oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint) throw ctx.error("BAD_REQUEST", { message: "OIDC discovery must be resolved before submitting; provide issuer, authorizationEndpoint, tokenEndpoint, and jwksEndpoint" });
|
|
6182
|
+
const om = oidcConfig.mapping;
|
|
6183
|
+
const discoveryEndpoint = oidcConfig.discoveryEndpoint ?? oidcConfig.discoveryUrl ?? oidcConfig.issuer;
|
|
6184
|
+
return {
|
|
6185
|
+
config: {
|
|
6186
|
+
clientId: oidcConfig.clientId,
|
|
6187
|
+
clientSecret: oidcConfig.clientSecret,
|
|
6188
|
+
issuer: oidcConfig.issuer,
|
|
6189
|
+
discoveryEndpoint,
|
|
6190
|
+
authorizationEndpoint: oidcConfig.authorizationEndpoint,
|
|
6191
|
+
tokenEndpoint: oidcConfig.tokenEndpoint,
|
|
6192
|
+
jwksEndpoint: oidcConfig.jwksEndpoint,
|
|
6193
|
+
userInfoEndpoint: oidcConfig.userInfoEndpoint,
|
|
6194
|
+
tokenEndpointAuthentication: oidcConfig.tokenEndpointAuthentication,
|
|
6195
|
+
pkce: true,
|
|
6196
|
+
...om ? { mapping: {
|
|
6197
|
+
id: om.id ?? "sub",
|
|
6198
|
+
email: om.email ?? "email",
|
|
6199
|
+
name: om.name ?? "name",
|
|
6200
|
+
emailVerified: om.emailVerified,
|
|
6201
|
+
image: om.image,
|
|
6202
|
+
extraFields: om.extraFields
|
|
6203
|
+
} } : {}
|
|
6204
|
+
},
|
|
6205
|
+
issuer: oidcConfig.issuer
|
|
6206
|
+
};
|
|
6288
6207
|
}
|
|
6289
6208
|
const listOrganizationSsoProviders = (options) => {
|
|
6290
6209
|
return createAuthEndpoint("/dash/organization/:id/sso-providers", {
|
|
@@ -6442,6 +6361,7 @@ const updateSsoProvider = (options) => {
|
|
|
6442
6361
|
try {
|
|
6443
6362
|
const result = await ssoPlugin.endpoints.updateSSOProvider({
|
|
6444
6363
|
body: updateBody,
|
|
6364
|
+
params: { providerId },
|
|
6445
6365
|
context: {
|
|
6446
6366
|
...ctx.context,
|
|
6447
6367
|
...buildSessionContext(providerUserId)
|
|
@@ -6589,6 +6509,7 @@ const deleteSsoProvider = (options) => {
|
|
|
6589
6509
|
try {
|
|
6590
6510
|
await ssoPlugin.endpoints.deleteSSOProvider({
|
|
6591
6511
|
body: { providerId },
|
|
6512
|
+
params: { providerId },
|
|
6592
6513
|
context: {
|
|
6593
6514
|
...ctx.context,
|
|
6594
6515
|
...buildSessionContext(provider.userId)
|
|
@@ -6646,6 +6567,64 @@ const markSsoProviderDomainVerified = (options) => {
|
|
|
6646
6567
|
});
|
|
6647
6568
|
};
|
|
6648
6569
|
//#endregion
|
|
6570
|
+
//#region src/routes/two-factor/backup-codes.ts
|
|
6571
|
+
/** Mirrors better-auth/plugins/two-factor/backup-codes generateBackupCodesFn. */
|
|
6572
|
+
function generateBackupCodesPlain(options) {
|
|
6573
|
+
if (options?.customBackupCodesGenerate) return options.customBackupCodesGenerate();
|
|
6574
|
+
const amount = options?.amount ?? 10;
|
|
6575
|
+
const length = options?.length ?? 10;
|
|
6576
|
+
return Array.from({ length: amount }, () => {
|
|
6577
|
+
const code = generateRandomString(length, "a-z", "0-9", "A-Z");
|
|
6578
|
+
return `${code.slice(0, 5)}-${code.slice(5)}`;
|
|
6579
|
+
});
|
|
6580
|
+
}
|
|
6581
|
+
/** Mirrors better-auth/plugins/two-factor/backup-codes generateBackupCodes. */
|
|
6582
|
+
async function generateBackupCodes$1(secret, options) {
|
|
6583
|
+
const backupCodes = generateBackupCodesPlain(options);
|
|
6584
|
+
const json = JSON.stringify(backupCodes);
|
|
6585
|
+
const storeBackupCodes = options?.storeBackupCodes ?? "encrypted";
|
|
6586
|
+
if (storeBackupCodes === "encrypted") return {
|
|
6587
|
+
backupCodes,
|
|
6588
|
+
encryptedBackupCodes: await symmetricEncrypt({
|
|
6589
|
+
key: secret,
|
|
6590
|
+
data: json
|
|
6591
|
+
})
|
|
6592
|
+
};
|
|
6593
|
+
if (typeof storeBackupCodes === "object" && "encrypt" in storeBackupCodes) return {
|
|
6594
|
+
backupCodes,
|
|
6595
|
+
encryptedBackupCodes: await storeBackupCodes.encrypt(json)
|
|
6596
|
+
};
|
|
6597
|
+
return {
|
|
6598
|
+
backupCodes,
|
|
6599
|
+
encryptedBackupCodes: json
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
function resolveBackupCodeOptions(twoFactorPlugin) {
|
|
6603
|
+
return {
|
|
6604
|
+
storeBackupCodes: "encrypted",
|
|
6605
|
+
...twoFactorPlugin?.options?.backupCodeOptions
|
|
6606
|
+
};
|
|
6607
|
+
}
|
|
6608
|
+
//#endregion
|
|
6609
|
+
//#region src/routes/two-factor/two-factor-options.ts
|
|
6610
|
+
function resolveTotpIssuer(twoFactorPlugin, appName) {
|
|
6611
|
+
const options = twoFactorPlugin?.options;
|
|
6612
|
+
return (options?.totpOptions)?.issuer ?? options?.issuer ?? appName ?? "BetterAuth";
|
|
6613
|
+
}
|
|
6614
|
+
function resolveTotpConfig(twoFactorPlugin) {
|
|
6615
|
+
const totpOptions = twoFactorPlugin?.options?.totpOptions;
|
|
6616
|
+
return {
|
|
6617
|
+
digits: totpOptions?.digits ?? 6,
|
|
6618
|
+
period: totpOptions?.period ?? 30
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
function buildTotpUri(params) {
|
|
6622
|
+
return createOTP(params.secret, {
|
|
6623
|
+
digits: params.digits,
|
|
6624
|
+
period: params.period
|
|
6625
|
+
}).url(params.issuer, params.account);
|
|
6626
|
+
}
|
|
6627
|
+
//#endregion
|
|
6649
6628
|
//#region src/routes/two-factor/index.ts
|
|
6650
6629
|
const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor", {
|
|
6651
6630
|
method: "POST",
|
|
@@ -6654,31 +6633,37 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
|
|
|
6654
6633
|
const { userId } = ctx.context.payload;
|
|
6655
6634
|
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6656
6635
|
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6636
|
+
if (twoFactorPlugin.options?.totpOptions?.disable === true) throw new APIError("BAD_REQUEST", {
|
|
6637
|
+
message: "TOTP is not configured",
|
|
6638
|
+
code: "TOTP_NOT_CONFIGURED"
|
|
6639
|
+
});
|
|
6640
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6641
|
+
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6642
|
+
if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
6643
|
+
if (user.twoFactorEnabled === true) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
|
|
6657
6644
|
if (await ctx.context.adapter.findOne({
|
|
6658
|
-
model:
|
|
6645
|
+
model: twoFactorTable,
|
|
6646
|
+
where: [{
|
|
6647
|
+
field: "userId",
|
|
6648
|
+
value: userId
|
|
6649
|
+
}]
|
|
6650
|
+
})) await ctx.context.adapter.delete({
|
|
6651
|
+
model: twoFactorTable,
|
|
6659
6652
|
where: [{
|
|
6660
6653
|
field: "userId",
|
|
6661
6654
|
value: userId
|
|
6662
6655
|
}]
|
|
6663
|
-
})
|
|
6656
|
+
});
|
|
6664
6657
|
const { generateRandomString, symmetricEncrypt } = await import("better-auth/crypto");
|
|
6665
|
-
const secret = generateRandomString(32
|
|
6666
|
-
const
|
|
6667
|
-
const backupCodeLength = 10;
|
|
6668
|
-
const backupCodes = [];
|
|
6669
|
-
for (let i = 0; i < backupCodeAmount; i++) backupCodes.push(generateId(backupCodeLength).toUpperCase());
|
|
6658
|
+
const secret = generateRandomString(32);
|
|
6659
|
+
const { backupCodes, encryptedBackupCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
|
|
6670
6660
|
const encryptedSecret = await symmetricEncrypt({
|
|
6671
6661
|
key: ctx.context.secret,
|
|
6672
6662
|
data: secret
|
|
6673
6663
|
});
|
|
6674
|
-
const encryptedBackupCodes = await symmetricEncrypt({
|
|
6675
|
-
key: ctx.context.secret,
|
|
6676
|
-
data: JSON.stringify(backupCodes)
|
|
6677
|
-
});
|
|
6678
6664
|
await ctx.context.adapter.create({
|
|
6679
|
-
model:
|
|
6665
|
+
model: twoFactorTable,
|
|
6680
6666
|
data: {
|
|
6681
|
-
id: generateId(32),
|
|
6682
6667
|
userId,
|
|
6683
6668
|
secret: encryptedSecret,
|
|
6684
6669
|
backupCodes: encryptedBackupCodes
|
|
@@ -6688,15 +6673,47 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
|
|
|
6688
6673
|
twoFactorEnabled: true,
|
|
6689
6674
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6690
6675
|
});
|
|
6691
|
-
const
|
|
6692
|
-
const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
|
|
6676
|
+
const totpConfig = resolveTotpConfig(twoFactorPlugin);
|
|
6693
6677
|
return {
|
|
6694
6678
|
success: true,
|
|
6695
|
-
totpURI:
|
|
6679
|
+
totpURI: buildTotpUri({
|
|
6680
|
+
secret,
|
|
6681
|
+
issuer: resolveTotpIssuer(twoFactorPlugin, ctx.context.appName),
|
|
6682
|
+
account: user?.email || userId,
|
|
6683
|
+
digits: totpConfig.digits,
|
|
6684
|
+
period: totpConfig.period
|
|
6685
|
+
}),
|
|
6696
6686
|
secret,
|
|
6697
6687
|
backupCodes
|
|
6698
6688
|
};
|
|
6699
6689
|
});
|
|
6690
|
+
const completeTwoFactorSetup = (options) => createAuthEndpoint("/dash/complete-two-factor-setup", {
|
|
6691
|
+
method: "POST",
|
|
6692
|
+
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6693
|
+
}, async (ctx) => {
|
|
6694
|
+
const { userId } = ctx.context.payload;
|
|
6695
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6696
|
+
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6697
|
+
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6698
|
+
if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
6699
|
+
if (user.twoFactorEnabled === true) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
|
|
6700
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6701
|
+
if (!await ctx.context.adapter.findOne({
|
|
6702
|
+
model: twoFactorTable,
|
|
6703
|
+
where: [{
|
|
6704
|
+
field: "userId",
|
|
6705
|
+
value: userId
|
|
6706
|
+
}]
|
|
6707
|
+
})) throw new APIError("BAD_REQUEST", {
|
|
6708
|
+
message: "Two-factor authentication setup has not been started",
|
|
6709
|
+
code: "TWO_FACTOR_SETUP_NOT_PENDING"
|
|
6710
|
+
});
|
|
6711
|
+
await ctx.context.internalAdapter.updateUser(userId, {
|
|
6712
|
+
twoFactorEnabled: true,
|
|
6713
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6714
|
+
});
|
|
6715
|
+
return { success: true };
|
|
6716
|
+
});
|
|
6700
6717
|
const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-factor-totp-uri", {
|
|
6701
6718
|
method: "POST",
|
|
6702
6719
|
metadata: { scope: "http" },
|
|
@@ -6705,8 +6722,13 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
|
|
|
6705
6722
|
const { userId } = ctx.context.payload;
|
|
6706
6723
|
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6707
6724
|
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6725
|
+
if (twoFactorPlugin.options?.totpOptions?.disable === true) throw new APIError("BAD_REQUEST", {
|
|
6726
|
+
message: "TOTP is not configured",
|
|
6727
|
+
code: "TOTP_NOT_CONFIGURED"
|
|
6728
|
+
});
|
|
6729
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6708
6730
|
const twoFactorRecord = await ctx.context.adapter.findOne({
|
|
6709
|
-
model:
|
|
6731
|
+
model: twoFactorTable,
|
|
6710
6732
|
where: [{
|
|
6711
6733
|
field: "userId",
|
|
6712
6734
|
value: userId
|
|
@@ -6722,8 +6744,14 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
|
|
|
6722
6744
|
});
|
|
6723
6745
|
} catch {}
|
|
6724
6746
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
6725
|
-
const
|
|
6726
|
-
return { totpURI:
|
|
6747
|
+
const totpConfig = resolveTotpConfig(twoFactorPlugin);
|
|
6748
|
+
return { totpURI: buildTotpUri({
|
|
6749
|
+
secret,
|
|
6750
|
+
issuer: resolveTotpIssuer(twoFactorPlugin, ctx.context.appName),
|
|
6751
|
+
account: user?.email || userId,
|
|
6752
|
+
digits: totpConfig.digits,
|
|
6753
|
+
period: totpConfig.period
|
|
6754
|
+
}) };
|
|
6727
6755
|
});
|
|
6728
6756
|
const viewBackupCodes = (options) => createAuthEndpoint("/dash/view-backup-codes", {
|
|
6729
6757
|
method: "POST",
|
|
@@ -6736,9 +6764,11 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
|
|
|
6736
6764
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6737
6765
|
}, async (ctx) => {
|
|
6738
6766
|
const { userId } = ctx.context.payload;
|
|
6739
|
-
|
|
6767
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6768
|
+
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
|
|
6769
|
+
const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
|
|
6740
6770
|
await ctx.context.adapter.delete({
|
|
6741
|
-
model:
|
|
6771
|
+
model: twoFactorTable,
|
|
6742
6772
|
where: [{
|
|
6743
6773
|
field: "userId",
|
|
6744
6774
|
value: userId
|
|
@@ -6755,25 +6785,19 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
|
|
|
6755
6785
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6756
6786
|
}, async (ctx) => {
|
|
6757
6787
|
const { userId } = ctx.context.payload;
|
|
6788
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6789
|
+
const twoFactorTable = twoFactorPlugin?.options?.twoFactorTable ?? "twoFactor";
|
|
6758
6790
|
const twoFactorRecord = await ctx.context.adapter.findOne({
|
|
6759
|
-
model:
|
|
6791
|
+
model: twoFactorTable,
|
|
6760
6792
|
where: [{
|
|
6761
6793
|
field: "userId",
|
|
6762
6794
|
value: userId
|
|
6763
6795
|
}]
|
|
6764
6796
|
});
|
|
6765
6797
|
if (!twoFactorRecord) throw new APIError("NOT_FOUND", { message: "Two-factor authentication not set up for this user" });
|
|
6766
|
-
const
|
|
6767
|
-
const backupCodeLength = 10;
|
|
6768
|
-
const newBackupCodes = [];
|
|
6769
|
-
for (let i = 0; i < backupCodeAmount; i++) newBackupCodes.push(generateId(backupCodeLength).toUpperCase());
|
|
6770
|
-
const { symmetricEncrypt } = await import("better-auth/crypto");
|
|
6771
|
-
const encryptedCodes = await symmetricEncrypt({
|
|
6772
|
-
key: ctx.context.secret,
|
|
6773
|
-
data: JSON.stringify(newBackupCodes)
|
|
6774
|
-
});
|
|
6798
|
+
const { backupCodes: newBackupCodes, encryptedBackupCodes: encryptedCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
|
|
6775
6799
|
await ctx.context.adapter.update({
|
|
6776
|
-
model:
|
|
6800
|
+
model: twoFactorTable,
|
|
6777
6801
|
where: [{
|
|
6778
6802
|
field: "id",
|
|
6779
6803
|
value: twoFactorRecord.id
|
|
@@ -6783,6 +6807,196 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
|
|
|
6783
6807
|
return { backupCodes: newBackupCodes };
|
|
6784
6808
|
});
|
|
6785
6809
|
//#endregion
|
|
6810
|
+
//#region src/compat/sessions.ts
|
|
6811
|
+
/** Secondary-storage sessions on older better-auth builds may omit `id`; dash expects one. */
|
|
6812
|
+
function normalizeDashSessions(sessions) {
|
|
6813
|
+
return sessions.map((session) => {
|
|
6814
|
+
if (session.id) return session;
|
|
6815
|
+
if (session.token) return {
|
|
6816
|
+
...session,
|
|
6817
|
+
id: session.token
|
|
6818
|
+
};
|
|
6819
|
+
return session;
|
|
6820
|
+
});
|
|
6821
|
+
}
|
|
6822
|
+
//#endregion
|
|
6823
|
+
//#region src/lib/sanitize-sessions.ts
|
|
6824
|
+
/**
|
|
6825
|
+
* Strips bearer tokens from session records before returning them from dash handlers.
|
|
6826
|
+
*/
|
|
6827
|
+
function sanitizeSessions(sessions) {
|
|
6828
|
+
return sessions.map((session) => {
|
|
6829
|
+
const { token: _token, ...rest } = session;
|
|
6830
|
+
return rest;
|
|
6831
|
+
});
|
|
6832
|
+
}
|
|
6833
|
+
//#endregion
|
|
6834
|
+
//#region src/routes/two-factor/two-factor-status.ts
|
|
6835
|
+
async function resolveTwoFactorStatus(context, userId, user) {
|
|
6836
|
+
if (!hasPlugin(context, "two-factor")) return;
|
|
6837
|
+
if (user.twoFactorEnabled === true) return "enabled";
|
|
6838
|
+
const twoFactorTable = context.getPlugin("two-factor")?.options?.twoFactorTable ?? "twoFactor";
|
|
6839
|
+
return await context.adapter.findOne({
|
|
6840
|
+
model: twoFactorTable,
|
|
6841
|
+
where: [{
|
|
6842
|
+
field: "userId",
|
|
6843
|
+
value: userId
|
|
6844
|
+
}]
|
|
6845
|
+
}) ? "pending" : "disabled";
|
|
6846
|
+
}
|
|
6847
|
+
//#endregion
|
|
6848
|
+
//#region src/routes/users/schemas.ts
|
|
6849
|
+
const DENIED_USER_WRITE_KEYS = new Set([
|
|
6850
|
+
"id",
|
|
6851
|
+
"createdAt",
|
|
6852
|
+
"updatedAt"
|
|
6853
|
+
]);
|
|
6854
|
+
/** Fields that must be changed through dedicated permission-checked endpoints. */
|
|
6855
|
+
const DENIED_SENSITIVE_USER_KEYS = new Set([
|
|
6856
|
+
"banned",
|
|
6857
|
+
"banReason",
|
|
6858
|
+
"banExpires",
|
|
6859
|
+
"role",
|
|
6860
|
+
"password",
|
|
6861
|
+
"twoFactorEnabled",
|
|
6862
|
+
"twoFactorSecret"
|
|
6863
|
+
]);
|
|
6864
|
+
const CREATE_REQUEST_ONLY_KEYS = new Set([
|
|
6865
|
+
"password",
|
|
6866
|
+
"generatePassword",
|
|
6867
|
+
"sendVerificationEmail",
|
|
6868
|
+
"sendOrganizationInvite",
|
|
6869
|
+
"organizationRole",
|
|
6870
|
+
"organizationId"
|
|
6871
|
+
]);
|
|
6872
|
+
const CORE_CREATE_KEYS = [
|
|
6873
|
+
"name",
|
|
6874
|
+
"email",
|
|
6875
|
+
"image",
|
|
6876
|
+
"emailVerified"
|
|
6877
|
+
];
|
|
6878
|
+
const CORE_UPDATE_KEYS = [
|
|
6879
|
+
"name",
|
|
6880
|
+
"email",
|
|
6881
|
+
"image",
|
|
6882
|
+
"emailVerified"
|
|
6883
|
+
];
|
|
6884
|
+
/**
|
|
6885
|
+
* Mirrors Better Auth's user input schema merge:
|
|
6886
|
+
* `user.additionalFields` plus each plugin's `schema.user` fields.
|
|
6887
|
+
*/
|
|
6888
|
+
function getUserInputFields(options) {
|
|
6889
|
+
let schema = { ...options?.user?.additionalFields ?? {} };
|
|
6890
|
+
for (const plugin of options?.plugins ?? []) {
|
|
6891
|
+
const pluginUser = plugin.schema?.user;
|
|
6892
|
+
if (pluginUser?.fields) schema = {
|
|
6893
|
+
...schema,
|
|
6894
|
+
...pluginUser.fields
|
|
6895
|
+
};
|
|
6896
|
+
if (pluginUser?.additionalFields) schema = {
|
|
6897
|
+
...schema,
|
|
6898
|
+
...pluginUser.additionalFields
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6901
|
+
return schema;
|
|
6902
|
+
}
|
|
6903
|
+
function isWritableUserInputField(field) {
|
|
6904
|
+
return field.input !== false && !field.references;
|
|
6905
|
+
}
|
|
6906
|
+
function getWritableUserAdditionalFieldNames(options) {
|
|
6907
|
+
return Object.entries(getUserInputFields(options)).filter(([, field]) => isWritableUserInputField(field)).map(([name]) => name);
|
|
6908
|
+
}
|
|
6909
|
+
function getWritableUserFieldNames(options, mode) {
|
|
6910
|
+
const keys = new Set(mode === "create" ? CORE_CREATE_KEYS : CORE_UPDATE_KEYS);
|
|
6911
|
+
for (const name of getWritableUserAdditionalFieldNames(options)) keys.add(name);
|
|
6912
|
+
return keys;
|
|
6913
|
+
}
|
|
6914
|
+
/**
|
|
6915
|
+
* Returns only user fields that may be persisted on create/update.
|
|
6916
|
+
* Strips unknown keys, denied keys, request-only fields, and non-input/reference additional fields.
|
|
6917
|
+
*/
|
|
6918
|
+
function pickWritableUserFields(body, options, mode) {
|
|
6919
|
+
const allowed = getWritableUserFieldNames(options, mode);
|
|
6920
|
+
const skip = new Set([
|
|
6921
|
+
...DENIED_USER_WRITE_KEYS,
|
|
6922
|
+
...DENIED_SENSITIVE_USER_KEYS,
|
|
6923
|
+
...mode === "create" ? CREATE_REQUEST_ONLY_KEYS : []
|
|
6924
|
+
]);
|
|
6925
|
+
const result = {};
|
|
6926
|
+
for (const [key, value] of Object.entries(body)) {
|
|
6927
|
+
if (skip.has(key) || !allowed.has(key) || value === void 0) continue;
|
|
6928
|
+
result[key] = value;
|
|
6929
|
+
}
|
|
6930
|
+
return result;
|
|
6931
|
+
}
|
|
6932
|
+
const BaseCreateUserCoreBodySchema = z$1.object({
|
|
6933
|
+
name: z$1.string(),
|
|
6934
|
+
email: z$1.email(),
|
|
6935
|
+
image: z$1.string().optional(),
|
|
6936
|
+
password: z$1.string().optional(),
|
|
6937
|
+
generatePassword: z$1.boolean().optional(),
|
|
6938
|
+
emailVerified: z$1.boolean().optional(),
|
|
6939
|
+
sendVerificationEmail: z$1.boolean().optional(),
|
|
6940
|
+
sendOrganizationInvite: z$1.boolean().optional(),
|
|
6941
|
+
organizationRole: z$1.string().optional(),
|
|
6942
|
+
organizationId: z$1.string().optional()
|
|
6943
|
+
});
|
|
6944
|
+
const BaseUpdateUserCoreBodySchema = z$1.object({
|
|
6945
|
+
name: z$1.string().nullable().optional(),
|
|
6946
|
+
email: z$1.email().optional(),
|
|
6947
|
+
image: z$1.string().nullable().optional(),
|
|
6948
|
+
emailVerified: z$1.boolean().optional()
|
|
6949
|
+
});
|
|
6950
|
+
const CreateUserBodySchema = BaseCreateUserCoreBodySchema.catchall(z$1.unknown());
|
|
6951
|
+
const UpdateUserBodySchema = BaseUpdateUserCoreBodySchema.catchall(z$1.unknown());
|
|
6952
|
+
function createSchemaForDBField(field) {
|
|
6953
|
+
switch (field.type) {
|
|
6954
|
+
case "number": return field.required ? z$1.coerce.number() : z$1.coerce.number().optional();
|
|
6955
|
+
case "boolean": return field.required ? z$1.coerce.boolean() : z$1.coerce.boolean().optional();
|
|
6956
|
+
case "date": return field.required ? z$1.union([z$1.string().min(1), z$1.coerce.date()]) : z$1.union([z$1.string(), z$1.coerce.date()]).optional();
|
|
6957
|
+
default: return field.required ? z$1.string().min(1) : z$1.string().optional();
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
/**
|
|
6961
|
+
* Validates required core + configured additional fields on create.
|
|
6962
|
+
*/
|
|
6963
|
+
function validateWritableCreateUserFields(data, options) {
|
|
6964
|
+
const shape = {
|
|
6965
|
+
name: z$1.string().min(1),
|
|
6966
|
+
email: z$1.email(),
|
|
6967
|
+
image: z$1.string().optional(),
|
|
6968
|
+
emailVerified: z$1.boolean().optional()
|
|
6969
|
+
};
|
|
6970
|
+
for (const [name, field] of Object.entries(getUserInputFields(options))) {
|
|
6971
|
+
if (!isWritableUserInputField(field)) continue;
|
|
6972
|
+
shape[name] = createSchemaForDBField(field);
|
|
6973
|
+
}
|
|
6974
|
+
const result = z$1.object(shape).strict().safeParse(data);
|
|
6975
|
+
if (!result.success) throw result.error;
|
|
6976
|
+
}
|
|
6977
|
+
/**
|
|
6978
|
+
* Ensures at least one field is present on update and validates additional field types.
|
|
6979
|
+
*/
|
|
6980
|
+
function validateWritableUserUpdateFields(data, options) {
|
|
6981
|
+
if (Object.keys(data).length === 0) throw new z$1.ZodError([{
|
|
6982
|
+
code: "custom",
|
|
6983
|
+
message: "No valid fields to update",
|
|
6984
|
+
path: []
|
|
6985
|
+
}]);
|
|
6986
|
+
const shape = {
|
|
6987
|
+
name: z$1.string().nullable().optional(),
|
|
6988
|
+
email: z$1.email().optional(),
|
|
6989
|
+
image: z$1.string().nullable().optional(),
|
|
6990
|
+
emailVerified: z$1.boolean().optional()
|
|
6991
|
+
};
|
|
6992
|
+
for (const [name, field] of Object.entries(getUserInputFields(options))) {
|
|
6993
|
+
if (!isWritableUserInputField(field)) continue;
|
|
6994
|
+
shape[name] = createSchemaForDBField(field);
|
|
6995
|
+
}
|
|
6996
|
+
const result = z$1.object(shape).partial().strict().safeParse(data);
|
|
6997
|
+
if (!result.success) throw result.error;
|
|
6998
|
+
}
|
|
6999
|
+
//#endregion
|
|
6786
7000
|
//#region src/routes/users/index.ts
|
|
6787
7001
|
function parseWhereClause(val) {
|
|
6788
7002
|
if (!val) return [];
|
|
@@ -6946,7 +7160,7 @@ const impersonateUser = (options) => {
|
|
|
6946
7160
|
query: z$1.object({ impersonation_token: z$1.string() }),
|
|
6947
7161
|
use: [jwtMiddleware(options, z$1.object({
|
|
6948
7162
|
userId: z$1.string(),
|
|
6949
|
-
redirectUrl:
|
|
7163
|
+
redirectUrl: safeUrlSchema,
|
|
6950
7164
|
impersonatedBy: z$1.string().optional()
|
|
6951
7165
|
}), async (ctx) => {
|
|
6952
7166
|
return ctx.query.impersonation_token;
|
|
@@ -6974,25 +7188,21 @@ const createUser = (options) => {
|
|
|
6974
7188
|
organizationId: z$1.string().optional(),
|
|
6975
7189
|
organizationRole: z$1.string().optional()
|
|
6976
7190
|
}))],
|
|
6977
|
-
body:
|
|
6978
|
-
name: z$1.string(),
|
|
6979
|
-
email: z$1.email(),
|
|
6980
|
-
image: z$1.string().optional(),
|
|
6981
|
-
password: z$1.string().optional(),
|
|
6982
|
-
generatePassword: z$1.boolean().optional(),
|
|
6983
|
-
emailVerified: z$1.boolean().optional(),
|
|
6984
|
-
sendVerificationEmail: z$1.boolean().optional(),
|
|
6985
|
-
sendOrganizationInvite: z$1.boolean().optional(),
|
|
6986
|
-
organizationRole: z$1.string().optional(),
|
|
6987
|
-
organizationId: z$1.string().optional()
|
|
6988
|
-
})
|
|
7191
|
+
body: CreateUserBodySchema
|
|
6989
7192
|
}, async (ctx) => {
|
|
6990
|
-
const
|
|
7193
|
+
const body = ctx.body;
|
|
7194
|
+
const userData = pickWritableUserFields(body, ctx.context.options, "create");
|
|
7195
|
+
try {
|
|
7196
|
+
validateWritableCreateUserFields(userData, ctx.context.options);
|
|
7197
|
+
} catch (error) {
|
|
7198
|
+
if (error instanceof z$1.ZodError) throw new APIError("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid user data" });
|
|
7199
|
+
throw error;
|
|
7200
|
+
}
|
|
6991
7201
|
const email = normalizeEmail(userData.email, ctx.context);
|
|
6992
7202
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", { message: "User with this email already exist" });
|
|
6993
7203
|
let password = null;
|
|
6994
|
-
if (
|
|
6995
|
-
else if (
|
|
7204
|
+
if (body.generatePassword && !body.password) password = generateId(12);
|
|
7205
|
+
else if (body.password && typeof body.password === "string" && body.password.trim() !== "") password = body.password;
|
|
6996
7206
|
const userSchema = getAuthTables(ctx.context.options).user;
|
|
6997
7207
|
for (const field in userSchema.fields || {}) {
|
|
6998
7208
|
const value = userSchema.fields[field];
|
|
@@ -7009,24 +7219,28 @@ const createUser = (options) => {
|
|
|
7009
7219
|
}
|
|
7010
7220
|
}
|
|
7011
7221
|
}
|
|
7012
|
-
const
|
|
7222
|
+
const emailVerified = userData.emailVerified;
|
|
7223
|
+
const userPayload = {
|
|
7013
7224
|
...userData,
|
|
7225
|
+
name: userData.name,
|
|
7014
7226
|
email,
|
|
7015
|
-
emailVerified
|
|
7227
|
+
emailVerified,
|
|
7016
7228
|
createdAt: /* @__PURE__ */ new Date(),
|
|
7017
7229
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7018
|
-
}
|
|
7230
|
+
};
|
|
7231
|
+
const adapter = ctx.context.internalAdapter;
|
|
7232
|
+
const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
|
|
7019
7233
|
if (password) await ctx.context.internalAdapter.createAccount({
|
|
7020
7234
|
userId: user.id,
|
|
7021
7235
|
providerId: "credential",
|
|
7022
7236
|
accountId: user.id,
|
|
7023
7237
|
password: await ctx.context.password.hash(password)
|
|
7024
7238
|
});
|
|
7025
|
-
if (
|
|
7239
|
+
if (body.sendVerificationEmail && !emailVerified) {
|
|
7026
7240
|
if (ctx.context.options.emailVerification?.sendVerificationEmail) await sendVerificationEmailFn(ctx, user);
|
|
7027
7241
|
}
|
|
7028
|
-
const organizationId = ctx.context.payload?.organizationId ||
|
|
7029
|
-
const organizationRole = ctx.context.payload?.organizationRole ||
|
|
7242
|
+
const organizationId = ctx.context.payload?.organizationId || body.organizationId;
|
|
7243
|
+
const organizationRole = ctx.context.payload?.organizationRole || body.organizationRole;
|
|
7030
7244
|
if (organizationId) {
|
|
7031
7245
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
7032
7246
|
const role = organizationRole || "member";
|
|
@@ -7136,6 +7350,7 @@ const getUserDetails = (options) => {
|
|
|
7136
7350
|
} }
|
|
7137
7351
|
});
|
|
7138
7352
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
7353
|
+
const twoFactorStatus = await resolveTwoFactorStatus(ctx.context, userId, user);
|
|
7139
7354
|
if (minimal) return {
|
|
7140
7355
|
...user,
|
|
7141
7356
|
lastActiveAt: user.lastActiveAt ?? null,
|
|
@@ -7143,10 +7358,11 @@ const getUserDetails = (options) => {
|
|
|
7143
7358
|
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7144
7359
|
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
7145
7360
|
account: [],
|
|
7146
|
-
session: []
|
|
7361
|
+
session: [],
|
|
7362
|
+
twoFactorStatus
|
|
7147
7363
|
};
|
|
7148
7364
|
const activityTrackingEnabled = !!options.activityTracking?.enabled;
|
|
7149
|
-
const sessions = secondaryStorageOnly ? await ctx.context.internalAdapter.listSessions(userId) : user.session || [];
|
|
7365
|
+
const sessions = secondaryStorageOnly ? normalizeDashSessions(await ctx.context.internalAdapter.listSessions(userId)) : user.session || [];
|
|
7150
7366
|
let lastActiveAt = null;
|
|
7151
7367
|
if (activityTrackingEnabled) {
|
|
7152
7368
|
lastActiveAt = user.lastActiveAt ?? null;
|
|
@@ -7175,13 +7391,15 @@ const getUserDetails = (options) => {
|
|
|
7175
7391
|
updateActivity();
|
|
7176
7392
|
}
|
|
7177
7393
|
}
|
|
7394
|
+
const { session: _joinedSessions, ...userWithoutSessions } = user;
|
|
7178
7395
|
return {
|
|
7179
|
-
...
|
|
7180
|
-
|
|
7396
|
+
...userWithoutSessions,
|
|
7397
|
+
session: sanitizeSessions(sessions),
|
|
7181
7398
|
lastActiveAt,
|
|
7182
7399
|
banned: hasAdminPlugin ? user.banned ?? false : false,
|
|
7183
7400
|
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7184
|
-
banExpires: hasAdminPlugin ? user.banExpires ?? null : null
|
|
7401
|
+
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
7402
|
+
twoFactorStatus
|
|
7185
7403
|
};
|
|
7186
7404
|
});
|
|
7187
7405
|
};
|
|
@@ -7234,20 +7452,19 @@ const getUserOrganizations = (options) => {
|
|
|
7234
7452
|
const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
|
|
7235
7453
|
method: "POST",
|
|
7236
7454
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
7237
|
-
body:
|
|
7238
|
-
name: z$1.string().optional(),
|
|
7239
|
-
email: z$1.email().optional(),
|
|
7240
|
-
image: z$1.string().optional(),
|
|
7241
|
-
emailVerified: z$1.boolean().optional()
|
|
7242
|
-
})
|
|
7455
|
+
body: UpdateUserBodySchema
|
|
7243
7456
|
}, async (ctx) => {
|
|
7244
|
-
const updateData = ctx.body;
|
|
7245
7457
|
const userId = ctx.context.payload?.userId;
|
|
7246
7458
|
if (!userId) throw new APIError("FORBIDDEN", { message: "Invalid payload" });
|
|
7247
|
-
const
|
|
7248
|
-
|
|
7459
|
+
const updateData = pickWritableUserFields(ctx.body, ctx.context.options, "update");
|
|
7460
|
+
try {
|
|
7461
|
+
validateWritableUserUpdateFields(updateData, ctx.context.options);
|
|
7462
|
+
} catch (error) {
|
|
7463
|
+
if (error instanceof z$1.ZodError) throw new APIError("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid user data" });
|
|
7464
|
+
throw error;
|
|
7465
|
+
}
|
|
7249
7466
|
const user = await ctx.context.internalAdapter.updateUser(userId, {
|
|
7250
|
-
...
|
|
7467
|
+
...updateData,
|
|
7251
7468
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7252
7469
|
});
|
|
7253
7470
|
if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
@@ -7711,11 +7928,12 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
|
|
|
7711
7928
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
7712
7929
|
body: z$1.object({
|
|
7713
7930
|
banReason: z$1.string().optional(),
|
|
7714
|
-
banExpires: z$1.number().optional()
|
|
7931
|
+
banExpires: z$1.number().optional(),
|
|
7932
|
+
deleteAllSessions: z$1.boolean().optional().default(true)
|
|
7715
7933
|
})
|
|
7716
7934
|
}, async (ctx) => {
|
|
7717
7935
|
const { userId } = ctx.context.payload;
|
|
7718
|
-
const { banReason, banExpires } = ctx.body;
|
|
7936
|
+
const { banReason, banExpires, deleteAllSessions } = ctx.body;
|
|
7719
7937
|
if (!await ctx.context.internalAdapter.findUserById(userId)) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
7720
7938
|
await ctx.context.internalAdapter.updateUser(userId, {
|
|
7721
7939
|
banned: true,
|
|
@@ -7723,7 +7941,7 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
|
|
|
7723
7941
|
banExpires: banExpires ? new Date(banExpires) : null,
|
|
7724
7942
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7725
7943
|
});
|
|
7726
|
-
await ctx.context.internalAdapter
|
|
7944
|
+
if (deleteAllSessions) await deleteUserSessionsCompat(ctx.context.internalAdapter, userId);
|
|
7727
7945
|
return { success: true };
|
|
7728
7946
|
});
|
|
7729
7947
|
const banManyUsers = (options) => {
|
|
@@ -7732,11 +7950,12 @@ const banManyUsers = (options) => {
|
|
|
7732
7950
|
use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
|
|
7733
7951
|
body: z$1.object({
|
|
7734
7952
|
banReason: z$1.string().optional(),
|
|
7735
|
-
banExpires: z$1.number().optional()
|
|
7953
|
+
banExpires: z$1.number().optional(),
|
|
7954
|
+
deleteAllSessions: z$1.boolean().optional().default(true)
|
|
7736
7955
|
})
|
|
7737
7956
|
}, async (ctx) => {
|
|
7738
7957
|
const { userIds } = ctx.context.payload;
|
|
7739
|
-
const { banReason, banExpires } = ctx.body;
|
|
7958
|
+
const { banReason, banExpires, deleteAllSessions } = ctx.body;
|
|
7740
7959
|
const start = performance.now();
|
|
7741
7960
|
await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
|
|
7742
7961
|
await ctx.context.adapter.updateMany({
|
|
@@ -7768,6 +7987,9 @@ const banManyUsers = (options) => {
|
|
|
7768
7987
|
select: ["id"]
|
|
7769
7988
|
})).map((u) => u.id);
|
|
7770
7989
|
const bannedUserIds = userIds.filter((id) => !skippedUserIds.includes(id));
|
|
7990
|
+
if (deleteAllSessions && bannedUserIds.length > 0) await withConcurrency(chunkArray(bannedUserIds, { batchSize: 50 }), async (chunk) => {
|
|
7991
|
+
for (const userId of chunk) await deleteUserSessionsCompat(ctx.context.internalAdapter, userId);
|
|
7992
|
+
}, { concurrency: 3 });
|
|
7771
7993
|
const end = performance.now();
|
|
7772
7994
|
console.log(`Time taken to ban ${bannedUserIds.length} users: ${(end - start) / 1e3}s`, skippedUserIds.length > 0 ? `Skipped: ${skippedUserIds.length}` : "");
|
|
7773
7995
|
return ctx.json({
|
|
@@ -7794,7 +8016,7 @@ const unbanUser = (options) => createAuthEndpoint("/dash/unban-user", {
|
|
|
7794
8016
|
const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verification-email", {
|
|
7795
8017
|
method: "POST",
|
|
7796
8018
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
7797
|
-
body: z$1.object({ callbackUrl:
|
|
8019
|
+
body: z$1.object({ callbackUrl: safeUrlSchema })
|
|
7798
8020
|
}, async (ctx) => {
|
|
7799
8021
|
const { userId } = ctx.context.payload;
|
|
7800
8022
|
const { callbackUrl } = ctx.body;
|
|
@@ -7816,7 +8038,7 @@ const sendManyVerificationEmails = (options) => {
|
|
|
7816
8038
|
return createAuthEndpoint("/dash/send-many-verification-emails", {
|
|
7817
8039
|
method: "POST",
|
|
7818
8040
|
use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
|
|
7819
|
-
body: z$1.object({ callbackUrl:
|
|
8041
|
+
body: z$1.object({ callbackUrl: safeUrlSchema })
|
|
7820
8042
|
}, async (ctx) => {
|
|
7821
8043
|
if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
|
|
7822
8044
|
const { userIds } = ctx.context.payload;
|
|
@@ -7876,7 +8098,7 @@ const sendManyVerificationEmails = (options) => {
|
|
|
7876
8098
|
const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset-password-email", {
|
|
7877
8099
|
method: "POST",
|
|
7878
8100
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
7879
|
-
body: z$1.object({ callbackUrl:
|
|
8101
|
+
body: z$1.object({ callbackUrl: safeUrlSchema })
|
|
7880
8102
|
}, async (ctx) => {
|
|
7881
8103
|
const { userId } = ctx.context.payload;
|
|
7882
8104
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
@@ -7968,6 +8190,8 @@ const SMS_TEMPLATES = {
|
|
|
7968
8190
|
"two-factor": { variables: {} },
|
|
7969
8191
|
"sign-in-otp": { variables: {} }
|
|
7970
8192
|
};
|
|
8193
|
+
/** End-user IP forwarded to v1 SMS for abuse limits on server-to-server calls. */
|
|
8194
|
+
const BETTER_AUTH_CLIENT_IP_HEADER = "x-better-auth-client-ip";
|
|
7971
8195
|
/**
|
|
7972
8196
|
* Create an SMS sender instance
|
|
7973
8197
|
*/
|
|
@@ -7978,7 +8202,10 @@ function createSMSSender(config) {
|
|
|
7978
8202
|
if (!apiKey) logger.warn("[Dash] No API key provided for SMS sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
|
|
7979
8203
|
const $api = createFetch({
|
|
7980
8204
|
baseURL: apiUrl,
|
|
7981
|
-
headers: {
|
|
8205
|
+
headers: {
|
|
8206
|
+
"user-agent": INFRA_USER_AGENT,
|
|
8207
|
+
Authorization: `Bearer ${apiKey}`
|
|
8208
|
+
},
|
|
7982
8209
|
timeout: config?.apiTimeout ?? 3e3
|
|
7983
8210
|
});
|
|
7984
8211
|
/**
|
|
@@ -7996,7 +8223,12 @@ function createSMSSender(config) {
|
|
|
7996
8223
|
to: options.to,
|
|
7997
8224
|
code: options.code,
|
|
7998
8225
|
template: options.template
|
|
7999
|
-
}
|
|
8226
|
+
},
|
|
8227
|
+
...options.clientIp ? { headers: {
|
|
8228
|
+
Authorization: `Bearer ${apiKey}`,
|
|
8229
|
+
"user-agent": INFRA_USER_AGENT,
|
|
8230
|
+
[BETTER_AUTH_CLIENT_IP_HEADER]: options.clientIp
|
|
8231
|
+
} } : {}
|
|
8000
8232
|
});
|
|
8001
8233
|
if (error) return {
|
|
8002
8234
|
success: false,
|
|
@@ -8084,6 +8316,7 @@ const dash = (options) => {
|
|
|
8084
8316
|
options: opts,
|
|
8085
8317
|
version: PLUGIN_VERSION,
|
|
8086
8318
|
init(ctx) {
|
|
8319
|
+
instrumentSocialProviders(ctx.socialProviders);
|
|
8087
8320
|
const organizationPlugin = ctx.getPlugin("organization");
|
|
8088
8321
|
if (organizationPlugin) {
|
|
8089
8322
|
const instrumentOrganizationHooks = (organizationPluginOptions) => {
|
|
@@ -8313,7 +8546,8 @@ const dash = (options) => {
|
|
|
8313
8546
|
routes.CHANGE_PASSWORD,
|
|
8314
8547
|
routes.SET_PASSWORD,
|
|
8315
8548
|
routes.RESET_PASSWORD,
|
|
8316
|
-
routes.ADMIN_SET_PASSWORD
|
|
8549
|
+
routes.ADMIN_SET_PASSWORD,
|
|
8550
|
+
routes.DASH_SET_PASSWORD
|
|
8317
8551
|
])) trackAccountPasswordChange(account, trigger, ctx, location);
|
|
8318
8552
|
} },
|
|
8319
8553
|
delete: { async after(account, _ctx) {
|
|
@@ -8349,11 +8583,14 @@ const dash = (options) => {
|
|
|
8349
8583
|
before: [{
|
|
8350
8584
|
matcher: (ctx) => {
|
|
8351
8585
|
if (ctx.request?.method !== "GET") return true;
|
|
8352
|
-
|
|
8353
|
-
return matchesAnyRoute(path, [
|
|
8586
|
+
return matchesAnyRoute(ctx.path, [
|
|
8354
8587
|
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8355
8588
|
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
8356
|
-
routes.DASH_IMPERSONATE_USER
|
|
8589
|
+
routes.DASH_IMPERSONATE_USER,
|
|
8590
|
+
routes.VERIFY_EMAIL,
|
|
8591
|
+
routes.MAGIC_LINK_VERIFY,
|
|
8592
|
+
routes.DASH_ACCEPT_INVITATION,
|
|
8593
|
+
routes.DASH_COMPLETE_INVITATION_SOCIAL
|
|
8357
8594
|
]);
|
|
8358
8595
|
},
|
|
8359
8596
|
handler: createIdentificationMiddleware($kv)
|
|
@@ -8361,8 +8598,7 @@ const dash = (options) => {
|
|
|
8361
8598
|
after: [{
|
|
8362
8599
|
matcher: (ctx) => {
|
|
8363
8600
|
if (ctx.request?.method !== "GET") return true;
|
|
8364
|
-
|
|
8365
|
-
return matchesAnyRoute(path, [
|
|
8601
|
+
return matchesAnyRoute(ctx.path, [
|
|
8366
8602
|
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8367
8603
|
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
8368
8604
|
routes.DASH_IMPERSONATE_USER
|
|
@@ -8371,11 +8607,11 @@ const dash = (options) => {
|
|
|
8371
8607
|
handler: createAuthMiddleware(async (_ctx) => {
|
|
8372
8608
|
const ctx = _ctx;
|
|
8373
8609
|
const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
|
|
8374
|
-
if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
|
|
8610
|
+
if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL, routes.DASH_SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
|
|
8375
8611
|
const body = ctx.body;
|
|
8376
8612
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger, ctx.context.location);
|
|
8377
8613
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger, ctx.context.location);
|
|
8378
|
-
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" &&
|
|
8614
|
+
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]) && ctx.request?.method === "GET" && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger, ctx.context.location);
|
|
8379
8615
|
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
8380
8616
|
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
8381
8617
|
maxAge: 600,
|
|
@@ -8470,6 +8706,7 @@ const dash = (options) => {
|
|
|
8470
8706
|
dashSendManyVerificationEmails: sendManyVerificationEmails(settings),
|
|
8471
8707
|
dashSendResetPasswordEmail: sendResetPasswordEmail(settings),
|
|
8472
8708
|
dashEnableTwoFactor: enableTwoFactor(settings),
|
|
8709
|
+
dashCompleteTwoFactorSetup: completeTwoFactorSetup(settings),
|
|
8473
8710
|
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(settings),
|
|
8474
8711
|
dashViewBackupCodes: viewBackupCodes(settings),
|
|
8475
8712
|
dashDisableTwoFactor: disableTwoFactor(settings),
|