better-auth 1.6.15 → 1.6.17
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/api/index.d.mts +2 -2
- package/dist/api/index.mjs +3 -4
- package/dist/api/middlewares/origin-check.mjs +6 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +31 -11
- package/dist/api/routes/callback.mjs +3 -3
- package/dist/api/routes/index.d.mts +1 -1
- package/dist/api/routes/password.mjs +3 -4
- package/dist/api/routes/session.d.mts +12 -1
- package/dist/api/routes/session.mjs +16 -2
- package/dist/api/routes/sign-in.mjs +5 -5
- package/dist/api/routes/sign-up.mjs +2 -2
- package/dist/api/routes/update-session.mjs +9 -4
- package/dist/api/routes/update-user.mjs +10 -12
- package/dist/auth/base.mjs +11 -7
- package/dist/client/equality.d.mts +19 -0
- package/dist/client/equality.mjs +42 -0
- package/dist/client/index.d.mts +5 -4
- package/dist/client/index.mjs +2 -1
- package/dist/client/lynx/index.d.mts +6 -5
- package/dist/client/path-to-object.d.mts +5 -2
- package/dist/client/plugins/index.d.mts +4 -1
- package/dist/client/plugins/index.mjs +4 -1
- package/dist/client/query.d.mts +4 -3
- package/dist/client/query.mjs +27 -17
- package/dist/client/react/index.d.mts +6 -5
- package/dist/client/session-atom.mjs +129 -4
- package/dist/client/session-refresh.d.mts +3 -18
- package/dist/client/session-refresh.mjs +38 -49
- package/dist/client/solid/index.d.mts +6 -5
- package/dist/client/svelte/index.d.mts +6 -5
- package/dist/client/types.d.mts +2 -2
- package/dist/client/vanilla.d.mts +6 -5
- package/dist/client/vue/index.d.mts +6 -5
- package/dist/context/create-context.mjs +3 -2
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +30 -2
- package/dist/db/internal-adapter.mjs +56 -0
- package/dist/oauth2/link-account.d.mts +13 -0
- package/dist/oauth2/link-account.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -3
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +73 -5
- package/dist/plugins/admin/schema.d.mts +1 -0
- package/dist/plugins/admin/schema.mjs +2 -1
- package/dist/plugins/captcha/constants.mjs +8 -1
- package/dist/plugins/captcha/index.mjs +8 -2
- package/dist/plugins/captcha/types.d.mts +21 -0
- package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
- package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
- package/dist/plugins/device-authorization/routes.mjs +16 -9
- package/dist/plugins/email-otp/routes.mjs +23 -53
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +20 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +5 -1
- package/dist/plugins/index.d.mts +5 -1
- package/dist/plugins/index.mjs +4 -1
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/mcp/client/index.mjs +1 -0
- package/dist/plugins/mcp/index.mjs +8 -0
- package/dist/plugins/multi-session/index.mjs +7 -5
- package/dist/plugins/oauth-popup/client.d.mts +82 -0
- package/dist/plugins/oauth-popup/client.mjs +203 -0
- package/dist/plugins/oauth-popup/constants.d.mts +11 -0
- package/dist/plugins/oauth-popup/constants.mjs +11 -0
- package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
- package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
- package/dist/plugins/oauth-popup/index.d.mts +67 -0
- package/dist/plugins/oauth-popup/index.mjs +227 -0
- package/dist/plugins/oauth-popup/types.d.mts +30 -0
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/utils.mjs +16 -2
- package/dist/plugins/oidc-provider/index.mjs +10 -0
- package/dist/plugins/one-tap/client.mjs +12 -6
- package/dist/plugins/one-tap/index.d.mts +1 -0
- package/dist/plugins/one-tap/index.mjs +9 -5
- package/dist/plugins/one-time-token/index.mjs +1 -3
- package/dist/plugins/open-api/generator.mjs +7 -4
- package/dist/plugins/organization/adapter.d.mts +29 -1
- package/dist/plugins/organization/adapter.mjs +66 -6
- package/dist/plugins/organization/organization.mjs +2 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +51 -5
- package/dist/plugins/organization/schema.d.mts +2 -0
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +30 -3
- package/dist/plugins/siwe/parse-message.mjs +60 -0
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
- package/dist/plugins/two-factor/index.mjs +9 -1
- package/dist/plugins/two-factor/otp/index.mjs +11 -13
- package/dist/plugins/two-factor/totp/index.mjs +1 -1
- package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
- package/dist/plugins/username/index.mjs +6 -6
- package/dist/test-utils/test-instance.d.mts +6 -5
- package/package.json +10 -10
|
@@ -9,6 +9,19 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
|
9
9
|
disableSignUp?: boolean | undefined;
|
|
10
10
|
overrideUserInfo?: boolean | undefined;
|
|
11
11
|
isTrustedProvider?: boolean | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Whether `account.providerId` may be matched against the globally
|
|
14
|
+
* configured `accountLinking.trustedProviders` list to infer trust.
|
|
15
|
+
*
|
|
16
|
+
* Defaults to `true` for built-in social/OAuth providers, whose
|
|
17
|
+
* `providerId` namespace is controlled by the developer's config. Callers
|
|
18
|
+
* whose `providerId` is user-controlled (e.g. the SSO plugin, where any
|
|
19
|
+
* authenticated user can register a provider with an arbitrary id) must
|
|
20
|
+
* pass `false` so a provider named after a trusted social provider can't
|
|
21
|
+
* launder that trust. Such callers should supply their own
|
|
22
|
+
* `isTrustedProvider` signal instead.
|
|
23
|
+
*/
|
|
24
|
+
trustProviderByName?: boolean | undefined;
|
|
12
25
|
}): Promise<{
|
|
13
26
|
error: string;
|
|
14
27
|
data: null;
|
|
@@ -17,7 +17,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
17
17
|
const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
|
|
18
18
|
if (!linkedAccount) {
|
|
19
19
|
const accountLinking = c.context.options.account?.accountLinking;
|
|
20
|
-
const isTrustedProvider = opts.isTrustedProvider || c.context.trustedProviders.includes(account.providerId);
|
|
20
|
+
const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
|
|
21
21
|
const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
|
|
22
22
|
if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
|
|
23
23
|
if (isDevelopment()) logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`);
|
package/dist/package.mjs
CHANGED
|
@@ -1,33 +1,63 @@
|
|
|
1
1
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
2
2
|
//#region src/plugins/access/access.ts
|
|
3
|
+
function unknownResourceResponse(requestedResource) {
|
|
4
|
+
return {
|
|
5
|
+
success: false,
|
|
6
|
+
error: `You are not allowed to access resource: ${requestedResource}`
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function unauthorizedResourceResponse(requestedResource) {
|
|
10
|
+
return {
|
|
11
|
+
success: false,
|
|
12
|
+
error: `unauthorized to access resource "${requestedResource}"`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function normalizeConnector(connector) {
|
|
16
|
+
return connector === "OR" ? "OR" : "AND";
|
|
17
|
+
}
|
|
18
|
+
function isActionList(actions) {
|
|
19
|
+
return Array.isArray(actions);
|
|
20
|
+
}
|
|
21
|
+
function normalizeActionRequest(requestedActions) {
|
|
22
|
+
if (isActionList(requestedActions)) return {
|
|
23
|
+
actions: requestedActions,
|
|
24
|
+
connector: "AND"
|
|
25
|
+
};
|
|
26
|
+
if (!requestedActions || typeof requestedActions !== "object") throw new BetterAuthError("Invalid access control request");
|
|
27
|
+
const { actions, connector } = requestedActions;
|
|
28
|
+
if (!isActionList(actions)) return {
|
|
29
|
+
actions: [],
|
|
30
|
+
connector: normalizeConnector(connector)
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
actions,
|
|
34
|
+
connector: normalizeConnector(connector)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function hasAllowedAction(allowedActions, requestedAction) {
|
|
38
|
+
return typeof requestedAction === "string" && allowedActions.includes(requestedAction);
|
|
39
|
+
}
|
|
40
|
+
function isResourceAuthorized(allowedActions, { actions, connector }) {
|
|
41
|
+
if (actions.length === 0) return false;
|
|
42
|
+
if (connector === "OR") return actions.some((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
|
|
43
|
+
return actions.every((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
|
|
44
|
+
}
|
|
3
45
|
function role(statements) {
|
|
4
46
|
return {
|
|
5
47
|
authorize(request, connector = "AND") {
|
|
6
|
-
let
|
|
48
|
+
let hasAuthorizedResource = false;
|
|
7
49
|
for (const [requestedResource, requestedActions] of Object.entries(request)) {
|
|
8
50
|
const allowedActions = statements[requestedResource];
|
|
9
51
|
if (!allowedActions) {
|
|
10
|
-
if (connector === "AND") return
|
|
11
|
-
success: false,
|
|
12
|
-
error: `You are not allowed to access resource: ${requestedResource}`
|
|
13
|
-
};
|
|
14
|
-
success = false;
|
|
52
|
+
if (connector === "AND") return unknownResourceResponse(requestedResource);
|
|
15
53
|
continue;
|
|
16
54
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
|
|
22
|
-
else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
|
|
23
|
-
} else throw new BetterAuthError("Invalid access control request");
|
|
24
|
-
if (success && connector === "OR") return { success };
|
|
25
|
-
if (!success && connector === "AND") return {
|
|
26
|
-
success: false,
|
|
27
|
-
error: `unauthorized to access resource "${requestedResource}"`
|
|
28
|
-
};
|
|
55
|
+
const isAuthorized = isResourceAuthorized(allowedActions, normalizeActionRequest(requestedActions));
|
|
56
|
+
if (isAuthorized) hasAuthorizedResource = true;
|
|
57
|
+
if (isAuthorized && connector === "OR") return { success: true };
|
|
58
|
+
if (!isAuthorized && connector === "AND") return unauthorizedResourceResponse(requestedResource);
|
|
29
59
|
}
|
|
30
|
-
if (
|
|
60
|
+
if (hasAuthorizedResource) return { success: true };
|
|
31
61
|
return {
|
|
32
62
|
success: false,
|
|
33
63
|
error: "Not authorized"
|
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
import { ExactRoleStatements, Role, RoleInput, Statements } from "../../access/types.mjs";
|
|
2
2
|
//#region src/plugins/admin/access/statement.d.ts
|
|
3
3
|
declare const defaultStatements: {
|
|
4
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
4
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
5
5
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
6
6
|
};
|
|
7
7
|
declare const defaultAc: {
|
|
8
8
|
newRole<const TRoleStatements extends Statements>(statements: RoleInput<{
|
|
9
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
9
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
10
10
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
11
11
|
}, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, {
|
|
12
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
12
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
13
13
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
14
14
|
}>;
|
|
15
15
|
statements: {
|
|
16
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
16
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
17
17
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
declare const adminAc: Role<ExactRoleStatements<{
|
|
21
|
-
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
|
|
21
|
+
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
|
|
22
22
|
readonly session: ["list", "revoke", "delete"];
|
|
23
23
|
}>, {
|
|
24
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
24
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
25
25
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
26
26
|
}>;
|
|
27
27
|
declare const userAc: Role<ExactRoleStatements<{
|
|
28
28
|
readonly user: [];
|
|
29
29
|
readonly session: [];
|
|
30
30
|
}>, {
|
|
31
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
31
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
32
32
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
33
33
|
}>;
|
|
34
34
|
declare const defaultRoles: {
|
|
35
35
|
admin: Role<ExactRoleStatements<{
|
|
36
|
-
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
|
|
36
|
+
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
|
|
37
37
|
readonly session: ["list", "revoke", "delete"];
|
|
38
38
|
}>, {
|
|
39
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
39
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
40
40
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
41
41
|
}>;
|
|
42
42
|
user: Role<ExactRoleStatements<{
|
|
43
43
|
readonly user: [];
|
|
44
44
|
readonly session: [];
|
|
45
45
|
}>, {
|
|
46
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
46
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
47
47
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
48
48
|
}>;
|
|
49
49
|
};
|
|
@@ -10,6 +10,7 @@ const defaultStatements = {
|
|
|
10
10
|
"impersonate-admins",
|
|
11
11
|
"delete",
|
|
12
12
|
"set-password",
|
|
13
|
+
"set-email",
|
|
13
14
|
"get",
|
|
14
15
|
"update"
|
|
15
16
|
],
|
|
@@ -29,6 +30,7 @@ const adminAc = defaultAc.newRole({
|
|
|
29
30
|
"impersonate",
|
|
30
31
|
"delete",
|
|
31
32
|
"set-password",
|
|
33
|
+
"set-email",
|
|
32
34
|
"get",
|
|
33
35
|
"update"
|
|
34
36
|
],
|
|
@@ -824,13 +824,13 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
824
824
|
$Infer: {
|
|
825
825
|
body: {
|
|
826
826
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
827
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
827
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
828
828
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
829
829
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
830
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
830
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
831
831
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
832
832
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
833
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
833
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
834
834
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
835
835
|
})[key]> : never)[] | undefined };
|
|
836
836
|
} & {
|
|
@@ -866,6 +866,8 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
866
866
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
867
867
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
868
868
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
869
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
870
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
869
871
|
};
|
|
870
872
|
schema: {
|
|
871
873
|
user: {
|
|
@@ -898,6 +900,7 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
898
900
|
impersonatedBy: {
|
|
899
901
|
type: "string";
|
|
900
902
|
required: false;
|
|
903
|
+
input: false;
|
|
901
904
|
};
|
|
902
905
|
};
|
|
903
906
|
};
|
|
@@ -14,7 +14,7 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
14
14
|
version: string;
|
|
15
15
|
$InferServerPlugin: ReturnType<typeof admin<{
|
|
16
16
|
ac: O["ac"] extends AccessControl ? O["ac"] : AccessControl<{
|
|
17
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
17
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
18
18
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
19
19
|
}>;
|
|
20
20
|
roles: O["roles"] extends Record<string, Role> ? O["roles"] : {
|
|
@@ -28,13 +28,13 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
28
28
|
roles: any;
|
|
29
29
|
} ? keyof O["roles"] : "admin" | "user")>(data: {
|
|
30
30
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
31
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
31
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
32
32
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
33
33
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
34
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
34
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
35
35
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
36
36
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
37
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
37
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
38
38
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
39
39
|
})[key]> : never)[] | undefined };
|
|
40
40
|
} & {
|
|
@@ -73,6 +73,8 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
73
73
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
74
74
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
75
75
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
76
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
77
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
76
78
|
};
|
|
77
79
|
};
|
|
78
80
|
//#endregion
|
|
@@ -23,6 +23,8 @@ declare const ADMIN_ERROR_CODES: {
|
|
|
23
23
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
24
24
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
25
25
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
26
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
27
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
26
28
|
};
|
|
27
29
|
//#endregion
|
|
28
30
|
export { ADMIN_ERROR_CODES };
|
|
@@ -21,7 +21,9 @@ const ADMIN_ERROR_CODES = defineErrorCodes({
|
|
|
21
21
|
YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself",
|
|
22
22
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: "You are not allowed to set a non-existent role value",
|
|
23
23
|
YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins",
|
|
24
|
-
INVALID_ROLE_TYPE: "Invalid role type"
|
|
24
|
+
INVALID_ROLE_TYPE: "Invalid role type",
|
|
25
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: "You are not allowed to update users email",
|
|
26
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: "Password cannot be updated through update-user. Use the set-user-password endpoint instead"
|
|
25
27
|
});
|
|
26
28
|
//#endregion
|
|
27
29
|
export { ADMIN_ERROR_CODES };
|
|
@@ -156,14 +156,43 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
156
156
|
permissions: { user: ["create"] }
|
|
157
157
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS);
|
|
158
158
|
}
|
|
159
|
+
const { role: dataRole, ...userData } = ctx.body.data ?? {};
|
|
160
|
+
const requestedRole = ctx.body.role ?? dataRole;
|
|
161
|
+
if (requestedRole !== void 0) {
|
|
162
|
+
if (session) {
|
|
163
|
+
if (!hasPermission({
|
|
164
|
+
userId: session.user.id,
|
|
165
|
+
role: session.user.role,
|
|
166
|
+
options: opts,
|
|
167
|
+
permissions: { user: ["set-role"] }
|
|
168
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE);
|
|
169
|
+
}
|
|
170
|
+
const inputRoles = Array.isArray(requestedRole) ? requestedRole : [requestedRole];
|
|
171
|
+
for (const role of inputRoles) {
|
|
172
|
+
if (typeof role !== "string") throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.INVALID_ROLE_TYPE);
|
|
173
|
+
if (opts.roles && !opts.roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (session && [
|
|
177
|
+
"banned",
|
|
178
|
+
"banReason",
|
|
179
|
+
"banExpires"
|
|
180
|
+
].some((key) => Object.prototype.hasOwnProperty.call(userData, key))) {
|
|
181
|
+
if (!hasPermission({
|
|
182
|
+
userId: session.user.id,
|
|
183
|
+
role: session.user.role,
|
|
184
|
+
options: opts,
|
|
185
|
+
permissions: { user: ["ban"] }
|
|
186
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
187
|
+
}
|
|
159
188
|
const email = ctx.body.email.toLowerCase();
|
|
160
189
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
161
190
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
162
191
|
const user = await ctx.context.internalAdapter.createUser({
|
|
192
|
+
...userData,
|
|
163
193
|
email,
|
|
164
194
|
name: ctx.body.name,
|
|
165
|
-
role:
|
|
166
|
-
...ctx.body.data
|
|
195
|
+
role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
|
|
167
196
|
});
|
|
168
197
|
if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
|
|
169
198
|
if (ctx.body.password) {
|
|
@@ -220,6 +249,9 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
220
249
|
permissions: { user: ["update"] }
|
|
221
250
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS);
|
|
222
251
|
if (Object.keys(ctx.body.data).length === 0) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE);
|
|
252
|
+
const updateData = ctx.body.data;
|
|
253
|
+
const hasDataKey = (key) => Object.prototype.hasOwnProperty.call(updateData, key);
|
|
254
|
+
if (hasDataKey("password")) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER);
|
|
223
255
|
if (Object.prototype.hasOwnProperty.call(ctx.body.data, "role")) {
|
|
224
256
|
if (!hasPermission({
|
|
225
257
|
userId: ctx.context.session.user.id,
|
|
@@ -235,8 +267,37 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
235
267
|
}
|
|
236
268
|
ctx.body.data.role = parseRoles(inputRoles);
|
|
237
269
|
}
|
|
270
|
+
if ([
|
|
271
|
+
"banned",
|
|
272
|
+
"banReason",
|
|
273
|
+
"banExpires"
|
|
274
|
+
].some(hasDataKey)) {
|
|
275
|
+
if (!hasPermission({
|
|
276
|
+
userId: ctx.context.session.user.id,
|
|
277
|
+
role: ctx.context.session.user.role,
|
|
278
|
+
options: opts,
|
|
279
|
+
permissions: { user: ["ban"] }
|
|
280
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
281
|
+
if (updateData.banned === true && ctx.body.userId === ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF);
|
|
282
|
+
}
|
|
283
|
+
if (hasDataKey("email") || hasDataKey("emailVerified")) {
|
|
284
|
+
if (!hasPermission({
|
|
285
|
+
userId: ctx.context.session.user.id,
|
|
286
|
+
role: ctx.context.session.user.role,
|
|
287
|
+
options: opts,
|
|
288
|
+
permissions: { user: ["set-email"] }
|
|
289
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL);
|
|
290
|
+
if (hasDataKey("email")) {
|
|
291
|
+
const email = String(updateData.email).toLowerCase();
|
|
292
|
+
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
293
|
+
const existUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
294
|
+
if (existUser && existUser.user.id !== ctx.body.userId) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
295
|
+
updateData.email = email;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
238
298
|
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
239
299
|
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
|
|
300
|
+
if (updateData.banned === true) await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
|
|
240
301
|
return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
|
|
241
302
|
});
|
|
242
303
|
const listUsersQuerySchema = z.object({
|
|
@@ -755,16 +816,23 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
|
|
|
755
816
|
const { newPassword, userId } = ctx.body;
|
|
756
817
|
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
757
818
|
if (newPassword.length < minPasswordLength) {
|
|
758
|
-
ctx.context.logger.
|
|
819
|
+
ctx.context.logger.warn("Password is too short");
|
|
759
820
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
760
821
|
}
|
|
761
822
|
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
762
823
|
if (newPassword.length > maxPasswordLength) {
|
|
763
|
-
ctx.context.logger.
|
|
824
|
+
ctx.context.logger.warn("Password is too long");
|
|
764
825
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
765
826
|
}
|
|
827
|
+
if (!await ctx.context.internalAdapter.findUserById(userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
766
828
|
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
767
|
-
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
829
|
+
if ((await ctx.context.internalAdapter.findAccounts(userId)).find((account) => account.providerId === "credential")) await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
830
|
+
else await ctx.context.internalAdapter.createAccount({
|
|
831
|
+
userId,
|
|
832
|
+
providerId: "credential",
|
|
833
|
+
accountId: userId,
|
|
834
|
+
password: hashedPassword
|
|
835
|
+
});
|
|
768
836
|
return ctx.json({ status: true });
|
|
769
837
|
});
|
|
770
838
|
const userHasPermissionBodySchema = z.object({
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
//#region src/plugins/captcha/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound (in milliseconds) for a single provider verification request.
|
|
4
|
+
* Without it, a hanging provider would tie up the request indefinitely before
|
|
5
|
+
* any rate limiting applies, so every verify handler aborts at this deadline
|
|
6
|
+
* and fails closed.
|
|
7
|
+
*/
|
|
8
|
+
const CAPTCHA_VERIFY_TIMEOUT_MS = 1e4;
|
|
2
9
|
const defaultEndpoints = [
|
|
3
10
|
"/sign-up/email",
|
|
4
11
|
"/sign-in/email",
|
|
@@ -17,4 +24,4 @@ const siteVerifyMap = {
|
|
|
17
24
|
[Providers.CAPTCHAFOX]: "https://api.captchafox.com/siteverify"
|
|
18
25
|
};
|
|
19
26
|
//#endregion
|
|
20
|
-
export { Providers, defaultEndpoints, siteVerifyMap };
|
|
27
|
+
export { CAPTCHA_VERIFY_TIMEOUT_MS, Providers, defaultEndpoints, siteVerifyMap };
|
|
@@ -42,10 +42,16 @@ const captcha = (options) => ({
|
|
|
42
42
|
secretKey: options.secretKey,
|
|
43
43
|
remoteIP: remoteUserIP
|
|
44
44
|
};
|
|
45
|
-
if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile(
|
|
45
|
+
if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile({
|
|
46
|
+
...handlerParams,
|
|
47
|
+
expectedAction: options.expectedAction,
|
|
48
|
+
allowedHostnames: options.allowedHostnames
|
|
49
|
+
});
|
|
46
50
|
if (options.provider === Providers.GOOGLE_RECAPTCHA) return await googleRecaptcha({
|
|
47
51
|
...handlerParams,
|
|
48
|
-
minScore: options.minScore
|
|
52
|
+
minScore: options.minScore,
|
|
53
|
+
expectedAction: options.expectedAction,
|
|
54
|
+
allowedHostnames: options.allowedHostnames
|
|
49
55
|
});
|
|
50
56
|
if (options.provider === Providers.HCAPTCHA) return await hCaptcha({
|
|
51
57
|
...handlerParams,
|
|
@@ -10,9 +10,30 @@ interface BaseCaptchaOptions {
|
|
|
10
10
|
interface GoogleRecaptchaOptions extends BaseCaptchaOptions {
|
|
11
11
|
provider: typeof Providers.GOOGLE_RECAPTCHA;
|
|
12
12
|
minScore?: number | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Expected reCAPTCHA v3 `action`. When set, a verification whose action does
|
|
15
|
+
* not match is rejected, preventing a token minted for another action on the
|
|
16
|
+
* same site key from being replayed against this endpoint.
|
|
17
|
+
*/
|
|
18
|
+
expectedAction?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Allow-list of hostnames the token must have been issued for. When set, a
|
|
21
|
+
* verification reporting a different hostname is rejected.
|
|
22
|
+
*/
|
|
23
|
+
allowedHostnames?: string[] | undefined;
|
|
13
24
|
}
|
|
14
25
|
interface CloudflareTurnstileOptions extends BaseCaptchaOptions {
|
|
15
26
|
provider: typeof Providers.CLOUDFLARE_TURNSTILE;
|
|
27
|
+
/**
|
|
28
|
+
* Expected Turnstile `action`. When set, a verification whose action does
|
|
29
|
+
* not match is rejected, preventing cross-context token reuse.
|
|
30
|
+
*/
|
|
31
|
+
expectedAction?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Allow-list of hostnames the token must have been issued for. When set, a
|
|
34
|
+
* verification reporting a different or missing hostname is rejected.
|
|
35
|
+
*/
|
|
36
|
+
allowedHostnames?: string[] | undefined;
|
|
16
37
|
}
|
|
17
38
|
interface HCaptchaOptions extends BaseCaptchaOptions {
|
|
18
39
|
provider: typeof Providers.HCAPTCHA;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { encodeToURLParams } from "../utils.mjs";
|
|
4
5
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
6
7
|
const captchaFox = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
|
|
7
8
|
const response = await betterFetch(siteVerifyURL, {
|
|
8
9
|
method: "POST",
|
|
10
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
9
11
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
10
12
|
body: encodeToURLParams({
|
|
11
13
|
secret: secretKey,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { betterFetch } from "@better-fetch/fetch";
|
|
4
5
|
//#region src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
|
|
5
|
-
const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP }) => {
|
|
6
|
+
const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP, expectedAction, allowedHostnames }) => {
|
|
6
7
|
const response = await betterFetch(siteVerifyURL, {
|
|
7
8
|
method: "POST",
|
|
9
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
8
10
|
headers: { "Content-Type": "application/json" },
|
|
9
11
|
body: JSON.stringify({
|
|
10
12
|
secret: secretKey,
|
|
@@ -13,11 +15,14 @@ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey,
|
|
|
13
15
|
})
|
|
14
16
|
});
|
|
15
17
|
if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
|
|
16
|
-
|
|
18
|
+
const verificationFailed = () => middlewareResponse({
|
|
17
19
|
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
|
18
20
|
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
|
19
21
|
status: 403
|
|
20
22
|
});
|
|
23
|
+
if (!response.data.success) return verificationFailed();
|
|
24
|
+
if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
|
|
25
|
+
if (allowedHostnames && allowedHostnames.length > 0 && !(response.data.hostname && allowedHostnames.includes(response.data.hostname))) return verificationFailed();
|
|
21
26
|
};
|
|
22
27
|
//#endregion
|
|
23
28
|
export { cloudflareTurnstile };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { encodeToURLParams } from "../utils.mjs";
|
|
4
5
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -6,9 +7,10 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
6
7
|
const isV3 = (response) => {
|
|
7
8
|
return "score" in response && typeof response.score === "number";
|
|
8
9
|
};
|
|
9
|
-
const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP }) => {
|
|
10
|
+
const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP, expectedAction, allowedHostnames }) => {
|
|
10
11
|
const response = await betterFetch(siteVerifyURL, {
|
|
11
12
|
method: "POST",
|
|
13
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
12
14
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
13
15
|
body: encodeToURLParams({
|
|
14
16
|
secret: secretKey,
|
|
@@ -17,11 +19,14 @@ const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minS
|
|
|
17
19
|
})
|
|
18
20
|
});
|
|
19
21
|
if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
|
|
20
|
-
|
|
22
|
+
const verificationFailed = () => middlewareResponse({
|
|
21
23
|
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
|
22
24
|
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
|
23
25
|
status: 403
|
|
24
26
|
});
|
|
27
|
+
if (!response.data.success || isV3(response.data) && response.data.score < minScore) return verificationFailed();
|
|
28
|
+
if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
|
|
29
|
+
if (allowedHostnames && allowedHostnames.length > 0 && !allowedHostnames.includes(response.data.hostname)) return verificationFailed();
|
|
25
30
|
};
|
|
26
31
|
//#endregion
|
|
27
32
|
export { googleRecaptcha };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { encodeToURLParams } from "../utils.mjs";
|
|
4
5
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
6
7
|
const hCaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
|
|
7
8
|
const response = await betterFetch(siteVerifyURL, {
|
|
8
9
|
method: "POST",
|
|
10
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
9
11
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
10
12
|
body: encodeToURLParams({
|
|
11
13
|
secret: secretKey,
|