@better-auth/infra 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +11 -8
- package/dist/index.mjs +597 -511
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EMAIL_TEMPLATES, EmailConfig, EmailTemplateId, EmailTemplateVariables, SendBulkEmailsOptions, SendBulkEmailsResult, SendEmailOptions, SendEmailResult, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
2
2
|
import * as _$better_auth0 from "better-auth";
|
|
3
|
-
import { Account, AuthContext, BetterAuthPlugin, GenericEndpointContext,
|
|
3
|
+
import { Account, AuthContext, BetterAuthPlugin, GenericEndpointContext, Session, User } from "better-auth";
|
|
4
4
|
import * as _$jose from "jose";
|
|
5
5
|
import * as zod from "zod";
|
|
6
6
|
import z$1 from "zod";
|
|
@@ -101,6 +101,9 @@ interface SecurityOptions {
|
|
|
101
101
|
action?: SecurityAction;
|
|
102
102
|
domainAllowlist?: string[];
|
|
103
103
|
};
|
|
104
|
+
emailNormalization?: {
|
|
105
|
+
enabled?: boolean;
|
|
106
|
+
};
|
|
104
107
|
staleUsers?: {
|
|
105
108
|
enabled: boolean;
|
|
106
109
|
staleDays?: number;
|
|
@@ -167,9 +170,6 @@ interface SecurityEvent {
|
|
|
167
170
|
}
|
|
168
171
|
type SecurityEventType = "unknown_device" | "credential_stuffing" | "impossible_travel" | "geo_blocked" | "bot_blocked" | "suspicious_ip_detected" | "velocity_exceeded" | "free_trial_abuse" | "compromised_password" | "stale_account_reactivation";
|
|
169
172
|
//#endregion
|
|
170
|
-
//#region src/validation/matchers.d.ts
|
|
171
|
-
type Matcher = (context: HookEndpointContext) => boolean;
|
|
172
|
-
//#endregion
|
|
173
173
|
//#region src/sentinel/sentinel.d.ts
|
|
174
174
|
declare const sentinel: (options?: SentinelOptions) => {
|
|
175
175
|
id: "sentinel";
|
|
@@ -181,6 +181,9 @@ declare const sentinel: (options?: SentinelOptions) => {
|
|
|
181
181
|
action?: SecurityAction;
|
|
182
182
|
domainAllowlist?: string[];
|
|
183
183
|
} | undefined;
|
|
184
|
+
emailNormalization: {
|
|
185
|
+
enabled?: boolean;
|
|
186
|
+
} | undefined;
|
|
184
187
|
databaseHooks: {
|
|
185
188
|
user: {
|
|
186
189
|
create: {
|
|
@@ -243,7 +246,7 @@ declare const sentinel: (options?: SentinelOptions) => {
|
|
|
243
246
|
};
|
|
244
247
|
hooks: {
|
|
245
248
|
before: ({
|
|
246
|
-
matcher:
|
|
249
|
+
matcher: (context: _$better_auth0.HookEndpointContext) => boolean;
|
|
247
250
|
handler: (inputContext: _$better_call0.MiddlewareInputContext<_$better_call0.MiddlewareOptions>) => Promise<{
|
|
248
251
|
context: {
|
|
249
252
|
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
@@ -415,7 +418,7 @@ declare const sentinel: (options?: SentinelOptions) => {
|
|
|
415
418
|
};
|
|
416
419
|
} | undefined>;
|
|
417
420
|
} | {
|
|
418
|
-
matcher:
|
|
421
|
+
matcher: (context: _$better_auth0.HookEndpointContext) => boolean;
|
|
419
422
|
handler: (inputContext: _$better_call0.MiddlewareInputContext<_$better_call0.MiddlewareOptions>) => Promise<void>;
|
|
420
423
|
})[];
|
|
421
424
|
after: {
|
|
@@ -723,7 +726,7 @@ interface DashIdRow {
|
|
|
723
726
|
id: string;
|
|
724
727
|
}
|
|
725
728
|
//#endregion
|
|
726
|
-
//#region ../../node_modules/.bun/@better-auth+scim@1.6.
|
|
729
|
+
//#region ../../node_modules/.bun/@better-auth+scim@1.6.4+f3cd00d7a973e2d8/node_modules/@better-auth/scim/dist/index.d.mts
|
|
727
730
|
//#region src/types.d.ts
|
|
728
731
|
interface SCIMProvider {
|
|
729
732
|
id: string;
|
|
@@ -4586,7 +4589,7 @@ interface DashCheckUserExistsResponse {
|
|
|
4586
4589
|
* - Normalize googlemail.com to gmail.com
|
|
4587
4590
|
*
|
|
4588
4591
|
* @param email - Raw email to normalize
|
|
4589
|
-
* @param context - Auth context
|
|
4592
|
+
* @param context - Auth context
|
|
4590
4593
|
*/
|
|
4591
4594
|
declare function normalizeEmail(email: string, context: AuthContext): string;
|
|
4592
4595
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -997,502 +997,6 @@ function getCountryCodeFromRequest(request) {
|
|
|
997
997
|
return cc ? cc.toUpperCase() : void 0;
|
|
998
998
|
}
|
|
999
999
|
//#endregion
|
|
1000
|
-
//#region src/validation/matchers.ts
|
|
1001
|
-
const paths = [
|
|
1002
|
-
"/sign-up/email",
|
|
1003
|
-
"/email-otp/verify-email",
|
|
1004
|
-
"/sign-in/email-otp",
|
|
1005
|
-
"/sign-in/magic-link",
|
|
1006
|
-
"/sign-in/email",
|
|
1007
|
-
"/forget-password/email-otp",
|
|
1008
|
-
"/email-otp/reset-password",
|
|
1009
|
-
"/email-otp/create-verification-otp",
|
|
1010
|
-
"/email-otp/get-verification-otp",
|
|
1011
|
-
"/email-otp/send-verification-otp",
|
|
1012
|
-
"/forget-password",
|
|
1013
|
-
"/request-password-reset",
|
|
1014
|
-
"/send-verification-email",
|
|
1015
|
-
"/change-email"
|
|
1016
|
-
];
|
|
1017
|
-
const all = new Set(paths);
|
|
1018
|
-
new Set(paths.slice(1, 12));
|
|
1019
|
-
/**
|
|
1020
|
-
* Path is one of `[
|
|
1021
|
-
* '/sign-up/email',
|
|
1022
|
-
* '/email-otp/verify-email',
|
|
1023
|
-
* '/sign-in/email-otp',
|
|
1024
|
-
* '/sign-in/magic-link',
|
|
1025
|
-
* '/sign-in/email',
|
|
1026
|
-
* '/forget-password/email-otp',
|
|
1027
|
-
* '/email-otp/reset-password',
|
|
1028
|
-
* '/email-otp/create-verification-otp',
|
|
1029
|
-
* '/email-otp/get-verification-otp',
|
|
1030
|
-
* '/email-otp/send-verification-otp',
|
|
1031
|
-
* '/forget-password',
|
|
1032
|
-
* '/request-password-reset',
|
|
1033
|
-
* '/send-verification-email',
|
|
1034
|
-
* '/change-email'
|
|
1035
|
-
* ]`.
|
|
1036
|
-
* @param context Request context
|
|
1037
|
-
* @param context.path Request path
|
|
1038
|
-
* @returns boolean
|
|
1039
|
-
*/
|
|
1040
|
-
const allEmail = ({ path }) => !!path && all.has(path);
|
|
1041
|
-
//#endregion
|
|
1042
|
-
//#region src/validation/email.ts
|
|
1043
|
-
/**
|
|
1044
|
-
* Gmail-like providers that ignore dots in the local part
|
|
1045
|
-
*/
|
|
1046
|
-
const GMAIL_LIKE_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
|
|
1047
|
-
/**
|
|
1048
|
-
* Providers known to support plus addressing
|
|
1049
|
-
*/
|
|
1050
|
-
const PLUS_ADDRESSING_DOMAINS = new Set([
|
|
1051
|
-
"gmail.com",
|
|
1052
|
-
"googlemail.com",
|
|
1053
|
-
"outlook.com",
|
|
1054
|
-
"hotmail.com",
|
|
1055
|
-
"live.com",
|
|
1056
|
-
"yahoo.com",
|
|
1057
|
-
"icloud.com",
|
|
1058
|
-
"me.com",
|
|
1059
|
-
"mac.com",
|
|
1060
|
-
"protonmail.com",
|
|
1061
|
-
"proton.me",
|
|
1062
|
-
"fastmail.com",
|
|
1063
|
-
"zoho.com"
|
|
1064
|
-
]);
|
|
1065
|
-
/**
|
|
1066
|
-
* Normalize an email address for comparison/deduplication
|
|
1067
|
-
* - Lowercase the entire email
|
|
1068
|
-
* - Remove dots from Gmail-like providers (they ignore dots)
|
|
1069
|
-
* - Remove plus addressing (user+tag@domain → user@domain)
|
|
1070
|
-
* - Normalize googlemail.com to gmail.com
|
|
1071
|
-
*
|
|
1072
|
-
* @param email - Raw email to normalize
|
|
1073
|
-
* @param context - Auth context with getPlugin (for sentinel policy)
|
|
1074
|
-
*/
|
|
1075
|
-
function normalizeEmail(email, context) {
|
|
1076
|
-
if (!email || typeof email !== "string") return email;
|
|
1077
|
-
if ((context.getPlugin?.("sentinel"))?.options?.emailValidation?.enabled === false) return email;
|
|
1078
|
-
const trimmed = email.trim().toLowerCase();
|
|
1079
|
-
const atIndex = trimmed.lastIndexOf("@");
|
|
1080
|
-
if (atIndex === -1) return trimmed;
|
|
1081
|
-
let localPart = trimmed.slice(0, atIndex);
|
|
1082
|
-
let domain = trimmed.slice(atIndex + 1);
|
|
1083
|
-
if (domain === "googlemail.com") domain = "gmail.com";
|
|
1084
|
-
if (PLUS_ADDRESSING_DOMAINS.has(domain)) {
|
|
1085
|
-
const plusIndex = localPart.indexOf("+");
|
|
1086
|
-
if (plusIndex !== -1) localPart = localPart.slice(0, plusIndex);
|
|
1087
|
-
}
|
|
1088
|
-
if (GMAIL_LIKE_DOMAINS.has(domain)) localPart = localPart.replace(/\./g, "");
|
|
1089
|
-
return `${localPart}@${domain}`;
|
|
1090
|
-
}
|
|
1091
|
-
function createEmailValidator(options = {}) {
|
|
1092
|
-
const { apiKey = "", apiUrl = INFRA_API_URL, kvUrl = INFRA_KV_URL, defaultConfig = {} } = options;
|
|
1093
|
-
const $api = createFetch({
|
|
1094
|
-
baseURL: apiUrl,
|
|
1095
|
-
headers: { "x-api-key": apiKey }
|
|
1096
|
-
});
|
|
1097
|
-
const $kv = createFetch({
|
|
1098
|
-
baseURL: kvUrl,
|
|
1099
|
-
headers: { "x-api-key": apiKey },
|
|
1100
|
-
timeout: KV_TIMEOUT_MS
|
|
1101
|
-
});
|
|
1102
|
-
/**
|
|
1103
|
-
* Fetch and resolve email validity policy from API with caching
|
|
1104
|
-
* Sends client config to API which merges with user's dashboard settings
|
|
1105
|
-
*/
|
|
1106
|
-
async function fetchPolicy() {
|
|
1107
|
-
try {
|
|
1108
|
-
const { data } = await $api("/security/resolve-policy", {
|
|
1109
|
-
method: "POST",
|
|
1110
|
-
body: {
|
|
1111
|
-
policyId: "email_validity",
|
|
1112
|
-
config: { emailValidation: {
|
|
1113
|
-
enabled: defaultConfig.enabled,
|
|
1114
|
-
strictness: defaultConfig.strictness,
|
|
1115
|
-
action: defaultConfig.action,
|
|
1116
|
-
domainAllowlist: defaultConfig.domainAllowlist
|
|
1117
|
-
} }
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
if (data?.policy) return data.policy;
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
logger.warn("[Dash] Failed to fetch email policy, using defaults:", error);
|
|
1123
|
-
}
|
|
1124
|
-
return null;
|
|
1125
|
-
}
|
|
1126
|
-
return { async validate(email, checkMx = true) {
|
|
1127
|
-
const trimmed = email.trim();
|
|
1128
|
-
const policy = await fetchPolicy();
|
|
1129
|
-
if (!policy?.enabled) return {
|
|
1130
|
-
valid: true,
|
|
1131
|
-
disposable: false,
|
|
1132
|
-
confidence: "high",
|
|
1133
|
-
policy
|
|
1134
|
-
};
|
|
1135
|
-
try {
|
|
1136
|
-
const { data } = await $kv("/email/validate", {
|
|
1137
|
-
method: "POST",
|
|
1138
|
-
body: {
|
|
1139
|
-
email: trimmed,
|
|
1140
|
-
checkMx,
|
|
1141
|
-
strictness: policy.strictness
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
return {
|
|
1145
|
-
...data || {
|
|
1146
|
-
valid: false,
|
|
1147
|
-
reason: "invalid_format"
|
|
1148
|
-
},
|
|
1149
|
-
policy
|
|
1150
|
-
};
|
|
1151
|
-
} catch (error) {
|
|
1152
|
-
logger.warn("[Dash] Email validation API error, falling back to allow:", error);
|
|
1153
|
-
return {
|
|
1154
|
-
valid: true,
|
|
1155
|
-
policy
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
} };
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* Basic local email format validation (fallback)
|
|
1162
|
-
*/
|
|
1163
|
-
function isValidEmailFormatLocal(email) {
|
|
1164
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return false;
|
|
1165
|
-
if (email.length > 254) return false;
|
|
1166
|
-
const [localPart, domain] = email.split("@");
|
|
1167
|
-
if (!localPart || !domain) return false;
|
|
1168
|
-
if (localPart.length > 64) return false;
|
|
1169
|
-
if (domain.length > 253) return false;
|
|
1170
|
-
return true;
|
|
1171
|
-
}
|
|
1172
|
-
const getEmail = (ctx) => {
|
|
1173
|
-
if (ctx.path === "/change-email") return {
|
|
1174
|
-
email: ctx.body?.newEmail,
|
|
1175
|
-
container: "body",
|
|
1176
|
-
field: "newEmail"
|
|
1177
|
-
};
|
|
1178
|
-
const body = ctx.body;
|
|
1179
|
-
const query = ctx.query;
|
|
1180
|
-
return {
|
|
1181
|
-
email: body?.email ?? query?.email,
|
|
1182
|
-
container: body ? "body" : "query",
|
|
1183
|
-
field: "email"
|
|
1184
|
-
};
|
|
1185
|
-
};
|
|
1186
|
-
/**
|
|
1187
|
-
* Create email normalization hook (shared between all configurations)
|
|
1188
|
-
*/
|
|
1189
|
-
function createEmailNormalizationHook() {
|
|
1190
|
-
return {
|
|
1191
|
-
matcher: allEmail,
|
|
1192
|
-
handler: createAuthMiddleware(async (ctx) => {
|
|
1193
|
-
const { email, container, field } = getEmail(ctx);
|
|
1194
|
-
if (typeof email !== "string") return;
|
|
1195
|
-
const normalized = normalizeEmail(email, ctx.context);
|
|
1196
|
-
if (normalized === email) return;
|
|
1197
|
-
const data = container === "query" ? {
|
|
1198
|
-
...ctx.query,
|
|
1199
|
-
[field]: normalized
|
|
1200
|
-
} : {
|
|
1201
|
-
...ctx.body,
|
|
1202
|
-
[field]: normalized
|
|
1203
|
-
};
|
|
1204
|
-
return { context: {
|
|
1205
|
-
...ctx,
|
|
1206
|
-
[container]: data
|
|
1207
|
-
} };
|
|
1208
|
-
})
|
|
1209
|
-
};
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Create email validation hook with configurable validation strategy
|
|
1213
|
-
*/
|
|
1214
|
-
function createEmailValidationHook(validator, onDisposableEmail) {
|
|
1215
|
-
return {
|
|
1216
|
-
matcher: allEmail,
|
|
1217
|
-
handler: createAuthMiddleware(async (ctx) => {
|
|
1218
|
-
const { email } = getEmail(ctx);
|
|
1219
|
-
if (typeof email !== "string") return;
|
|
1220
|
-
const trimmed = email.trim();
|
|
1221
|
-
if (!isValidEmailFormatLocal(trimmed)) throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
|
|
1222
|
-
if (validator) {
|
|
1223
|
-
const result = await validator.validate(trimmed);
|
|
1224
|
-
const policy = result.policy;
|
|
1225
|
-
if (!policy?.enabled) return;
|
|
1226
|
-
if (policy.domainAllowlist?.length) {
|
|
1227
|
-
const domain = trimmed.toLowerCase().split("@")[1];
|
|
1228
|
-
if (domain && policy.domainAllowlist.includes(domain)) return;
|
|
1229
|
-
}
|
|
1230
|
-
const action = policy.action;
|
|
1231
|
-
if (!result.valid) {
|
|
1232
|
-
if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist" || result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short") && onDisposableEmail) {
|
|
1233
|
-
const ip = ctx.request?.headers?.get("x-forwarded-for")?.split(",")[0] || ctx.request?.headers?.get("cf-connecting-ip") || void 0;
|
|
1234
|
-
onDisposableEmail({
|
|
1235
|
-
email: trimmed,
|
|
1236
|
-
reason: result.reason || "disposable",
|
|
1237
|
-
confidence: result.confidence,
|
|
1238
|
-
ip,
|
|
1239
|
-
path: ctx.path,
|
|
1240
|
-
action
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1243
|
-
if (action === "allow") return;
|
|
1244
|
-
throw new APIError$1("BAD_REQUEST", { message: result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short" ? "This email address appears to be invalid" : "Invalid email" });
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
})
|
|
1248
|
-
};
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Create email validation hooks with optional API-backed validation
|
|
1252
|
-
*
|
|
1253
|
-
* @param options - Configuration options
|
|
1254
|
-
* @param options.enabled - Enable email validation (default: true)
|
|
1255
|
-
* @param options.useApi - Use API-backed validation (requires apiKey)
|
|
1256
|
-
* @param options.apiKey - API key for remote validation
|
|
1257
|
-
* @param options.apiUrl - API URL for policy fetching (defaults to INFRA_API_URL)
|
|
1258
|
-
* @param options.kvUrl - KV URL for email validation (defaults to INFRA_KV_URL)
|
|
1259
|
-
* @param options.strictness - Default strictness level: 'low', 'medium' (default), or 'high'
|
|
1260
|
-
* @param options.action - Default action when invalid: 'allow', 'block' (default), or 'challenge'
|
|
1261
|
-
*
|
|
1262
|
-
* @example
|
|
1263
|
-
* // Local validation only
|
|
1264
|
-
* createEmailHooks()
|
|
1265
|
-
*
|
|
1266
|
-
* @example
|
|
1267
|
-
* // API-backed validation
|
|
1268
|
-
* createEmailHooks({ useApi: true, apiKey: "your-api-key" })
|
|
1269
|
-
*
|
|
1270
|
-
* @example
|
|
1271
|
-
* // API-backed validation with high strictness default
|
|
1272
|
-
* createEmailHooks({ useApi: true, apiKey: "your-api-key", strictness: "high" })
|
|
1273
|
-
*/
|
|
1274
|
-
function createEmailHooks(options = {}) {
|
|
1275
|
-
const { useApi = false, apiKey = "", apiUrl = INFRA_API_URL, kvUrl = INFRA_KV_URL, defaultConfig, onDisposableEmail } = options;
|
|
1276
|
-
const emailConfig = {
|
|
1277
|
-
enabled: true,
|
|
1278
|
-
strictness: "medium",
|
|
1279
|
-
action: "block",
|
|
1280
|
-
...defaultConfig
|
|
1281
|
-
};
|
|
1282
|
-
if (!emailConfig.enabled) return { before: [] };
|
|
1283
|
-
const validator = useApi ? createEmailValidator({
|
|
1284
|
-
apiUrl,
|
|
1285
|
-
kvUrl,
|
|
1286
|
-
apiKey,
|
|
1287
|
-
defaultConfig: emailConfig
|
|
1288
|
-
}) : void 0;
|
|
1289
|
-
return { before: [createEmailNormalizationHook(), createEmailValidationHook(validator, onDisposableEmail)] };
|
|
1290
|
-
}
|
|
1291
|
-
createEmailHooks();
|
|
1292
|
-
//#endregion
|
|
1293
|
-
//#region src/validation/phone.ts
|
|
1294
|
-
/**
|
|
1295
|
-
* Common fake/test phone numbers that should be blocked
|
|
1296
|
-
* These are numbers commonly used in testing, movies, documentation, etc.
|
|
1297
|
-
*/
|
|
1298
|
-
const INVALID_PHONE_NUMBERS = new Set([
|
|
1299
|
-
"+15550000000",
|
|
1300
|
-
"+15550001111",
|
|
1301
|
-
"+15550001234",
|
|
1302
|
-
"+15551234567",
|
|
1303
|
-
"+15555555555",
|
|
1304
|
-
"+15551111111",
|
|
1305
|
-
"+15550000001",
|
|
1306
|
-
"+15550123456",
|
|
1307
|
-
"+12125551234",
|
|
1308
|
-
"+13105551234",
|
|
1309
|
-
"+14155551234",
|
|
1310
|
-
"+12025551234",
|
|
1311
|
-
"+10000000000",
|
|
1312
|
-
"+11111111111",
|
|
1313
|
-
"+12222222222",
|
|
1314
|
-
"+13333333333",
|
|
1315
|
-
"+14444444444",
|
|
1316
|
-
"+15555555555",
|
|
1317
|
-
"+16666666666",
|
|
1318
|
-
"+17777777777",
|
|
1319
|
-
"+18888888888",
|
|
1320
|
-
"+19999999999",
|
|
1321
|
-
"+11234567890",
|
|
1322
|
-
"+10123456789",
|
|
1323
|
-
"+19876543210",
|
|
1324
|
-
"+441632960000",
|
|
1325
|
-
"+447700900000",
|
|
1326
|
-
"+447700900001",
|
|
1327
|
-
"+447700900123",
|
|
1328
|
-
"+447700900999",
|
|
1329
|
-
"+442079460000",
|
|
1330
|
-
"+442079460123",
|
|
1331
|
-
"+441134960000",
|
|
1332
|
-
"+0000000000",
|
|
1333
|
-
"+1000000000",
|
|
1334
|
-
"+123456789",
|
|
1335
|
-
"+1234567890",
|
|
1336
|
-
"+12345678901",
|
|
1337
|
-
"+0123456789",
|
|
1338
|
-
"+9876543210",
|
|
1339
|
-
"+11111111111",
|
|
1340
|
-
"+99999999999",
|
|
1341
|
-
"+491234567890",
|
|
1342
|
-
"+491111111111",
|
|
1343
|
-
"+33123456789",
|
|
1344
|
-
"+33111111111",
|
|
1345
|
-
"+61123456789",
|
|
1346
|
-
"+61111111111",
|
|
1347
|
-
"+81123456789",
|
|
1348
|
-
"+81111111111",
|
|
1349
|
-
"+19001234567",
|
|
1350
|
-
"+19761234567",
|
|
1351
|
-
"+1911",
|
|
1352
|
-
"+1411",
|
|
1353
|
-
"+1611",
|
|
1354
|
-
"+44999",
|
|
1355
|
-
"+44112"
|
|
1356
|
-
]);
|
|
1357
|
-
/**
|
|
1358
|
-
* Patterns that indicate fake/test phone numbers
|
|
1359
|
-
*/
|
|
1360
|
-
const INVALID_PHONE_PATTERNS = [
|
|
1361
|
-
/^\+\d(\d)\1{6,}$/,
|
|
1362
|
-
/^\+\d*1234567890/,
|
|
1363
|
-
/^\+\d*0123456789/,
|
|
1364
|
-
/^\+\d*9876543210/,
|
|
1365
|
-
/^\+\d*0987654321/,
|
|
1366
|
-
/^\+\d*(12){4,}/,
|
|
1367
|
-
/^\+\d*(21){4,}/,
|
|
1368
|
-
/^\+\d*(00){4,}/,
|
|
1369
|
-
/^\+1\d{3}555\d{4}$/,
|
|
1370
|
-
/^\+\d{1,3}\d{1,5}$/,
|
|
1371
|
-
/^\+\d+0{7,}$/,
|
|
1372
|
-
/^\+\d*147258369/,
|
|
1373
|
-
/^\+\d*258369147/,
|
|
1374
|
-
/^\+\d*369258147/,
|
|
1375
|
-
/^\+\d*789456123/,
|
|
1376
|
-
/^\+\d*123456789/,
|
|
1377
|
-
/^\+\d*1234512345/,
|
|
1378
|
-
/^\+\d*1111122222/,
|
|
1379
|
-
/^\+\d*1212121212/,
|
|
1380
|
-
/^\+\d*1010101010/
|
|
1381
|
-
];
|
|
1382
|
-
/**
|
|
1383
|
-
* Invalid area codes / prefixes that indicate test numbers
|
|
1384
|
-
* Key: country code, Value: set of invalid prefixes
|
|
1385
|
-
*/
|
|
1386
|
-
const INVALID_PREFIXES_BY_COUNTRY = {
|
|
1387
|
-
US: new Set([
|
|
1388
|
-
"555",
|
|
1389
|
-
"000",
|
|
1390
|
-
"111",
|
|
1391
|
-
"911",
|
|
1392
|
-
"411",
|
|
1393
|
-
"611"
|
|
1394
|
-
]),
|
|
1395
|
-
CA: new Set([
|
|
1396
|
-
"555",
|
|
1397
|
-
"000",
|
|
1398
|
-
"911"
|
|
1399
|
-
]),
|
|
1400
|
-
GB: new Set([
|
|
1401
|
-
"7700900",
|
|
1402
|
-
"1632960",
|
|
1403
|
-
"1134960"
|
|
1404
|
-
]),
|
|
1405
|
-
AU: new Set([
|
|
1406
|
-
"0491570",
|
|
1407
|
-
"0491571",
|
|
1408
|
-
"0491572"
|
|
1409
|
-
])
|
|
1410
|
-
};
|
|
1411
|
-
/**
|
|
1412
|
-
* Check if a phone number is a commonly used fake/test number
|
|
1413
|
-
* @param phone - The phone number to check (E.164 format preferred)
|
|
1414
|
-
* @param defaultCountry - Default country code if not included in phone string
|
|
1415
|
-
* @returns true if the phone appears to be fake/test, false if it seems legitimate
|
|
1416
|
-
*/
|
|
1417
|
-
const isFakePhoneNumber = (phone, defaultCountry) => {
|
|
1418
|
-
const parsed = parsePhoneNumberFromString(phone, defaultCountry);
|
|
1419
|
-
if (!parsed) return true;
|
|
1420
|
-
const e164 = parsed.number;
|
|
1421
|
-
const nationalNumber = parsed.nationalNumber;
|
|
1422
|
-
const country = parsed.country;
|
|
1423
|
-
if (INVALID_PHONE_NUMBERS.has(e164)) return true;
|
|
1424
|
-
for (const pattern of INVALID_PHONE_PATTERNS) if (pattern.test(e164)) return true;
|
|
1425
|
-
if (country && INVALID_PREFIXES_BY_COUNTRY[country]) {
|
|
1426
|
-
const prefixes = INVALID_PREFIXES_BY_COUNTRY[country];
|
|
1427
|
-
for (const prefix of prefixes) if (nationalNumber.startsWith(prefix)) return true;
|
|
1428
|
-
}
|
|
1429
|
-
if (/^(\d)\1+$/.test(nationalNumber)) return true;
|
|
1430
|
-
const digits = nationalNumber.split("").map(Number);
|
|
1431
|
-
let isSequential = digits.length >= 6;
|
|
1432
|
-
for (let i = 1; i < digits.length && isSequential; i++) {
|
|
1433
|
-
const current = digits[i];
|
|
1434
|
-
const previous = digits[i - 1];
|
|
1435
|
-
if (current === void 0 || previous === void 0 || current !== previous + 1 && current !== previous - 1) isSequential = false;
|
|
1436
|
-
}
|
|
1437
|
-
if (isSequential) return true;
|
|
1438
|
-
return false;
|
|
1439
|
-
};
|
|
1440
|
-
/**
|
|
1441
|
-
* Validate a phone number format
|
|
1442
|
-
* @param phone - The phone number to validate
|
|
1443
|
-
* @param defaultCountry - Default country code if not included in phone string
|
|
1444
|
-
* @returns true if the phone number is valid
|
|
1445
|
-
*/
|
|
1446
|
-
const isValidPhone = (phone, defaultCountry) => {
|
|
1447
|
-
return isValidPhoneNumber(phone, defaultCountry);
|
|
1448
|
-
};
|
|
1449
|
-
/**
|
|
1450
|
-
* Comprehensive phone number validation
|
|
1451
|
-
* @param phone - The phone number to validate
|
|
1452
|
-
* @param options - Validation options
|
|
1453
|
-
* @returns true if valid, false otherwise
|
|
1454
|
-
*/
|
|
1455
|
-
const validatePhone = (phone, options = {}) => {
|
|
1456
|
-
const { mobileOnly = false, allowedCountries, blockedCountries, blockFakeNumbers = true, blockPremiumRate = true, blockTollFree = false, blockVoip = false, defaultCountry } = options;
|
|
1457
|
-
if (!isValidPhone(phone, defaultCountry)) return false;
|
|
1458
|
-
const parsed = parsePhoneNumberFromString(phone, defaultCountry);
|
|
1459
|
-
if (!parsed) return false;
|
|
1460
|
-
if (blockFakeNumbers && isFakePhoneNumber(phone, defaultCountry)) return false;
|
|
1461
|
-
const country = parsed.country;
|
|
1462
|
-
if (country) {
|
|
1463
|
-
if (allowedCountries && !allowedCountries.includes(country)) return false;
|
|
1464
|
-
if (blockedCountries?.includes(country)) return false;
|
|
1465
|
-
}
|
|
1466
|
-
const phoneType = parsed.getType();
|
|
1467
|
-
if (mobileOnly) {
|
|
1468
|
-
if (phoneType !== "MOBILE" && phoneType !== "FIXED_LINE_OR_MOBILE") return false;
|
|
1469
|
-
}
|
|
1470
|
-
if (blockPremiumRate && phoneType === "PREMIUM_RATE") return false;
|
|
1471
|
-
if (blockTollFree && phoneType === "TOLL_FREE") return false;
|
|
1472
|
-
if (blockVoip && phoneType === "VOIP") return false;
|
|
1473
|
-
return true;
|
|
1474
|
-
};
|
|
1475
|
-
const allPhonePaths = new Set([
|
|
1476
|
-
"/phone-number/send-otp",
|
|
1477
|
-
"/phone-number/verify",
|
|
1478
|
-
"/sign-in/phone-number",
|
|
1479
|
-
"/phone-number/request-password-reset",
|
|
1480
|
-
"/phone-number/reset-password"
|
|
1481
|
-
]);
|
|
1482
|
-
const getPhoneNumber = (ctx) => ctx.body?.phoneNumber ?? ctx.query?.phoneNumber;
|
|
1483
|
-
/**
|
|
1484
|
-
* Better Auth plugin for phone number validation
|
|
1485
|
-
* Validates phone numbers on all phone-related endpoints
|
|
1486
|
-
*/
|
|
1487
|
-
const phoneValidationHooks = { before: [{
|
|
1488
|
-
matcher: (context) => !!context.path && allPhonePaths.has(context.path),
|
|
1489
|
-
handler: createAuthMiddleware(async (ctx) => {
|
|
1490
|
-
const phoneNumber = getPhoneNumber(ctx);
|
|
1491
|
-
if (typeof phoneNumber !== "string") return;
|
|
1492
|
-
if (!validatePhone(phoneNumber)) throw new APIError$1("BAD_REQUEST", { message: "Invalid phone number" });
|
|
1493
|
-
})
|
|
1494
|
-
}] };
|
|
1495
|
-
//#endregion
|
|
1496
1000
|
//#region src/sentinel/security.ts
|
|
1497
1001
|
async function hashForFingerprint(input) {
|
|
1498
1002
|
const data = new TextEncoder().encode(input);
|
|
@@ -1504,6 +1008,16 @@ async function sha1Hash(input) {
|
|
|
1504
1008
|
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
|
|
1505
1009
|
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
1506
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Whether Sentinel should normalize emails for deduplication/sign-in consistency.
|
|
1013
|
+
* If `emailNormalization` is set, it wins; otherwise legacy behavior uses `emailValidation.enabled`.
|
|
1014
|
+
* @internal Not part of the package public API; used by the sentinel plugin and email helpers.
|
|
1015
|
+
*/
|
|
1016
|
+
function isEmailNormalizationEnabled(security) {
|
|
1017
|
+
const explicit = security?.emailNormalization;
|
|
1018
|
+
if (explicit !== void 0) return explicit.enabled !== false;
|
|
1019
|
+
return security?.emailValidation?.enabled !== false;
|
|
1020
|
+
}
|
|
1507
1021
|
function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
1508
1022
|
const resolvedApiUrl = apiUrl || INFRA_API_URL;
|
|
1509
1023
|
const $api = createFetch({
|
|
@@ -1883,6 +1397,498 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1883
1397
|
};
|
|
1884
1398
|
}
|
|
1885
1399
|
//#endregion
|
|
1400
|
+
//#region src/validation/matchers.ts
|
|
1401
|
+
const all = new Set([
|
|
1402
|
+
"/sign-up/email",
|
|
1403
|
+
"/email-otp/verify-email",
|
|
1404
|
+
"/sign-in/email-otp",
|
|
1405
|
+
"/sign-in/magic-link",
|
|
1406
|
+
"/sign-in/email",
|
|
1407
|
+
"/forget-password/email-otp",
|
|
1408
|
+
"/email-otp/reset-password",
|
|
1409
|
+
"/email-otp/create-verification-otp",
|
|
1410
|
+
"/email-otp/get-verification-otp",
|
|
1411
|
+
"/email-otp/send-verification-otp",
|
|
1412
|
+
"/forget-password",
|
|
1413
|
+
"/request-password-reset",
|
|
1414
|
+
"/send-verification-email",
|
|
1415
|
+
"/change-email"
|
|
1416
|
+
]);
|
|
1417
|
+
/**
|
|
1418
|
+
* Path is one of `[
|
|
1419
|
+
* '/sign-up/email',
|
|
1420
|
+
* '/email-otp/verify-email',
|
|
1421
|
+
* '/sign-in/email-otp',
|
|
1422
|
+
* '/sign-in/magic-link',
|
|
1423
|
+
* '/sign-in/email',
|
|
1424
|
+
* '/forget-password/email-otp',
|
|
1425
|
+
* '/email-otp/reset-password',
|
|
1426
|
+
* '/email-otp/create-verification-otp',
|
|
1427
|
+
* '/email-otp/get-verification-otp',
|
|
1428
|
+
* '/email-otp/send-verification-otp',
|
|
1429
|
+
* '/forget-password',
|
|
1430
|
+
* '/request-password-reset',
|
|
1431
|
+
* '/send-verification-email',
|
|
1432
|
+
* '/change-email'
|
|
1433
|
+
* ]`.
|
|
1434
|
+
* @param context Request context
|
|
1435
|
+
* @param context.path Request path
|
|
1436
|
+
* @returns boolean
|
|
1437
|
+
*/
|
|
1438
|
+
const allEmail = ({ path }) => !!path && all.has(path);
|
|
1439
|
+
//#endregion
|
|
1440
|
+
//#region src/validation/email.ts
|
|
1441
|
+
/**
|
|
1442
|
+
* Gmail-like providers that ignore dots in the local part
|
|
1443
|
+
*/
|
|
1444
|
+
const GMAIL_LIKE_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
|
|
1445
|
+
/**
|
|
1446
|
+
* Providers known to support plus addressing
|
|
1447
|
+
*/
|
|
1448
|
+
const PLUS_ADDRESSING_DOMAINS = new Set([
|
|
1449
|
+
"gmail.com",
|
|
1450
|
+
"googlemail.com",
|
|
1451
|
+
"outlook.com",
|
|
1452
|
+
"hotmail.com",
|
|
1453
|
+
"live.com",
|
|
1454
|
+
"yahoo.com",
|
|
1455
|
+
"icloud.com",
|
|
1456
|
+
"me.com",
|
|
1457
|
+
"mac.com",
|
|
1458
|
+
"protonmail.com",
|
|
1459
|
+
"proton.me",
|
|
1460
|
+
"fastmail.com",
|
|
1461
|
+
"zoho.com"
|
|
1462
|
+
]);
|
|
1463
|
+
/**
|
|
1464
|
+
* Normalize an email address for comparison/deduplication
|
|
1465
|
+
* - Lowercase the entire email
|
|
1466
|
+
* - Remove dots from Gmail-like providers (they ignore dots)
|
|
1467
|
+
* - Remove plus addressing (user+tag@domain → user@domain)
|
|
1468
|
+
* - Normalize googlemail.com to gmail.com
|
|
1469
|
+
*
|
|
1470
|
+
* @param email - Raw email to normalize
|
|
1471
|
+
* @param context - Auth context
|
|
1472
|
+
*/
|
|
1473
|
+
function normalizeEmail(email, context) {
|
|
1474
|
+
if (!email || typeof email !== "string") return email;
|
|
1475
|
+
const mergedOpts = context.options;
|
|
1476
|
+
if (!isEmailNormalizationEnabled(mergedOpts)) return email;
|
|
1477
|
+
const trimmed = email.trim().toLowerCase();
|
|
1478
|
+
const atIndex = trimmed.lastIndexOf("@");
|
|
1479
|
+
if (atIndex === -1) return trimmed;
|
|
1480
|
+
let localPart = trimmed.slice(0, atIndex);
|
|
1481
|
+
let domain = trimmed.slice(atIndex + 1);
|
|
1482
|
+
if (domain === "googlemail.com") domain = "gmail.com";
|
|
1483
|
+
if (PLUS_ADDRESSING_DOMAINS.has(domain)) {
|
|
1484
|
+
const plusIndex = localPart.indexOf("+");
|
|
1485
|
+
if (plusIndex !== -1) localPart = localPart.slice(0, plusIndex);
|
|
1486
|
+
}
|
|
1487
|
+
if (GMAIL_LIKE_DOMAINS.has(domain)) localPart = localPart.replace(/\./g, "");
|
|
1488
|
+
return `${localPart}@${domain}`;
|
|
1489
|
+
}
|
|
1490
|
+
function createEmailValidator(options = {}) {
|
|
1491
|
+
const { apiKey = "", apiUrl = INFRA_API_URL, kvUrl = INFRA_KV_URL, emailValidationOptions = {} } = options;
|
|
1492
|
+
const $api = createFetch({
|
|
1493
|
+
baseURL: apiUrl,
|
|
1494
|
+
headers: { "x-api-key": apiKey }
|
|
1495
|
+
});
|
|
1496
|
+
const $kv = createFetch({
|
|
1497
|
+
baseURL: kvUrl,
|
|
1498
|
+
headers: { "x-api-key": apiKey },
|
|
1499
|
+
timeout: KV_TIMEOUT_MS
|
|
1500
|
+
});
|
|
1501
|
+
/**
|
|
1502
|
+
* Fetch and resolve email validity policy from API with caching
|
|
1503
|
+
* Sends client config to API which merges with user's dashboard settings
|
|
1504
|
+
*/
|
|
1505
|
+
async function fetchPolicy() {
|
|
1506
|
+
try {
|
|
1507
|
+
const { data } = await $api("/security/resolve-policy", {
|
|
1508
|
+
method: "POST",
|
|
1509
|
+
body: {
|
|
1510
|
+
policyId: "email_validity",
|
|
1511
|
+
config: { emailValidation: {
|
|
1512
|
+
enabled: emailValidationOptions.enabled,
|
|
1513
|
+
strictness: emailValidationOptions.strictness,
|
|
1514
|
+
action: emailValidationOptions.action,
|
|
1515
|
+
domainAllowlist: emailValidationOptions.domainAllowlist
|
|
1516
|
+
} }
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
if (data?.policy) return data.policy;
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
logger.warn("[Dash] Failed to fetch email policy, using defaults:", error);
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
return { async validate(email, checkMx = true) {
|
|
1526
|
+
const trimmed = email.trim();
|
|
1527
|
+
const policy = await fetchPolicy();
|
|
1528
|
+
if (!policy?.enabled) return {
|
|
1529
|
+
valid: true,
|
|
1530
|
+
disposable: false,
|
|
1531
|
+
confidence: "high",
|
|
1532
|
+
policy
|
|
1533
|
+
};
|
|
1534
|
+
try {
|
|
1535
|
+
const { data } = await $kv("/email/validate", {
|
|
1536
|
+
method: "POST",
|
|
1537
|
+
body: {
|
|
1538
|
+
email: trimmed,
|
|
1539
|
+
checkMx,
|
|
1540
|
+
strictness: policy.strictness
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
...data || {
|
|
1545
|
+
valid: false,
|
|
1546
|
+
reason: "invalid_format"
|
|
1547
|
+
},
|
|
1548
|
+
policy
|
|
1549
|
+
};
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
logger.warn("[Dash] Email validation API error, falling back to allow:", error);
|
|
1552
|
+
return {
|
|
1553
|
+
valid: true,
|
|
1554
|
+
policy
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
} };
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Basic local email format validation (fallback)
|
|
1561
|
+
*/
|
|
1562
|
+
function isValidEmailFormatLocal(email) {
|
|
1563
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return false;
|
|
1564
|
+
if (email.length > 254) return false;
|
|
1565
|
+
const [localPart, domain] = email.split("@");
|
|
1566
|
+
if (!localPart || !domain) return false;
|
|
1567
|
+
if (localPart.length > 64) return false;
|
|
1568
|
+
if (domain.length > 253) return false;
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
const getEmail = (ctx) => {
|
|
1572
|
+
if (ctx.path === "/change-email") return {
|
|
1573
|
+
email: ctx.body?.newEmail,
|
|
1574
|
+
container: "body",
|
|
1575
|
+
field: "newEmail"
|
|
1576
|
+
};
|
|
1577
|
+
const body = ctx.body;
|
|
1578
|
+
const query = ctx.query;
|
|
1579
|
+
return {
|
|
1580
|
+
email: body?.email ?? query?.email,
|
|
1581
|
+
container: body ? "body" : "query",
|
|
1582
|
+
field: "email"
|
|
1583
|
+
};
|
|
1584
|
+
};
|
|
1585
|
+
/**
|
|
1586
|
+
* Create email normalization hook (shared between all configurations)
|
|
1587
|
+
*/
|
|
1588
|
+
function createEmailNormalizationHook() {
|
|
1589
|
+
return {
|
|
1590
|
+
matcher: allEmail,
|
|
1591
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
1592
|
+
const { email, container, field } = getEmail(ctx);
|
|
1593
|
+
if (typeof email !== "string") return;
|
|
1594
|
+
const normalized = normalizeEmail(email, ctx.context);
|
|
1595
|
+
if (normalized === email) return;
|
|
1596
|
+
const data = container === "query" ? {
|
|
1597
|
+
...ctx.query,
|
|
1598
|
+
[field]: normalized
|
|
1599
|
+
} : {
|
|
1600
|
+
...ctx.body,
|
|
1601
|
+
[field]: normalized
|
|
1602
|
+
};
|
|
1603
|
+
return { context: {
|
|
1604
|
+
...ctx,
|
|
1605
|
+
[container]: data
|
|
1606
|
+
} };
|
|
1607
|
+
})
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Create email validation hook with configurable validation strategy
|
|
1612
|
+
*/
|
|
1613
|
+
function createEmailValidationHook(validator, onDisposableEmail) {
|
|
1614
|
+
return {
|
|
1615
|
+
matcher: allEmail,
|
|
1616
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
1617
|
+
const { email } = getEmail(ctx);
|
|
1618
|
+
if (typeof email !== "string") return;
|
|
1619
|
+
const trimmed = email.trim();
|
|
1620
|
+
if (!isValidEmailFormatLocal(trimmed)) throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
|
|
1621
|
+
if (validator) {
|
|
1622
|
+
const result = await validator.validate(trimmed);
|
|
1623
|
+
const policy = result.policy;
|
|
1624
|
+
if (!policy?.enabled) return;
|
|
1625
|
+
if (policy.domainAllowlist?.length) {
|
|
1626
|
+
const domain = trimmed.toLowerCase().split("@")[1];
|
|
1627
|
+
if (domain && policy.domainAllowlist.includes(domain)) return;
|
|
1628
|
+
}
|
|
1629
|
+
const action = policy.action;
|
|
1630
|
+
if (!result.valid) {
|
|
1631
|
+
if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist" || result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short") && onDisposableEmail) {
|
|
1632
|
+
const ip = ctx.request?.headers?.get("x-forwarded-for")?.split(",")[0] || ctx.request?.headers?.get("cf-connecting-ip") || void 0;
|
|
1633
|
+
onDisposableEmail({
|
|
1634
|
+
email: trimmed,
|
|
1635
|
+
reason: result.reason || "disposable",
|
|
1636
|
+
confidence: result.confidence,
|
|
1637
|
+
ip,
|
|
1638
|
+
path: ctx.path,
|
|
1639
|
+
action
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
if (action === "allow") return;
|
|
1643
|
+
throw new APIError$1("BAD_REQUEST", { message: result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short" ? "This email address appears to be invalid" : "Invalid email" });
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
})
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Create email validation hooks with optional API-backed validation.
|
|
1651
|
+
*
|
|
1652
|
+
* @param options - {@link EmailHooksOptions}
|
|
1653
|
+
*
|
|
1654
|
+
* @example
|
|
1655
|
+
* // Local validation only
|
|
1656
|
+
* createEmailHooks()
|
|
1657
|
+
*
|
|
1658
|
+
* @example
|
|
1659
|
+
* // API-backed validation
|
|
1660
|
+
* createEmailHooks({ useApi: true, apiKey: "your-api-key" })
|
|
1661
|
+
*
|
|
1662
|
+
* @example
|
|
1663
|
+
* // High strictness + API
|
|
1664
|
+
* createEmailHooks({
|
|
1665
|
+
* emailValidationOptions: { strictness: "high" },
|
|
1666
|
+
* useApi: true,
|
|
1667
|
+
* apiKey: "your-api-key",
|
|
1668
|
+
* })
|
|
1669
|
+
*/
|
|
1670
|
+
function createEmailHooks(options = {}) {
|
|
1671
|
+
const { emailValidationOptions, emailNormalizationOptions, useApi = false, apiKey = "", apiUrl = INFRA_API_URL, kvUrl = INFRA_KV_URL, onDisposableEmail } = options;
|
|
1672
|
+
const emailConfig = {
|
|
1673
|
+
enabled: true,
|
|
1674
|
+
strictness: "medium",
|
|
1675
|
+
action: "block",
|
|
1676
|
+
...emailValidationOptions
|
|
1677
|
+
};
|
|
1678
|
+
return { before: [...isEmailNormalizationEnabled({
|
|
1679
|
+
emailValidation: emailValidationOptions,
|
|
1680
|
+
emailNormalization: emailNormalizationOptions
|
|
1681
|
+
}) ? [createEmailNormalizationHook()] : [], ...emailConfig.enabled ? [createEmailValidationHook(useApi ? createEmailValidator({
|
|
1682
|
+
apiUrl,
|
|
1683
|
+
kvUrl,
|
|
1684
|
+
apiKey,
|
|
1685
|
+
emailValidationOptions: emailConfig
|
|
1686
|
+
}) : void 0, onDisposableEmail)] : []] };
|
|
1687
|
+
}
|
|
1688
|
+
//#endregion
|
|
1689
|
+
//#region src/validation/phone.ts
|
|
1690
|
+
/**
|
|
1691
|
+
* Common fake/test phone numbers that should be blocked
|
|
1692
|
+
* These are numbers commonly used in testing, movies, documentation, etc.
|
|
1693
|
+
*/
|
|
1694
|
+
const INVALID_PHONE_NUMBERS = new Set([
|
|
1695
|
+
"+15550000000",
|
|
1696
|
+
"+15550001111",
|
|
1697
|
+
"+15550001234",
|
|
1698
|
+
"+15551234567",
|
|
1699
|
+
"+15555555555",
|
|
1700
|
+
"+15551111111",
|
|
1701
|
+
"+15550000001",
|
|
1702
|
+
"+15550123456",
|
|
1703
|
+
"+12125551234",
|
|
1704
|
+
"+13105551234",
|
|
1705
|
+
"+14155551234",
|
|
1706
|
+
"+12025551234",
|
|
1707
|
+
"+10000000000",
|
|
1708
|
+
"+11111111111",
|
|
1709
|
+
"+12222222222",
|
|
1710
|
+
"+13333333333",
|
|
1711
|
+
"+14444444444",
|
|
1712
|
+
"+15555555555",
|
|
1713
|
+
"+16666666666",
|
|
1714
|
+
"+17777777777",
|
|
1715
|
+
"+18888888888",
|
|
1716
|
+
"+19999999999",
|
|
1717
|
+
"+11234567890",
|
|
1718
|
+
"+10123456789",
|
|
1719
|
+
"+19876543210",
|
|
1720
|
+
"+441632960000",
|
|
1721
|
+
"+447700900000",
|
|
1722
|
+
"+447700900001",
|
|
1723
|
+
"+447700900123",
|
|
1724
|
+
"+447700900999",
|
|
1725
|
+
"+442079460000",
|
|
1726
|
+
"+442079460123",
|
|
1727
|
+
"+441134960000",
|
|
1728
|
+
"+0000000000",
|
|
1729
|
+
"+1000000000",
|
|
1730
|
+
"+123456789",
|
|
1731
|
+
"+1234567890",
|
|
1732
|
+
"+12345678901",
|
|
1733
|
+
"+0123456789",
|
|
1734
|
+
"+9876543210",
|
|
1735
|
+
"+11111111111",
|
|
1736
|
+
"+99999999999",
|
|
1737
|
+
"+491234567890",
|
|
1738
|
+
"+491111111111",
|
|
1739
|
+
"+33123456789",
|
|
1740
|
+
"+33111111111",
|
|
1741
|
+
"+61123456789",
|
|
1742
|
+
"+61111111111",
|
|
1743
|
+
"+81123456789",
|
|
1744
|
+
"+81111111111",
|
|
1745
|
+
"+19001234567",
|
|
1746
|
+
"+19761234567",
|
|
1747
|
+
"+1911",
|
|
1748
|
+
"+1411",
|
|
1749
|
+
"+1611",
|
|
1750
|
+
"+44999",
|
|
1751
|
+
"+44112"
|
|
1752
|
+
]);
|
|
1753
|
+
/**
|
|
1754
|
+
* Patterns that indicate fake/test phone numbers
|
|
1755
|
+
*/
|
|
1756
|
+
const INVALID_PHONE_PATTERNS = [
|
|
1757
|
+
/^\+\d(\d)\1{6,}$/,
|
|
1758
|
+
/^\+\d*1234567890/,
|
|
1759
|
+
/^\+\d*0123456789/,
|
|
1760
|
+
/^\+\d*9876543210/,
|
|
1761
|
+
/^\+\d*0987654321/,
|
|
1762
|
+
/^\+\d*(12){4,}/,
|
|
1763
|
+
/^\+\d*(21){4,}/,
|
|
1764
|
+
/^\+\d*(00){4,}/,
|
|
1765
|
+
/^\+1\d{3}555\d{4}$/,
|
|
1766
|
+
/^\+\d{1,3}\d{1,5}$/,
|
|
1767
|
+
/^\+\d+0{7,}$/,
|
|
1768
|
+
/^\+\d*147258369/,
|
|
1769
|
+
/^\+\d*258369147/,
|
|
1770
|
+
/^\+\d*369258147/,
|
|
1771
|
+
/^\+\d*789456123/,
|
|
1772
|
+
/^\+\d*123456789/,
|
|
1773
|
+
/^\+\d*1234512345/,
|
|
1774
|
+
/^\+\d*1111122222/,
|
|
1775
|
+
/^\+\d*1212121212/,
|
|
1776
|
+
/^\+\d*1010101010/
|
|
1777
|
+
];
|
|
1778
|
+
/**
|
|
1779
|
+
* Invalid area codes / prefixes that indicate test numbers
|
|
1780
|
+
* Key: country code, Value: set of invalid prefixes
|
|
1781
|
+
*/
|
|
1782
|
+
const INVALID_PREFIXES_BY_COUNTRY = {
|
|
1783
|
+
US: new Set([
|
|
1784
|
+
"555",
|
|
1785
|
+
"000",
|
|
1786
|
+
"111",
|
|
1787
|
+
"911",
|
|
1788
|
+
"411",
|
|
1789
|
+
"611"
|
|
1790
|
+
]),
|
|
1791
|
+
CA: new Set([
|
|
1792
|
+
"555",
|
|
1793
|
+
"000",
|
|
1794
|
+
"911"
|
|
1795
|
+
]),
|
|
1796
|
+
GB: new Set([
|
|
1797
|
+
"7700900",
|
|
1798
|
+
"1632960",
|
|
1799
|
+
"1134960"
|
|
1800
|
+
]),
|
|
1801
|
+
AU: new Set([
|
|
1802
|
+
"0491570",
|
|
1803
|
+
"0491571",
|
|
1804
|
+
"0491572"
|
|
1805
|
+
])
|
|
1806
|
+
};
|
|
1807
|
+
/**
|
|
1808
|
+
* Check if a phone number is a commonly used fake/test number
|
|
1809
|
+
* @param phone - The phone number to check (E.164 format preferred)
|
|
1810
|
+
* @param defaultCountry - Default country code if not included in phone string
|
|
1811
|
+
* @returns true if the phone appears to be fake/test, false if it seems legitimate
|
|
1812
|
+
*/
|
|
1813
|
+
const isFakePhoneNumber = (phone, defaultCountry) => {
|
|
1814
|
+
const parsed = parsePhoneNumberFromString(phone, defaultCountry);
|
|
1815
|
+
if (!parsed) return true;
|
|
1816
|
+
const e164 = parsed.number;
|
|
1817
|
+
const nationalNumber = parsed.nationalNumber;
|
|
1818
|
+
const country = parsed.country;
|
|
1819
|
+
if (INVALID_PHONE_NUMBERS.has(e164)) return true;
|
|
1820
|
+
for (const pattern of INVALID_PHONE_PATTERNS) if (pattern.test(e164)) return true;
|
|
1821
|
+
if (country && INVALID_PREFIXES_BY_COUNTRY[country]) {
|
|
1822
|
+
const prefixes = INVALID_PREFIXES_BY_COUNTRY[country];
|
|
1823
|
+
for (const prefix of prefixes) if (nationalNumber.startsWith(prefix)) return true;
|
|
1824
|
+
}
|
|
1825
|
+
if (/^(\d)\1+$/.test(nationalNumber)) return true;
|
|
1826
|
+
const digits = nationalNumber.split("").map(Number);
|
|
1827
|
+
let isSequential = digits.length >= 6;
|
|
1828
|
+
for (let i = 1; i < digits.length && isSequential; i++) {
|
|
1829
|
+
const current = digits[i];
|
|
1830
|
+
const previous = digits[i - 1];
|
|
1831
|
+
if (current === void 0 || previous === void 0 || current !== previous + 1 && current !== previous - 1) isSequential = false;
|
|
1832
|
+
}
|
|
1833
|
+
if (isSequential) return true;
|
|
1834
|
+
return false;
|
|
1835
|
+
};
|
|
1836
|
+
/**
|
|
1837
|
+
* Validate a phone number format
|
|
1838
|
+
* @param phone - The phone number to validate
|
|
1839
|
+
* @param defaultCountry - Default country code if not included in phone string
|
|
1840
|
+
* @returns true if the phone number is valid
|
|
1841
|
+
*/
|
|
1842
|
+
const isValidPhone = (phone, defaultCountry) => {
|
|
1843
|
+
return isValidPhoneNumber(phone, defaultCountry);
|
|
1844
|
+
};
|
|
1845
|
+
/**
|
|
1846
|
+
* Comprehensive phone number validation
|
|
1847
|
+
* @param phone - The phone number to validate
|
|
1848
|
+
* @param options - Validation options
|
|
1849
|
+
* @returns true if valid, false otherwise
|
|
1850
|
+
*/
|
|
1851
|
+
const validatePhone = (phone, options = {}) => {
|
|
1852
|
+
const { mobileOnly = false, allowedCountries, blockedCountries, blockFakeNumbers = true, blockPremiumRate = true, blockTollFree = false, blockVoip = false, defaultCountry } = options;
|
|
1853
|
+
if (!isValidPhone(phone, defaultCountry)) return false;
|
|
1854
|
+
const parsed = parsePhoneNumberFromString(phone, defaultCountry);
|
|
1855
|
+
if (!parsed) return false;
|
|
1856
|
+
if (blockFakeNumbers && isFakePhoneNumber(phone, defaultCountry)) return false;
|
|
1857
|
+
const country = parsed.country;
|
|
1858
|
+
if (country) {
|
|
1859
|
+
if (allowedCountries && !allowedCountries.includes(country)) return false;
|
|
1860
|
+
if (blockedCountries?.includes(country)) return false;
|
|
1861
|
+
}
|
|
1862
|
+
const phoneType = parsed.getType();
|
|
1863
|
+
if (mobileOnly) {
|
|
1864
|
+
if (phoneType !== "MOBILE" && phoneType !== "FIXED_LINE_OR_MOBILE") return false;
|
|
1865
|
+
}
|
|
1866
|
+
if (blockPremiumRate && phoneType === "PREMIUM_RATE") return false;
|
|
1867
|
+
if (blockTollFree && phoneType === "TOLL_FREE") return false;
|
|
1868
|
+
if (blockVoip && phoneType === "VOIP") return false;
|
|
1869
|
+
return true;
|
|
1870
|
+
};
|
|
1871
|
+
const allPhonePaths = new Set([
|
|
1872
|
+
"/phone-number/send-otp",
|
|
1873
|
+
"/phone-number/verify",
|
|
1874
|
+
"/sign-in/phone-number",
|
|
1875
|
+
"/phone-number/request-password-reset",
|
|
1876
|
+
"/phone-number/reset-password"
|
|
1877
|
+
]);
|
|
1878
|
+
const getPhoneNumber = (ctx) => ctx.body?.phoneNumber ?? ctx.query?.phoneNumber;
|
|
1879
|
+
/**
|
|
1880
|
+
* Better Auth plugin for phone number validation
|
|
1881
|
+
* Validates phone numbers on all phone-related endpoints
|
|
1882
|
+
*/
|
|
1883
|
+
const phoneValidationHooks = { before: [{
|
|
1884
|
+
matcher: (context) => !!context.path && allPhonePaths.has(context.path),
|
|
1885
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
1886
|
+
const phoneNumber = getPhoneNumber(ctx);
|
|
1887
|
+
if (typeof phoneNumber !== "string") return;
|
|
1888
|
+
if (!validatePhone(phoneNumber)) throw new APIError$1("BAD_REQUEST", { message: "Invalid phone number" });
|
|
1889
|
+
})
|
|
1890
|
+
}] };
|
|
1891
|
+
//#endregion
|
|
1886
1892
|
//#region src/sentinel/security-hooks.ts
|
|
1887
1893
|
const ERROR_MESSAGES = {
|
|
1888
1894
|
geo_blocked: "Access from your location is not allowed.",
|
|
@@ -2024,11 +2030,12 @@ const sentinel = (options) => {
|
|
|
2024
2030
|
});
|
|
2025
2031
|
});
|
|
2026
2032
|
const emailHooks = createEmailHooks({
|
|
2033
|
+
emailValidationOptions: opts.security?.emailValidation,
|
|
2034
|
+
emailNormalizationOptions: opts.security?.emailNormalization,
|
|
2027
2035
|
useApi: !!opts.apiKey,
|
|
2028
2036
|
apiKey: opts.apiKey,
|
|
2029
2037
|
apiUrl: opts.apiUrl,
|
|
2030
2038
|
kvUrl: opts.kvUrl,
|
|
2031
|
-
defaultConfig: opts.security?.emailValidation,
|
|
2032
2039
|
onDisposableEmail: (data) => {
|
|
2033
2040
|
const isNoMxRecord = data.reason === "no_mx_records";
|
|
2034
2041
|
const reason = isNoMxRecord ? "no_mx_records" : "disposable_email";
|
|
@@ -2061,6 +2068,7 @@ const sentinel = (options) => {
|
|
|
2061
2068
|
const activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
|
|
2062
2069
|
return { options: {
|
|
2063
2070
|
emailValidation: opts.security?.emailValidation,
|
|
2071
|
+
emailNormalization: opts.security?.emailNormalization,
|
|
2064
2072
|
databaseHooks: {
|
|
2065
2073
|
user: { create: {
|
|
2066
2074
|
async before(user, ctx) {
|
|
@@ -2070,7 +2078,7 @@ const sentinel = (options) => {
|
|
|
2070
2078
|
const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
|
|
2071
2079
|
if (abuseCheck.isAbuse && abuseCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
|
|
2072
2080
|
}
|
|
2073
|
-
if (user.email && typeof user.email === "string" && opts.security
|
|
2081
|
+
if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
|
|
2074
2082
|
...user,
|
|
2075
2083
|
email: normalizeEmail(user.email, ctx.context)
|
|
2076
2084
|
} };
|
|
@@ -2746,10 +2754,76 @@ const jwtValidateMiddleware = (options) => createAuthMiddleware(async (ctx) => {
|
|
|
2746
2754
|
});
|
|
2747
2755
|
//#endregion
|
|
2748
2756
|
//#region src/version.ts
|
|
2749
|
-
const PLUGIN_VERSION = "0.2.
|
|
2757
|
+
const PLUGIN_VERSION = "0.2.4";
|
|
2750
2758
|
//#endregion
|
|
2751
2759
|
//#region src/routes/auth/config.ts
|
|
2752
2760
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
2761
|
+
const EXACT_SENSITIVE_STRING_KEYS_LOWER = new Set([...new Set([
|
|
2762
|
+
"accessToken",
|
|
2763
|
+
"apiKey",
|
|
2764
|
+
"apiSecret",
|
|
2765
|
+
"authorization",
|
|
2766
|
+
"authToken",
|
|
2767
|
+
"bearerToken",
|
|
2768
|
+
"clientSecret",
|
|
2769
|
+
"consumerSecret",
|
|
2770
|
+
"encPrivateKey",
|
|
2771
|
+
"encPrivateKeyPass",
|
|
2772
|
+
"encryptionKey",
|
|
2773
|
+
"encryptionSecret",
|
|
2774
|
+
"idToken",
|
|
2775
|
+
"password",
|
|
2776
|
+
"privateKey",
|
|
2777
|
+
"privateKeyPass",
|
|
2778
|
+
"refreshToken",
|
|
2779
|
+
"secret",
|
|
2780
|
+
"secretKey",
|
|
2781
|
+
"signingSecret",
|
|
2782
|
+
"stripeWebhookSecret",
|
|
2783
|
+
"webhookSecret"
|
|
2784
|
+
])].map((k) => k.toLowerCase()));
|
|
2785
|
+
/**
|
|
2786
|
+
* Case-insensitive suffixes for unknown plugin options
|
|
2787
|
+
* Matching is more permissive to account for unknown plugin options
|
|
2788
|
+
*/
|
|
2789
|
+
const SENSITIVE_KEY_SUFFIXES_LOWER = [
|
|
2790
|
+
"secret",
|
|
2791
|
+
"password",
|
|
2792
|
+
"passphrase",
|
|
2793
|
+
"privatekey",
|
|
2794
|
+
"keypass",
|
|
2795
|
+
"apikey",
|
|
2796
|
+
"token",
|
|
2797
|
+
"signingkey",
|
|
2798
|
+
"credentials",
|
|
2799
|
+
"authheader"
|
|
2800
|
+
];
|
|
2801
|
+
const REDACTED_SIMPLE_STRING = "[REDACTED]";
|
|
2802
|
+
function snakeCaseToCamelCase(key) {
|
|
2803
|
+
return key.replace(/_([a-zA-Z])/g, (_, ch) => ch.toUpperCase());
|
|
2804
|
+
}
|
|
2805
|
+
function keyVariantsForMatching(key) {
|
|
2806
|
+
const trimmed = key.replace(/^[\s._-]+/, "");
|
|
2807
|
+
if (!trimmed) return [key];
|
|
2808
|
+
const out = new Set([key, trimmed]);
|
|
2809
|
+
if (trimmed.includes("_")) out.add(snakeCaseToCamelCase(trimmed));
|
|
2810
|
+
return [...out];
|
|
2811
|
+
}
|
|
2812
|
+
function shouldRedactSensitiveStringKey(key) {
|
|
2813
|
+
const variants = keyVariantsForMatching(key);
|
|
2814
|
+
const compactLower = key.replace(/[\s._-]/g, "").toLowerCase();
|
|
2815
|
+
const forms = new Set([compactLower]);
|
|
2816
|
+
for (const raw of variants) {
|
|
2817
|
+
forms.add(raw);
|
|
2818
|
+
forms.add(raw.toLowerCase());
|
|
2819
|
+
}
|
|
2820
|
+
for (const form of forms) {
|
|
2821
|
+
const fl = form.toLowerCase();
|
|
2822
|
+
if (EXACT_SENSITIVE_STRING_KEYS_LOWER.has(fl)) return true;
|
|
2823
|
+
for (const suffix of SENSITIVE_KEY_SUFFIXES_LOWER) if (fl.endsWith(suffix)) return true;
|
|
2824
|
+
}
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2753
2827
|
function isPlainSerializable(value) {
|
|
2754
2828
|
if (value === null || typeof value !== "object") return true;
|
|
2755
2829
|
if (Array.isArray(value)) return true;
|
|
@@ -2758,22 +2832,28 @@ function isPlainSerializable(value) {
|
|
|
2758
2832
|
if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
|
|
2759
2833
|
return true;
|
|
2760
2834
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2835
|
+
/**
|
|
2836
|
+
* Sanitize any plain JSON-like subtree from Better Auth options
|
|
2837
|
+
*/
|
|
2838
|
+
function sanitizeConfig(value, visiting, excludeKeys) {
|
|
2839
|
+
if (value === null || value === void 0) return value;
|
|
2840
|
+
if (typeof value === "function") return void 0;
|
|
2841
|
+
if (typeof value !== "object") return value;
|
|
2842
|
+
const obj = value;
|
|
2766
2843
|
if (visiting.has(obj)) return void 0;
|
|
2767
2844
|
visiting.add(obj);
|
|
2768
2845
|
try {
|
|
2769
|
-
|
|
2770
|
-
if (Array.isArray(options)) return options.map((item) => sanitizePluginOptions(pluginId, item, visiting)).filter((item) => item !== void 0);
|
|
2846
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeConfig(item, visiting, excludeKeys)).filter((item) => item !== void 0);
|
|
2771
2847
|
const result = {};
|
|
2772
|
-
for (const [key,
|
|
2848
|
+
for (const [key, val] of Object.entries(value)) {
|
|
2773
2849
|
if (excludeKeys?.has(key)) continue;
|
|
2774
|
-
if (typeof
|
|
2775
|
-
if (
|
|
2776
|
-
|
|
2850
|
+
if (typeof val === "function") continue;
|
|
2851
|
+
if (typeof val === "string" && shouldRedactSensitiveStringKey(key)) {
|
|
2852
|
+
result[key] = REDACTED_SIMPLE_STRING;
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
if (val !== null && typeof val === "object" && !isPlainSerializable(val)) continue;
|
|
2856
|
+
const sanitized = sanitizeConfig(val, visiting, excludeKeys);
|
|
2777
2857
|
if (sanitized !== void 0) result[key] = sanitized;
|
|
2778
2858
|
}
|
|
2779
2859
|
return result;
|
|
@@ -2781,6 +2861,9 @@ function sanitizePluginOptions(pluginId, options, visiting = /* @__PURE__ */ new
|
|
|
2781
2861
|
visiting.delete(obj);
|
|
2782
2862
|
}
|
|
2783
2863
|
}
|
|
2864
|
+
function sanitizePluginOptions(pluginId, options) {
|
|
2865
|
+
return sanitizeConfig(options, /* @__PURE__ */ new WeakSet(), PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId]);
|
|
2866
|
+
}
|
|
2784
2867
|
function estimateEntropy(str) {
|
|
2785
2868
|
const unique = new Set(str).size;
|
|
2786
2869
|
if (unique === 0) return 0;
|
|
@@ -2796,7 +2879,7 @@ const getConfig = (options) => {
|
|
|
2796
2879
|
return {
|
|
2797
2880
|
version: ctx.context.version || null,
|
|
2798
2881
|
socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
|
|
2799
|
-
emailAndPassword: ctx.context.options.emailAndPassword,
|
|
2882
|
+
emailAndPassword: sanitizeConfig(ctx.context.options.emailAndPassword, /* @__PURE__ */ new WeakSet(), void 0),
|
|
2800
2883
|
plugins: ctx.context.options.plugins?.map((plugin) => {
|
|
2801
2884
|
const base = {
|
|
2802
2885
|
id: plugin.id,
|
|
@@ -5413,11 +5496,14 @@ function getSSOPlugin(ctx) {
|
|
|
5413
5496
|
const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
|
|
5414
5497
|
const RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
|
|
5415
5498
|
const SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1";
|
|
5499
|
+
/** @public */
|
|
5416
5500
|
const DEPRECATED_SIGNATURE_ALGORITHMS = [RSA_SHA1];
|
|
5501
|
+
/** @public */
|
|
5417
5502
|
const DEPRECATED_DIGEST_ALGORITHMS = [SHA1];
|
|
5418
5503
|
function validateSAMLMetadataSize(metadataXml, maxSize = DEFAULT_MAX_SAML_METADATA_SIZE) {
|
|
5419
5504
|
if (new TextEncoder().encode(metadataXml).byteLength > maxSize) throw new Error(`IdP metadata exceeds maximum allowed size (${Math.round(maxSize / 1024)}KB)`);
|
|
5420
5505
|
}
|
|
5506
|
+
/** @public */
|
|
5421
5507
|
function extractAlgorithmsFromSAMLMetadata(metadataXml) {
|
|
5422
5508
|
const signatureAlgorithms = [];
|
|
5423
5509
|
const digestAlgorithms = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/infra",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Dashboard and analytics plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"@types/node": "catalog:",
|
|
69
69
|
"better-auth": "catalog:",
|
|
70
70
|
"expo-crypto": "^14.0.2",
|
|
71
|
-
"happy-dom": "^20.
|
|
72
|
-
"msw": "^2.13.
|
|
73
|
-
"tsdown": "^0.21.
|
|
71
|
+
"happy-dom": "^20.9.0",
|
|
72
|
+
"msw": "^2.13.3",
|
|
73
|
+
"tsdown": "^0.21.9",
|
|
74
74
|
"typescript": "catalog:",
|
|
75
75
|
"zod": "catalog:"
|
|
76
76
|
},
|