@better-auth/infra 0.3.2 → 0.3.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/CHANGELOG.md +27 -0
- package/dist/client.mjs +2 -2
- package/dist/{constants-9UwOSy9A.mjs → constants-D81vWTXm.mjs} +1 -1
- package/dist/{crypto-B1NYwfFV.mjs → crypto-BlYEkWgT.mjs} +1 -1
- package/dist/email.mjs +1 -1
- package/dist/index.d.mts +21 -5193
- package/dist/index.mjs +224 -80
- package/dist/native.mjs +2 -2
- package/dist/{pow-retry-BfzBUq0O.mjs → pow-retry-BN8NVM3m.mjs} +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-
|
|
2
|
-
import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-D81vWTXm.mjs";
|
|
2
|
+
import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-BlYEkWgT.mjs";
|
|
3
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
4
|
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
5
5
|
import { APIError, generateId, getAuthTables, logger } from "better-auth";
|
|
@@ -9,7 +9,7 @@ import { createFetch } from "@better-fetch/fetch";
|
|
|
9
9
|
import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
|
|
10
10
|
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
11
11
|
import z$1, { z } from "zod";
|
|
12
|
-
import { setSessionCookie } from "better-auth/cookies";
|
|
12
|
+
import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
|
|
13
13
|
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
|
|
14
14
|
import { createOTP } from "@better-auth/utils/otp";
|
|
15
15
|
//#region src/options.ts
|
|
@@ -1700,7 +1700,7 @@ function createEmailValidator(options) {
|
|
|
1700
1700
|
* Validate an email address
|
|
1701
1701
|
* Returns validation result - caller should use getPolicy() to determine action
|
|
1702
1702
|
*/
|
|
1703
|
-
async validate(email
|
|
1703
|
+
async validate(email) {
|
|
1704
1704
|
const trimmed = email.trim();
|
|
1705
1705
|
const policy = await fetchPolicy();
|
|
1706
1706
|
if (!policy?.enabled) return {
|
|
@@ -1714,7 +1714,6 @@ async validate(email, checkMx = true) {
|
|
|
1714
1714
|
method: "POST",
|
|
1715
1715
|
body: {
|
|
1716
1716
|
email: trimmed,
|
|
1717
|
-
checkMx,
|
|
1718
1717
|
strictness: policy.strictness
|
|
1719
1718
|
}
|
|
1720
1719
|
});
|
|
@@ -3348,6 +3347,18 @@ async function getScimProviderOwner(ctx, organizationId, providerId) {
|
|
|
3348
3347
|
select: ["userId"]
|
|
3349
3348
|
}))?.userId ?? null;
|
|
3350
3349
|
}
|
|
3350
|
+
async function scimProviderExists(ctx, organizationId, providerId) {
|
|
3351
|
+
return await ctx.context.adapter.findOne({
|
|
3352
|
+
model: "scimProvider",
|
|
3353
|
+
where: [{
|
|
3354
|
+
field: "organizationId",
|
|
3355
|
+
value: organizationId
|
|
3356
|
+
}, {
|
|
3357
|
+
field: "providerId",
|
|
3358
|
+
value: providerId
|
|
3359
|
+
}]
|
|
3360
|
+
}) != null;
|
|
3361
|
+
}
|
|
3351
3362
|
function getScimEndpoint(baseUrl) {
|
|
3352
3363
|
return `${baseUrl}/scim/v2`;
|
|
3353
3364
|
}
|
|
@@ -3415,6 +3426,7 @@ const createOrganizationDirectory = (options) => {
|
|
|
3415
3426
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3416
3427
|
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" });
|
|
3417
3428
|
const { providerId, ownerUserId } = ctx.body;
|
|
3429
|
+
if (await scimProviderExists(ctx, organizationId, providerId)) throw ctx.error("BAD_REQUEST", { message: DIRECTORY_SYNC_DUPLICATE_MESSAGE });
|
|
3418
3430
|
let scimToken;
|
|
3419
3431
|
try {
|
|
3420
3432
|
scimToken = (await scimPlugin.endpoints.generateSCIMToken({
|
|
@@ -3895,6 +3907,61 @@ function usesPlatformInviteAcceptFlow(authMode) {
|
|
|
3895
3907
|
function shouldCreateSessionOnInviteComplete(authMode) {
|
|
3896
3908
|
return authMode !== "create_no_session";
|
|
3897
3909
|
}
|
|
3910
|
+
/** Password/social completion on /invite/accept (not direct_redirect). */
|
|
3911
|
+
function canCompleteViaPlatformAuth(authMode) {
|
|
3912
|
+
return usesPlatformInviteAcceptFlow(authMode);
|
|
3913
|
+
}
|
|
3914
|
+
/** Whether complete-invitation may create a new user without a password. */
|
|
3915
|
+
function allowsPasswordlessInviteComplete(authMode) {
|
|
3916
|
+
return authMode === "create_with_session";
|
|
3917
|
+
}
|
|
3918
|
+
//#endregion
|
|
3919
|
+
//#region ../utils/dist/url.mjs
|
|
3920
|
+
/**
|
|
3921
|
+
* URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
|
|
3922
|
+
*/
|
|
3923
|
+
const HTTP_PROTOCOLS = ["http:", "https:"];
|
|
3924
|
+
function isAllowedProtocol(protocol, allowedProtocols) {
|
|
3925
|
+
return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
|
|
3926
|
+
}
|
|
3927
|
+
function toAllowedOriginSet(allowedOrigins) {
|
|
3928
|
+
const set = /* @__PURE__ */ new Set();
|
|
3929
|
+
for (const raw of allowedOrigins) try {
|
|
3930
|
+
const parsed = new URL(raw.trim());
|
|
3931
|
+
if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
|
|
3932
|
+
} catch {}
|
|
3933
|
+
return set;
|
|
3934
|
+
}
|
|
3935
|
+
function isAllowedOrigin(origin, allowedOrigins) {
|
|
3936
|
+
if (!allowedOrigins.length) return false;
|
|
3937
|
+
return toAllowedOriginSet(allowedOrigins).has(origin);
|
|
3938
|
+
}
|
|
3939
|
+
function safeResolveUrl(raw, baseURL) {
|
|
3940
|
+
const trim = raw.trim();
|
|
3941
|
+
if (!trim) return null;
|
|
3942
|
+
if (trim.startsWith("//") || trim.includes("\\")) return null;
|
|
3943
|
+
if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
|
|
3944
|
+
try {
|
|
3945
|
+
if (trim.startsWith("/")) return new URL(trim, baseURL);
|
|
3946
|
+
return new URL(trim);
|
|
3947
|
+
} catch {
|
|
3948
|
+
return null;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
function getSafeUniqueOrigins(urls) {
|
|
3952
|
+
return [...toAllowedOriginSet(urls)];
|
|
3953
|
+
}
|
|
3954
|
+
/**
|
|
3955
|
+
* Returns an absolute URL only if its protocol and origin are allowed (after
|
|
3956
|
+
* resolving relative paths against `baseURL`). Otherwise returns `null`.
|
|
3957
|
+
*/
|
|
3958
|
+
function safeUrlParse(input) {
|
|
3959
|
+
if (input.raw == null || input.raw.trim() === "") return null;
|
|
3960
|
+
const target = safeResolveUrl(input.raw, input.baseURL);
|
|
3961
|
+
if (!target || !isAllowedProtocol(target.protocol, input.allowedProtocols)) return null;
|
|
3962
|
+
if (!isAllowedOrigin(target.origin, input.allowedOrigins ?? [])) return null;
|
|
3963
|
+
return target.href;
|
|
3964
|
+
}
|
|
3898
3965
|
//#endregion
|
|
3899
3966
|
//#region src/lib/safe-url.ts
|
|
3900
3967
|
function isSafeHttpUrl(value) {
|
|
@@ -3908,12 +3975,44 @@ function isSafeHttpUrl(value) {
|
|
|
3908
3975
|
/** http(s) absolute URL without embedded credentials. */
|
|
3909
3976
|
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
3977
|
const optionalSafeUrlSchema = z$1.union([safeUrlSchema, z$1.literal("")]).optional();
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3978
|
+
function getAuthBaseUrl(ctx) {
|
|
3979
|
+
const baseURL = ctx.context.options.baseURL;
|
|
3980
|
+
return typeof baseURL === "string" && baseURL.trim() ? baseURL : "/";
|
|
3981
|
+
}
|
|
3982
|
+
function getAllowedAuthOrigins(ctx, extraOrigins = []) {
|
|
3983
|
+
const baseURL = getAuthBaseUrl(ctx);
|
|
3984
|
+
const trusted = ctx.context.options.trustedOrigins;
|
|
3985
|
+
return getSafeUniqueOrigins([
|
|
3986
|
+
baseURL,
|
|
3987
|
+
...Array.isArray(trusted) ? trusted.filter((origin) => typeof origin === "string") : [],
|
|
3988
|
+
...extraOrigins
|
|
3989
|
+
]);
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Resolve a redirect against the auth server's base URL and trusted origins.
|
|
3993
|
+
* Falls back when the target origin is not allowed.
|
|
3994
|
+
*/
|
|
3995
|
+
function resolveTrustedAuthRedirectUrl(ctx, raw, fallback) {
|
|
3996
|
+
return safeUrlParse({
|
|
3997
|
+
raw,
|
|
3998
|
+
baseURL: getAuthBaseUrl(ctx),
|
|
3999
|
+
allowedOrigins: getAllowedAuthOrigins(ctx)
|
|
4000
|
+
}) ?? fallback;
|
|
4001
|
+
}
|
|
4002
|
+
/** Returns a trusted redirect URL or null when the origin is not allowed. */
|
|
4003
|
+
function parseTrustedAuthRedirectUrl(ctx, raw) {
|
|
4004
|
+
return safeUrlParse({
|
|
4005
|
+
raw,
|
|
4006
|
+
baseURL: getAuthBaseUrl(ctx),
|
|
4007
|
+
allowedOrigins: getAllowedAuthOrigins(ctx)
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
const TRUSTED_CALLBACK_URL_ERROR = "Callback URL must use http(s) and match the auth base URL or a trusted origin.";
|
|
4011
|
+
/** Rejects email/redirect callbacks whose origin is not on the auth allowlist. */
|
|
4012
|
+
function requireTrustedAuthCallbackUrl(ctx, raw) {
|
|
4013
|
+
const trusted = parseTrustedAuthRedirectUrl(ctx, raw);
|
|
4014
|
+
if (!trusted) throw new APIError$1("BAD_REQUEST", { message: TRUSTED_CALLBACK_URL_ERROR });
|
|
4015
|
+
return trusted;
|
|
3917
4016
|
}
|
|
3918
4017
|
//#endregion
|
|
3919
4018
|
//#region src/routes/invitations/index.ts
|
|
@@ -3939,11 +4038,56 @@ async function verifyPendingInvitation(token, $api, ctx) {
|
|
|
3939
4038
|
}
|
|
3940
4039
|
return invitation;
|
|
3941
4040
|
}
|
|
3942
|
-
function
|
|
3943
|
-
|
|
3944
|
-
}
|
|
3945
|
-
|
|
3946
|
-
|
|
4041
|
+
async function executePlatformInvitationCompletion(ctx, $api, invitation, args) {
|
|
4042
|
+
if (!canCompleteViaPlatformAuth(invitation.authMode)) throw new APIError("BAD_REQUEST", { message: "This invitation cannot be completed through this endpoint." });
|
|
4043
|
+
if (!ctx.context) throw new APIError("BAD_REQUEST", { message: "Context is required" });
|
|
4044
|
+
const { token, password } = args;
|
|
4045
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
4046
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user) => user?.user);
|
|
4047
|
+
if (existingUser) {
|
|
4048
|
+
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
4049
|
+
method: "POST",
|
|
4050
|
+
body: {
|
|
4051
|
+
token,
|
|
4052
|
+
userId: existingUser.id
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
4056
|
+
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
4057
|
+
session: await ctx.context.internalAdapter.createSession(existingUser.id),
|
|
4058
|
+
user: existingUser
|
|
4059
|
+
});
|
|
4060
|
+
return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
|
|
4061
|
+
}
|
|
4062
|
+
if (!password && !allowsPasswordlessInviteComplete(invitation.authMode)) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
|
|
4063
|
+
const userPayload = {
|
|
4064
|
+
email: invitationEmail,
|
|
4065
|
+
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
4066
|
+
emailVerified: true,
|
|
4067
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
4068
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4069
|
+
};
|
|
4070
|
+
const adapter = ctx.context.internalAdapter;
|
|
4071
|
+
const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
|
|
4072
|
+
if (password) await ctx.context.internalAdapter.createAccount({
|
|
4073
|
+
userId: user.id,
|
|
4074
|
+
providerId: "credential",
|
|
4075
|
+
accountId: user.id,
|
|
4076
|
+
password: await ctx.context.password.hash(password)
|
|
4077
|
+
});
|
|
4078
|
+
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
4079
|
+
method: "POST",
|
|
4080
|
+
body: {
|
|
4081
|
+
token,
|
|
4082
|
+
userId: user.id
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
4086
|
+
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
4087
|
+
session: await ctx.context.internalAdapter.createSession(user.id),
|
|
4088
|
+
user
|
|
4089
|
+
});
|
|
4090
|
+
return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
|
|
3947
4091
|
}
|
|
3948
4092
|
/**
|
|
3949
4093
|
* Accept invitation endpoint
|
|
@@ -3973,7 +4117,7 @@ const acceptInvitation = (options) => {
|
|
|
3973
4117
|
session: await ctx.context.internalAdapter.createSession(existingUser.id),
|
|
3974
4118
|
user: existingUser
|
|
3975
4119
|
});
|
|
3976
|
-
const redirectUrl =
|
|
4120
|
+
const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
|
|
3977
4121
|
return ctx.redirect(redirectUrl);
|
|
3978
4122
|
}
|
|
3979
4123
|
if (usesPlatformInviteAcceptFlow(invitation.authMode)) {
|
|
@@ -4005,7 +4149,7 @@ const acceptInvitation = (options) => {
|
|
|
4005
4149
|
session: await ctx.context.internalAdapter.createSession(user.id),
|
|
4006
4150
|
user
|
|
4007
4151
|
});
|
|
4008
|
-
const redirectUrl =
|
|
4152
|
+
const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
|
|
4009
4153
|
return ctx.redirect(redirectUrl);
|
|
4010
4154
|
});
|
|
4011
4155
|
};
|
|
@@ -4024,62 +4168,38 @@ const completeInvitation = (options) => {
|
|
|
4024
4168
|
})
|
|
4025
4169
|
}, async (ctx) => {
|
|
4026
4170
|
const { token, password } = ctx.body;
|
|
4027
|
-
const
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user) => user?.user);
|
|
4031
|
-
if (existingUser) {
|
|
4032
|
-
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
4033
|
-
method: "POST",
|
|
4034
|
-
body: {
|
|
4035
|
-
token,
|
|
4036
|
-
userId: existingUser.id
|
|
4037
|
-
}
|
|
4038
|
-
});
|
|
4039
|
-
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
4040
|
-
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
4041
|
-
session: await ctx.context.internalAdapter.createSession(existingUser.id),
|
|
4042
|
-
user: existingUser
|
|
4043
|
-
});
|
|
4044
|
-
return {
|
|
4045
|
-
success: true,
|
|
4046
|
-
redirectUrl: resolveInvitationRedirect(ctx, invitation.redirectUrl)
|
|
4047
|
-
};
|
|
4048
|
-
}
|
|
4049
|
-
if (!password) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
|
|
4050
|
-
const userPayload = {
|
|
4051
|
-
email: invitationEmail,
|
|
4052
|
-
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
4053
|
-
emailVerified: true,
|
|
4054
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
4055
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
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);
|
|
4059
|
-
await ctx.context.internalAdapter.createAccount({
|
|
4060
|
-
userId: user.id,
|
|
4061
|
-
providerId: "credential",
|
|
4062
|
-
accountId: user.id,
|
|
4063
|
-
password: await ctx.context.password.hash(password)
|
|
4064
|
-
});
|
|
4065
|
-
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
4066
|
-
method: "POST",
|
|
4067
|
-
body: {
|
|
4068
|
-
token,
|
|
4069
|
-
userId: user.id
|
|
4070
|
-
}
|
|
4071
|
-
});
|
|
4072
|
-
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
4073
|
-
if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
|
|
4074
|
-
session: await ctx.context.internalAdapter.createSession(user.id),
|
|
4075
|
-
user
|
|
4171
|
+
const { redirectUrl } = await executePlatformInvitationCompletion(ctx, $api, await verifyPendingInvitation(token, $api, ctx), {
|
|
4172
|
+
token,
|
|
4173
|
+
password
|
|
4076
4174
|
});
|
|
4077
4175
|
return {
|
|
4078
4176
|
success: true,
|
|
4079
|
-
redirectUrl
|
|
4177
|
+
redirectUrl
|
|
4080
4178
|
};
|
|
4081
4179
|
});
|
|
4082
4180
|
};
|
|
4181
|
+
/**
|
|
4182
|
+
* Browser handoff completion — runs on the auth server so Set-Cookie reaches the invitee.
|
|
4183
|
+
* The platform stores a one-time handoff after validating the invitation (and password).
|
|
4184
|
+
*/
|
|
4185
|
+
const completeInvitationHandoff = (options) => {
|
|
4186
|
+
const { $api } = options;
|
|
4187
|
+
return createAuthEndpoint("/dash/complete-invitation-handoff", {
|
|
4188
|
+
method: "GET",
|
|
4189
|
+
query: z$1.object({ handoff: z$1.string().min(1) })
|
|
4190
|
+
}, async (ctx) => {
|
|
4191
|
+
const { data, error } = await $api("/api/internal/invitations/redeem-handoff", {
|
|
4192
|
+
method: "POST",
|
|
4193
|
+
body: { handoff: ctx.query.handoff }
|
|
4194
|
+
});
|
|
4195
|
+
if (error || !data?.invitationToken) throw new APIError("BAD_REQUEST", { message: "This invitation link has expired. Please try again." });
|
|
4196
|
+
const { redirectUrl } = await executePlatformInvitationCompletion(ctx, $api, await verifyPendingInvitation(data.invitationToken, $api, ctx), {
|
|
4197
|
+
token: data.invitationToken,
|
|
4198
|
+
password: data.password ?? void 0
|
|
4199
|
+
});
|
|
4200
|
+
return ctx.redirect(redirectUrl);
|
|
4201
|
+
});
|
|
4202
|
+
};
|
|
4083
4203
|
const completeInvitationSocial = (options) => {
|
|
4084
4204
|
const { $api } = options;
|
|
4085
4205
|
return createAuthEndpoint("/dash/complete-invitation-social", {
|
|
@@ -4090,6 +4210,7 @@ const completeInvitationSocial = (options) => {
|
|
|
4090
4210
|
const sessionUser = ctx.context.session?.user;
|
|
4091
4211
|
if (!sessionUser) throw new APIError("UNAUTHORIZED", { message: "Authentication required." });
|
|
4092
4212
|
const invitation = await verifyPendingInvitation(ctx.query.token, $api, ctx);
|
|
4213
|
+
if (!canCompleteViaPlatformAuth(invitation.authMode)) throw new APIError("BAD_REQUEST", { message: "This invitation cannot be completed through this endpoint." });
|
|
4093
4214
|
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
4094
4215
|
const sessionEmail = sessionUser.email ? normalizeEmail(sessionUser.email, ctx.context) : null;
|
|
4095
4216
|
if (!sessionEmail || sessionEmail !== invitationEmail) throw new APIError("BAD_REQUEST", { message: "Signed-in account does not match this invitation." });
|
|
@@ -4101,7 +4222,12 @@ const completeInvitationSocial = (options) => {
|
|
|
4101
4222
|
}
|
|
4102
4223
|
});
|
|
4103
4224
|
if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
|
|
4104
|
-
|
|
4225
|
+
if (!shouldCreateSessionOnInviteComplete(invitation.authMode)) {
|
|
4226
|
+
const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
|
|
4227
|
+
if (sessionCookieToken) await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
|
4228
|
+
deleteSessionCookie(ctx);
|
|
4229
|
+
}
|
|
4230
|
+
return ctx.redirect(resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)));
|
|
4105
4231
|
});
|
|
4106
4232
|
};
|
|
4107
4233
|
/**
|
|
@@ -6532,14 +6658,14 @@ const markSsoProviderDomainVerified = (options) => {
|
|
|
6532
6658
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
|
|
6533
6659
|
body: z$1.object({
|
|
6534
6660
|
providerId: z$1.string(),
|
|
6535
|
-
verified: z$1.
|
|
6661
|
+
verified: z$1.literal(false).describe("Clears domain verification. Domains can only be marked verified via DNS (verify-domain).")
|
|
6536
6662
|
})
|
|
6537
6663
|
}, async (ctx) => {
|
|
6538
6664
|
requireOrganizationPlugin(ctx);
|
|
6539
6665
|
requireOrganizationAccess(ctx);
|
|
6540
6666
|
if (!getSSOPlugin(ctx)?.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
6541
6667
|
const organizationId = tryDecode(ctx.params.id);
|
|
6542
|
-
const { providerId
|
|
6668
|
+
const { providerId } = ctx.body;
|
|
6543
6669
|
const provider = await ctx.context.adapter.findOne({
|
|
6544
6670
|
model: "ssoProvider",
|
|
6545
6671
|
where: [{
|
|
@@ -6557,12 +6683,12 @@ const markSsoProviderDomainVerified = (options) => {
|
|
|
6557
6683
|
field: "providerId",
|
|
6558
6684
|
value: provider.providerId
|
|
6559
6685
|
}],
|
|
6560
|
-
update: { domainVerified:
|
|
6686
|
+
update: { domainVerified: false }
|
|
6561
6687
|
});
|
|
6562
6688
|
return {
|
|
6563
6689
|
success: true,
|
|
6564
|
-
domainVerified:
|
|
6565
|
-
message:
|
|
6690
|
+
domainVerified: false,
|
|
6691
|
+
message: "Domain verification unmarked"
|
|
6566
6692
|
};
|
|
6567
6693
|
});
|
|
6568
6694
|
};
|
|
@@ -7004,6 +7130,18 @@ function parseWhereClause(val) {
|
|
|
7004
7130
|
if (!Array.isArray(parsed)) return [];
|
|
7005
7131
|
return parsed;
|
|
7006
7132
|
}
|
|
7133
|
+
const USER_LIST_MAX_LIMIT = 100;
|
|
7134
|
+
const USER_LIST_DEFAULT_LIMIT = 10;
|
|
7135
|
+
function clampUserListLimit(value, fallback = USER_LIST_DEFAULT_LIMIT) {
|
|
7136
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
7137
|
+
if (!Number.isFinite(n)) return fallback;
|
|
7138
|
+
return Math.min(Math.max(1, Math.floor(n)), USER_LIST_MAX_LIMIT);
|
|
7139
|
+
}
|
|
7140
|
+
function clampUserListOffset(value, fallback = 0) {
|
|
7141
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
7142
|
+
if (!Number.isFinite(n)) return fallback;
|
|
7143
|
+
return Math.max(0, Math.floor(n));
|
|
7144
|
+
}
|
|
7007
7145
|
const getUsersQuerySchema = z$1.object({
|
|
7008
7146
|
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
7009
7147
|
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
@@ -7027,10 +7165,12 @@ const getUsers = (options) => {
|
|
|
7027
7165
|
})();
|
|
7028
7166
|
const where = ctx.query?.where?.length ? ctx.query.where : void 0;
|
|
7029
7167
|
const countWhere = ctx.query?.countWhere?.length ? ctx.query.countWhere : void 0;
|
|
7168
|
+
const limit = clampUserListLimit(ctx.query?.limit, USER_LIST_DEFAULT_LIMIT);
|
|
7169
|
+
const offset = clampUserListOffset(ctx.query?.offset);
|
|
7030
7170
|
const userQuery = ctx.context.adapter.findMany({
|
|
7031
7171
|
model: "user",
|
|
7032
|
-
limit
|
|
7033
|
-
offset
|
|
7172
|
+
limit,
|
|
7173
|
+
offset,
|
|
7034
7174
|
sortBy: {
|
|
7035
7175
|
field: ctx.query?.sortBy || "createdAt",
|
|
7036
7176
|
direction: ctx.query?.sortOrder || "desc"
|
|
@@ -7168,6 +7308,8 @@ const impersonateUser = (options) => {
|
|
|
7168
7308
|
}, async (ctx) => {
|
|
7169
7309
|
const { userId, redirectUrl, impersonatedBy } = ctx.context.payload;
|
|
7170
7310
|
if (!(userId && redirectUrl)) throw ctx.error("BAD_REQUEST", { message: "Invalid token" });
|
|
7311
|
+
const trustedRedirectUrl = parseTrustedAuthRedirectUrl(ctx, redirectUrl);
|
|
7312
|
+
if (!trustedRedirectUrl) throw ctx.error("BAD_REQUEST", { message: "Invalid redirect URL" });
|
|
7171
7313
|
const session = await ctx.context.internalAdapter.createSession(userId, true, {
|
|
7172
7314
|
expiresAt: new Date(Date.now() + 1e3 * 60 * 10),
|
|
7173
7315
|
impersonatedBy: impersonatedBy || void 0
|
|
@@ -7178,7 +7320,7 @@ const impersonateUser = (options) => {
|
|
|
7178
7320
|
session,
|
|
7179
7321
|
user
|
|
7180
7322
|
}, true);
|
|
7181
|
-
throw ctx.redirect(
|
|
7323
|
+
throw ctx.redirect(trustedRedirectUrl);
|
|
7182
7324
|
});
|
|
7183
7325
|
};
|
|
7184
7326
|
const createUser = (options) => {
|
|
@@ -8019,7 +8161,7 @@ const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verifi
|
|
|
8019
8161
|
body: z$1.object({ callbackUrl: safeUrlSchema })
|
|
8020
8162
|
}, async (ctx) => {
|
|
8021
8163
|
const { userId } = ctx.context.payload;
|
|
8022
|
-
const
|
|
8164
|
+
const callbackUrl = requireTrustedAuthCallbackUrl(ctx, ctx.body.callbackUrl);
|
|
8023
8165
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
8024
8166
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
8025
8167
|
if (user.emailVerified) throw ctx.error("BAD_REQUEST", { message: "Email is already verified" });
|
|
@@ -8042,7 +8184,7 @@ const sendManyVerificationEmails = (options) => {
|
|
|
8042
8184
|
}, async (ctx) => {
|
|
8043
8185
|
if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
|
|
8044
8186
|
const { userIds } = ctx.context.payload;
|
|
8045
|
-
const
|
|
8187
|
+
const callbackUrl = requireTrustedAuthCallbackUrl(ctx, ctx.body.callbackUrl);
|
|
8046
8188
|
const sentEmailUserIds = /* @__PURE__ */ new Set();
|
|
8047
8189
|
const skippedEmailUserIds = /* @__PURE__ */ new Set();
|
|
8048
8190
|
const start = performance.now();
|
|
@@ -8101,10 +8243,11 @@ const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset
|
|
|
8101
8243
|
body: z$1.object({ callbackUrl: safeUrlSchema })
|
|
8102
8244
|
}, async (ctx) => {
|
|
8103
8245
|
const { userId } = ctx.context.payload;
|
|
8246
|
+
const callbackUrl = requireTrustedAuthCallbackUrl(ctx, ctx.body.callbackUrl);
|
|
8104
8247
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
8105
8248
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
8106
8249
|
if (!user.email) throw ctx.error("BAD_REQUEST", { message: "User has no associated email address" });
|
|
8107
|
-
ctx.body.redirectTo =
|
|
8250
|
+
ctx.body.redirectTo = callbackUrl;
|
|
8108
8251
|
ctx.body.email = user.email;
|
|
8109
8252
|
return await requestPasswordReset(ctx);
|
|
8110
8253
|
});
|
|
@@ -8717,6 +8860,7 @@ const dash = (options) => {
|
|
|
8717
8860
|
getEventTypes: getEventTypes(settings),
|
|
8718
8861
|
dashAcceptInvitation: acceptInvitation(settings),
|
|
8719
8862
|
dashCompleteInvitation: completeInvitation(settings),
|
|
8863
|
+
dashCompleteInvitationHandoff: completeInvitationHandoff(settings),
|
|
8720
8864
|
dashCompleteInvitationSocial: completeInvitationSocial(settings),
|
|
8721
8865
|
dashCheckUserExists: checkUserExists(settings),
|
|
8722
8866
|
listDashOrganizationDirectories: listOrganizationDirectories(settings),
|
package/dist/native.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as createKV, t as bytesToHex } from "./crypto-
|
|
2
|
-
import { a as resolveSentinelClientIdentifyUrl, c as dashClient, i as solvePoWChallenge, n as decodePoWChallenge, r as encodePoWSolution, s as identify, t as createPowRetryTimeout } from "./pow-retry-
|
|
1
|
+
import { o as createKV, t as bytesToHex } from "./crypto-BlYEkWgT.mjs";
|
|
2
|
+
import { a as resolveSentinelClientIdentifyUrl, c as dashClient, i as solvePoWChallenge, n as decodePoWChallenge, r as encodePoWSolution, s as identify, t as createPowRetryTimeout } from "./pow-retry-BN8NVM3m.mjs";
|
|
3
3
|
import { env } from "@better-auth/core/env";
|
|
4
4
|
import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
|
|
5
5
|
//#region src/sentinel/native/components.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-
|
|
1
|
+
import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-BlYEkWgT.mjs";
|
|
2
2
|
//#region src/dash-client.ts
|
|
3
3
|
function resolveDashUserId(input, options) {
|
|
4
4
|
return input.userId || options?.resolveUserId?.({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/infra",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Dashboard and analytics plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://better-auth.com",
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@better-auth/scim": "1.7.0-beta.
|
|
66
|
-
"@better-auth/sso": "1.7.0-beta.
|
|
65
|
+
"@better-auth/scim": "1.7.0-beta.10",
|
|
66
|
+
"@better-auth/sso": "1.7.0-beta.10",
|
|
67
67
|
"@infra/mocks": "0.0.0",
|
|
68
68
|
"@infra/utils": "0.0.0",
|
|
69
69
|
"@types/bun": "latest",
|
|
70
70
|
"@types/node": "^24.12.0",
|
|
71
|
-
"better-auth": "1.7.0-beta.
|
|
71
|
+
"better-auth": "1.7.0-beta.10",
|
|
72
72
|
"expo-crypto": "^14.0.2",
|
|
73
73
|
"happy-dom": "^20.9.0",
|
|
74
74
|
"msw": "^2.14.6",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@better-auth/utils": "^0.4.2",
|
|
81
81
|
"@better-fetch/fetch": "1.3.1",
|
|
82
|
-
"better-call": "
|
|
82
|
+
"better-call": "1.3.7",
|
|
83
83
|
"jose": "^6.1.0",
|
|
84
84
|
"libphonenumber-js": "^1.13.3"
|
|
85
85
|
},
|