@gatwech/auth 1.0.0-beta.22 → 1.0.0-beta.24
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/adapters/index.d.mts +1 -1
- package/dist/api/index.d.mts +1 -1
- package/dist/{audit-log-DgqHJ-uv.mjs → audit-log-DhRRCMrv.mjs} +21 -6
- package/dist/client/index.d.mts +1 -1
- package/dist/client/plugins/index.d.mts +27 -27
- package/dist/client/react/index.d.mts +1 -1
- package/dist/{client-ukvqpcbI.d.mts → client-CGV_kj99.d.mts} +1 -1
- package/dist/{client-DO9MgP_0.d.mts → client-CT-acJeN.d.mts} +1 -1
- package/dist/cookies/index.d.mts +2 -2
- package/dist/db/index.d.mts +1 -1
- package/dist/{index-BfLofiXO.d.mts → index-B-i2ob-C.d.mts} +9 -9
- package/dist/{index-CTSeVJJB.d.mts → index-B6to9CaK.d.mts} +1 -1
- package/dist/{index-hP8gvyp-.d.mts → index-BRUQHEUF.d.mts} +5 -5
- package/dist/{index-D3uQsW8T.d.mts → index-BYxt8qOP.d.mts} +33 -33
- package/dist/{index-CRNh-5Tc.d.mts → index-CGGitykk.d.mts} +7 -7
- package/dist/{index-eOfQxlQV.d.mts → index-CHMYUshk.d.mts} +13 -13
- package/dist/{index-Dl0bD1_6.d.mts → index-CLhlur69.d.mts} +6 -6
- package/dist/{index-DngQlUSI.d.mts → index-CZtQ0rUe.d.mts} +113 -113
- package/dist/{index-Dkm27DLN.d.mts → index-Cl9ITXy1.d.mts} +10 -10
- package/dist/{index-SYRh5FW7.d.mts → index-Clwezlxm.d.mts} +8 -8
- package/dist/{index-BzzhaGLe.d.mts → index-Czr_eQFE.d.mts} +6 -6
- package/dist/{index-DKirx_GW.d.mts → index-D5cyFBEb.d.mts} +4 -4
- package/dist/{index-BC0aAKsT.d.mts → index-D85KXOKz.d.mts} +3 -3
- package/dist/{index-CxhQu1HM.d.mts → index-DGRGLg16.d.mts} +2 -2
- package/dist/{index-B4hJVVRn.d.mts → index-DWGj7du8.d.mts} +19 -19
- package/dist/{index-AiKdOFSd.d.mts → index-DWSbFJWU.d.mts} +387 -49
- package/dist/{index-Do8AV4PF.d.mts → index-DYqQWdEv.d.mts} +13 -13
- package/dist/{index-DlS-HqZR.d.mts → index-Dmjw6re9.d.mts} +2 -2
- package/dist/{index-CRiKKdkb.d.mts → index-Dt6JhAV4.d.mts} +6 -6
- package/dist/{index-pdl9MzQu.d.mts → index-Hoybqpnt.d.mts} +9 -9
- package/dist/{index-DSoiUc1C.d.mts → index-JzSy-vFn.d.mts} +166 -166
- package/dist/{index-D-rjuF7p.d.mts → index-UDzaYKxq.d.mts} +9 -9
- package/dist/{index-CBGNX7W1.d.mts → index-rcUKPBBy.d.mts} +6 -6
- package/dist/index.d.mts +1 -1
- package/dist/integrations/next-js.d.mts +4 -4
- package/dist/integrations/node.d.mts +1 -1
- package/dist/integrations/react-start.d.mts +4 -4
- package/dist/oauth2/index.d.mts +1 -1
- package/dist/plugins/admin/index.d.mts +2 -2
- package/dist/plugins/anonymous/index.d.mts +2 -2
- package/dist/plugins/audit-log/client.d.mts +23 -23
- package/dist/plugins/audit-log/index.d.mts +22 -22
- package/dist/plugins/audit-log/index.mjs +2 -2
- package/dist/plugins/bearer/index.d.mts +1 -1
- package/dist/plugins/captcha/index.d.mts +1 -1
- package/dist/plugins/custom-session/index.d.mts +2 -2
- package/dist/plugins/device-authorization/index.d.mts +1 -1
- package/dist/plugins/email-otp/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +2 -2
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +22 -22
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/jwt/index.d.mts +2 -2
- package/dist/plugins/magic-link/index.d.mts +1 -1
- package/dist/plugins/multi-session/index.d.mts +1 -1
- package/dist/plugins/oauth-proxy/index.d.mts +1 -1
- package/dist/plugins/one-tap/index.d.mts +1 -1
- package/dist/plugins/one-time-token/index.d.mts +2 -2
- package/dist/plugins/one-time-token/index.mjs +3 -3
- package/dist/plugins/organization/index.d.mts +2 -2
- package/dist/plugins/passkey/index.d.mts +2 -2
- package/dist/plugins/phone-number/index.d.mts +2 -2
- package/dist/plugins/sentinel/client.d.mts +23 -23
- package/dist/plugins/sentinel/index.d.mts +22 -22
- package/dist/plugins/sentinel/index.mjs +2 -2
- package/dist/plugins/two-factor/index.d.mts +2 -2
- package/dist/plugins/username/index.d.mts +1 -1
- package/dist/{registry-CqG_3Qnx.mjs → registry-GztLf62p.mjs} +14 -2
- package/dist/{sentinel-B-2jmmZh.mjs → sentinel-b8XEetew.mjs} +265 -9
- package/dist/src/plugins/audit-log/events.d.ts +1 -0
- package/dist/src/plugins/audit-log/index.d.ts +45 -0
- package/dist/src/plugins/sentinel/actions.d.ts +2 -0
- package/dist/src/plugins/sentinel/index.d.ts +247 -0
- package/dist/src/plugins/sentinel/schema.d.ts +37 -0
- package/dist/src/plugins/sentinel/store.d.ts +34 -0
- package/dist/src/plugins/sentinel/types.d.ts +7 -0
- package/dist/src/utils/has-role.d.ts +5 -0
- package/dist/test-utils/index.d.mts +273 -273
- package/dist/test-utils/index.mjs +3 -3
- package/dist/types/index.d.mts +1 -1
- package/package.json +3 -3
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import "../../helper-BgOIGHsu.mjs";
|
|
2
|
-
import "../../index-
|
|
2
|
+
import "../../index-CZtQ0rUe.mjs";
|
|
3
3
|
import "../../types-DNQ9XQrB.mjs";
|
|
4
4
|
import "../../index-BL_t8cRp-C4QYBsac.mjs";
|
|
5
5
|
import "../../plugins-Dy_FrD4Y.mjs";
|
|
6
6
|
import "../../index-CxlWnL9c.mjs";
|
|
7
|
-
import "../../index-
|
|
8
|
-
import "../../index-
|
|
9
|
-
import "../../index-
|
|
10
|
-
import "../../index-
|
|
11
|
-
import "../../index-
|
|
12
|
-
import "../../index-
|
|
13
|
-
import "../../index-
|
|
14
|
-
import "../../index-
|
|
15
|
-
import "../../index-
|
|
16
|
-
import "../../index-
|
|
17
|
-
import "../../index-
|
|
18
|
-
import "../../index-
|
|
19
|
-
import "../../index-
|
|
20
|
-
import "../../index-
|
|
21
|
-
import "../../index-
|
|
22
|
-
import "../../index-
|
|
7
|
+
import "../../index-BYxt8qOP.mjs";
|
|
8
|
+
import "../../index-Dt6JhAV4.mjs";
|
|
9
|
+
import "../../index-DWSbFJWU.mjs";
|
|
10
|
+
import "../../index-Czr_eQFE.mjs";
|
|
11
|
+
import "../../index-Dmjw6re9.mjs";
|
|
12
|
+
import "../../index-CLhlur69.mjs";
|
|
13
|
+
import "../../index-rcUKPBBy.mjs";
|
|
14
|
+
import "../../index-DYqQWdEv.mjs";
|
|
15
|
+
import "../../index-Clwezlxm.mjs";
|
|
16
|
+
import "../../index-D85KXOKz.mjs";
|
|
17
|
+
import "../../index-UDzaYKxq.mjs";
|
|
18
|
+
import "../../index-D5cyFBEb.mjs";
|
|
19
|
+
import "../../index-B-i2ob-C.mjs";
|
|
20
|
+
import "../../index-Cl9ITXy1.mjs";
|
|
21
|
+
import "../../index-DGRGLg16.mjs";
|
|
22
|
+
import "../../index-BRUQHEUF.mjs";
|
|
23
23
|
import "../../index-QmocqGM-.mjs";
|
|
24
|
-
import "../../index-
|
|
25
|
-
import "../../index-
|
|
26
|
-
import "../../index-
|
|
27
|
-
import "../../index-
|
|
28
|
-
import "../../index-
|
|
29
|
-
import { t as sentinelClient } from "../../client-
|
|
24
|
+
import "../../index-JzSy-vFn.mjs";
|
|
25
|
+
import "../../index-CGGitykk.mjs";
|
|
26
|
+
import "../../index-DWGj7du8.mjs";
|
|
27
|
+
import "../../index-CHMYUshk.mjs";
|
|
28
|
+
import "../../index-Hoybqpnt.mjs";
|
|
29
|
+
import { t as sentinelClient } from "../../client-CT-acJeN.mjs";
|
|
30
30
|
export { sentinelClient };
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import "../../helper-BgOIGHsu.mjs";
|
|
2
|
-
import "../../index-
|
|
2
|
+
import "../../index-CZtQ0rUe.mjs";
|
|
3
3
|
import "../../types-DNQ9XQrB.mjs";
|
|
4
4
|
import "../../index-BL_t8cRp-C4QYBsac.mjs";
|
|
5
5
|
import "../../plugins-Dy_FrD4Y.mjs";
|
|
6
6
|
import "../../index-CxlWnL9c.mjs";
|
|
7
|
-
import "../../index-
|
|
8
|
-
import "../../index-
|
|
9
|
-
import { S as SentinelOptions, _ as haversineDistanceKm, b as GeoLocation, c as sentinel, d as POW_SOLUTION_HEADER, f as createNonce, g as verifySolution, h as solveChallenge, l as POW_CHALLENGE_HEADER, m as parsePowHeader, p as hasLeadingZeroBits, u as POW_REASON_HEADER, v as normalizeEmail, x as SecurityAction, y as SENTINEL_ERROR_CODES } from "../../index-
|
|
10
|
-
import "../../index-
|
|
11
|
-
import "../../index-
|
|
12
|
-
import "../../index-
|
|
13
|
-
import "../../index-
|
|
14
|
-
import "../../index-
|
|
15
|
-
import "../../index-
|
|
16
|
-
import "../../index-
|
|
17
|
-
import "../../index-
|
|
18
|
-
import "../../index-
|
|
19
|
-
import "../../index-
|
|
20
|
-
import "../../index-
|
|
21
|
-
import "../../index-
|
|
22
|
-
import "../../index-
|
|
7
|
+
import "../../index-BYxt8qOP.mjs";
|
|
8
|
+
import "../../index-Dt6JhAV4.mjs";
|
|
9
|
+
import { S as SentinelOptions, _ as haversineDistanceKm, b as GeoLocation, c as sentinel, d as POW_SOLUTION_HEADER, f as createNonce, g as verifySolution, h as solveChallenge, l as POW_CHALLENGE_HEADER, m as parsePowHeader, p as hasLeadingZeroBits, u as POW_REASON_HEADER, v as normalizeEmail, x as SecurityAction, y as SENTINEL_ERROR_CODES } from "../../index-DWSbFJWU.mjs";
|
|
10
|
+
import "../../index-Czr_eQFE.mjs";
|
|
11
|
+
import "../../index-Dmjw6re9.mjs";
|
|
12
|
+
import "../../index-CLhlur69.mjs";
|
|
13
|
+
import "../../index-rcUKPBBy.mjs";
|
|
14
|
+
import "../../index-DYqQWdEv.mjs";
|
|
15
|
+
import "../../index-Clwezlxm.mjs";
|
|
16
|
+
import "../../index-D85KXOKz.mjs";
|
|
17
|
+
import "../../index-UDzaYKxq.mjs";
|
|
18
|
+
import "../../index-D5cyFBEb.mjs";
|
|
19
|
+
import "../../index-B-i2ob-C.mjs";
|
|
20
|
+
import "../../index-Cl9ITXy1.mjs";
|
|
21
|
+
import "../../index-DGRGLg16.mjs";
|
|
22
|
+
import "../../index-BRUQHEUF.mjs";
|
|
23
23
|
import "../../index-QmocqGM-.mjs";
|
|
24
|
-
import "../../index-
|
|
25
|
-
import "../../index-
|
|
26
|
-
import "../../index-
|
|
27
|
-
import "../../index-
|
|
28
|
-
import "../../index-
|
|
24
|
+
import "../../index-JzSy-vFn.mjs";
|
|
25
|
+
import "../../index-CGGitykk.mjs";
|
|
26
|
+
import "../../index-DWGj7du8.mjs";
|
|
27
|
+
import "../../index-CHMYUshk.mjs";
|
|
28
|
+
import "../../index-Hoybqpnt.mjs";
|
|
29
29
|
export { GeoLocation, POW_CHALLENGE_HEADER, POW_REASON_HEADER, POW_SOLUTION_HEADER, SENTINEL_ERROR_CODES, SecurityAction, SentinelOptions, createNonce, hasLeadingZeroBits, haversineDistanceKm, normalizeEmail, parsePowHeader, sentinel, solveChallenge, verifySolution };
|
|
@@ -14,9 +14,9 @@ import "../../get-tables-CKvSyDST.mjs";
|
|
|
14
14
|
import "../../types-DCPpFKUB.mjs";
|
|
15
15
|
import "../../misc-fKH4d5XQ.mjs";
|
|
16
16
|
import "../../session-BB3oqApE.mjs";
|
|
17
|
-
import "../../registry-
|
|
17
|
+
import "../../registry-GztLf62p.mjs";
|
|
18
18
|
import "../../haveibeenpwned-_3a5amT-.mjs";
|
|
19
19
|
import { a as hasLeadingZeroBits, c as verifySolution, i as createNonce, n as POW_REASON_HEADER, o as parsePowHeader, r as POW_SOLUTION_HEADER, s as solveChallenge, t as POW_CHALLENGE_HEADER } from "../../pow-DvkUopsK.mjs";
|
|
20
|
-
import { i as SENTINEL_ERROR_CODES, n as haversineDistanceKm, r as normalizeEmail, t as sentinel } from "../../sentinel-
|
|
20
|
+
import { i as SENTINEL_ERROR_CODES, n as haversineDistanceKm, r as normalizeEmail, t as sentinel } from "../../sentinel-b8XEetew.mjs";
|
|
21
21
|
|
|
22
22
|
export { POW_CHALLENGE_HEADER, POW_REASON_HEADER, POW_SOLUTION_HEADER, SENTINEL_ERROR_CODES, createNonce, hasLeadingZeroBits, haversineDistanceKm, normalizeEmail, parsePowHeader, sentinel, solveChallenge, verifySolution };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../../helper-BgOIGHsu.mjs";
|
|
2
|
-
import "../../index-
|
|
2
|
+
import "../../index-CZtQ0rUe.mjs";
|
|
3
3
|
import "../../types-DNQ9XQrB.mjs";
|
|
4
4
|
import "../../index-BL_t8cRp-C4QYBsac.mjs";
|
|
5
5
|
import "../../plugins-Dy_FrD4Y.mjs";
|
|
6
|
-
import { a as TwoFactorProvider, i as TwoFactorOptions, n as twoFactorClient, o as TwoFactorTable, r as TWO_FACTOR_ERROR_CODES, s as UserWithTwoFactor, t as twoFactor } from "../../index-
|
|
6
|
+
import { a as TwoFactorProvider, i as TwoFactorOptions, n as twoFactorClient, o as TwoFactorTable, r as TWO_FACTOR_ERROR_CODES, s as UserWithTwoFactor, t as twoFactor } from "../../index-DWGj7du8.mjs";
|
|
7
7
|
export { TWO_FACTOR_ERROR_CODES, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor, twoFactor, twoFactorClient };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "../../helper-BgOIGHsu.mjs";
|
|
2
2
|
import "../../plugins-Dy_FrD4Y.mjs";
|
|
3
|
-
import { n as username, r as USERNAME_ERROR_CODES, t as UsernameOptions } from "../../index-
|
|
3
|
+
import { n as username, r as USERNAME_ERROR_CODES, t as UsernameOptions } from "../../index-Hoybqpnt.mjs";
|
|
4
4
|
export { USERNAME_ERROR_CODES, UsernameOptions, username };
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
//#region src/utils/has-role.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a user's role field, a single role or a comma separated list,
|
|
4
|
+
* includes one of the given roles
|
|
5
|
+
*/
|
|
6
|
+
function hasRole(roleField, roles) {
|
|
7
|
+
if (typeof roleField !== "string") return false;
|
|
8
|
+
return roleField.split(",").map((role) => role.trim()).some((role) => roles.includes(role));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
1
12
|
//#region src/plugins/audit-log/events.ts
|
|
2
13
|
/**
|
|
3
14
|
* Audit event types recorded by the audit log plugin
|
|
@@ -74,7 +85,8 @@ const SECURITY_EVENT_TYPES = {
|
|
|
74
85
|
FREE_TRIAL_ABUSE: "security_free_trial_abuse",
|
|
75
86
|
COMPROMISED_PASSWORD: "security_compromised_password",
|
|
76
87
|
STALE_ACCOUNT: "security_stale_account",
|
|
77
|
-
INVALID_EMAIL: "security_invalid_email"
|
|
88
|
+
INVALID_EMAIL: "security_invalid_email",
|
|
89
|
+
UNBLOCKED: "security_unblocked"
|
|
78
90
|
};
|
|
79
91
|
const ALL_EVENT_TYPES = [...Object.values(AUDIT_EVENT_TYPES), ...Object.values(SECURITY_EVENT_TYPES)];
|
|
80
92
|
/** Endpoints that complete a sign-in by creating a session */
|
|
@@ -191,4 +203,4 @@ function getAuditRecorder(context) {
|
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
//#endregion
|
|
194
|
-
export { CREDENTIAL_SIGN_IN_PATHS as a, SIGN_IN_PATHS as c, AUDIT_EVENT_TYPES as i, VERIFICATION_PATHS as l, registerAuditRecorder as n, PATH_EVENTS as o, ALL_EVENT_TYPES as r, SECURITY_EVENT_TYPES as s, getAuditRecorder as t };
|
|
206
|
+
export { CREDENTIAL_SIGN_IN_PATHS as a, SIGN_IN_PATHS as c, AUDIT_EVENT_TYPES as i, VERIFICATION_PATHS as l, registerAuditRecorder as n, PATH_EVENTS as o, ALL_EVENT_TYPES as r, SECURITY_EVENT_TYPES as s, getAuditRecorder as t, hasRole as u };
|
|
@@ -2,14 +2,16 @@ import { t as getIp } from "./get-request-ip-DSjrEpGo.mjs";
|
|
|
2
2
|
import { t as safeJSONParse } from "./json-D0TBLy4q.mjs";
|
|
3
3
|
import { n as deleteSessionCookie } from "./cookies-BuvlgcA-.mjs";
|
|
4
4
|
import { n as mergeSchema } from "./date-CMr04reD.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { u as sessionMiddleware } from "./session-BB3oqApE.mjs";
|
|
6
|
+
import { a as CREDENTIAL_SIGN_IN_PATHS, c as SIGN_IN_PATHS, l as VERIFICATION_PATHS, s as SECURITY_EVENT_TYPES, t as getAuditRecorder, u as hasRole } from "./registry-GztLf62p.mjs";
|
|
6
7
|
import { t as getPasswordBreachCount } from "./haveibeenpwned-_3a5amT-.mjs";
|
|
7
8
|
import { c as verifySolution, i as createNonce, n as POW_REASON_HEADER, o as parsePowHeader, r as POW_SOLUTION_HEADER, t as POW_CHALLENGE_HEADER } from "./pow-DvkUopsK.mjs";
|
|
8
9
|
import { env } from "@gatwech/core/env";
|
|
9
10
|
import { PayvoAuthError } from "@gatwech/core/error";
|
|
10
11
|
import { defineErrorCodes } from "@gatwech/core/utils";
|
|
11
12
|
import { APIError } from "better-call";
|
|
12
|
-
import { createAuthMiddleware } from "@gatwech/core/api";
|
|
13
|
+
import { createAuthEndpoint, createAuthMiddleware } from "@gatwech/core/api";
|
|
14
|
+
import * as z$1 from "zod";
|
|
13
15
|
import { betterFetch } from "@better-fetch/fetch";
|
|
14
16
|
|
|
15
17
|
//#region src/plugins/sentinel/actions.ts
|
|
@@ -111,7 +113,9 @@ function challengeHeaders(decision) {
|
|
|
111
113
|
}
|
|
112
114
|
const SENTINEL_ERROR_CODES = defineErrorCodes({
|
|
113
115
|
POW_CHALLENGE_REQUIRED: "Challenge required",
|
|
114
|
-
SECURITY_BLOCKED: "Request blocked for security reasons"
|
|
116
|
+
SECURITY_BLOCKED: "Request blocked for security reasons",
|
|
117
|
+
SECURITY_ADMIN_REQUIRED: "Only an administrator can manage security blocks",
|
|
118
|
+
BLOCK_NOT_FOUND: "That block doesn't exist or has ended"
|
|
115
119
|
});
|
|
116
120
|
/** The error for a decision, or null when the request is allowed */
|
|
117
121
|
function errorForDecision(decision) {
|
|
@@ -520,6 +524,37 @@ const schema = {
|
|
|
520
524
|
required: true
|
|
521
525
|
}
|
|
522
526
|
} },
|
|
527
|
+
sentinelBlock: { fields: {
|
|
528
|
+
key: {
|
|
529
|
+
type: "string",
|
|
530
|
+
required: true,
|
|
531
|
+
unique: true
|
|
532
|
+
},
|
|
533
|
+
feature: {
|
|
534
|
+
type: "string",
|
|
535
|
+
required: true
|
|
536
|
+
},
|
|
537
|
+
kind: {
|
|
538
|
+
type: "string",
|
|
539
|
+
required: true
|
|
540
|
+
},
|
|
541
|
+
identifier: {
|
|
542
|
+
type: "string",
|
|
543
|
+
required: true
|
|
544
|
+
},
|
|
545
|
+
account: {
|
|
546
|
+
type: "string",
|
|
547
|
+
required: false
|
|
548
|
+
},
|
|
549
|
+
blockedUntil: {
|
|
550
|
+
type: "date",
|
|
551
|
+
required: true
|
|
552
|
+
},
|
|
553
|
+
createdAt: {
|
|
554
|
+
type: "date",
|
|
555
|
+
required: true
|
|
556
|
+
}
|
|
557
|
+
} },
|
|
523
558
|
sentinelSignIn: { fields: {
|
|
524
559
|
userId: {
|
|
525
560
|
type: "string",
|
|
@@ -632,6 +667,27 @@ async function deleteAllExpiredRows(auth, byPassLastCheckTime = false) {
|
|
|
632
667
|
auth.logger.error("[sentinel] Failed to delete expired rows:", error);
|
|
633
668
|
});
|
|
634
669
|
}
|
|
670
|
+
/** When each auth instance last deleted expired blocks */
|
|
671
|
+
const lastBlocksChecked = /* @__PURE__ */ new WeakMap();
|
|
672
|
+
/**
|
|
673
|
+
* Deletes blocks that ended a while ago, at most every 10 seconds per auth
|
|
674
|
+
* instance. Blocks live in the database even with secondary storage.
|
|
675
|
+
*/
|
|
676
|
+
async function deleteExpiredBlocks(auth, byPassLastCheckTime = false) {
|
|
677
|
+
const checkedAt = lastBlocksChecked.get(auth.adapter);
|
|
678
|
+
if (!byPassLastCheckTime && checkedAt && Date.now() - checkedAt < 1e4) return;
|
|
679
|
+
lastBlocksChecked.set(auth.adapter, Date.now());
|
|
680
|
+
await auth.adapter.deleteMany({
|
|
681
|
+
model: "sentinelBlock",
|
|
682
|
+
where: [{
|
|
683
|
+
field: "blockedUntil",
|
|
684
|
+
operator: "lt",
|
|
685
|
+
value: new Date(Date.now() - EXPIRED_ROW_GRACE_MS)
|
|
686
|
+
}]
|
|
687
|
+
}).catch((error) => {
|
|
688
|
+
auth.logger.error("[sentinel] Failed to delete expired blocks:", error);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
635
691
|
/**
|
|
636
692
|
* Counters and challenges, kept in secondary storage when it's configured
|
|
637
693
|
* and in the database otherwise
|
|
@@ -719,6 +775,92 @@ function createSentinelStore(auth) {
|
|
|
719
775
|
if (!row || new Date(row.expiresAt).getTime() <= Date.now()) return 0;
|
|
720
776
|
return row.count;
|
|
721
777
|
},
|
|
778
|
+
async recordBlock(block) {
|
|
779
|
+
deleteExpiredBlocks(auth);
|
|
780
|
+
const existing = await auth.adapter.findOne({
|
|
781
|
+
model: "sentinelBlock",
|
|
782
|
+
where: [{
|
|
783
|
+
field: "key",
|
|
784
|
+
value: block.key
|
|
785
|
+
}]
|
|
786
|
+
});
|
|
787
|
+
if (existing) {
|
|
788
|
+
await auth.adapter.update({
|
|
789
|
+
model: "sentinelBlock",
|
|
790
|
+
where: [{
|
|
791
|
+
field: "id",
|
|
792
|
+
value: existing.id
|
|
793
|
+
}],
|
|
794
|
+
update: {
|
|
795
|
+
...block,
|
|
796
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
await auth.adapter.create({
|
|
802
|
+
model: "sentinelBlock",
|
|
803
|
+
data: {
|
|
804
|
+
...block,
|
|
805
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
806
|
+
}
|
|
807
|
+
}).catch(async () => {
|
|
808
|
+
await auth.adapter.updateMany({
|
|
809
|
+
model: "sentinelBlock",
|
|
810
|
+
where: [{
|
|
811
|
+
field: "key",
|
|
812
|
+
value: block.key
|
|
813
|
+
}],
|
|
814
|
+
update: {
|
|
815
|
+
...block,
|
|
816
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
},
|
|
821
|
+
async listBlocks(page) {
|
|
822
|
+
const where = [{
|
|
823
|
+
field: "blockedUntil",
|
|
824
|
+
operator: "gt",
|
|
825
|
+
value: /* @__PURE__ */ new Date()
|
|
826
|
+
}];
|
|
827
|
+
const [blocks, total] = await Promise.all([auth.adapter.findMany({
|
|
828
|
+
model: "sentinelBlock",
|
|
829
|
+
where,
|
|
830
|
+
limit: page.limit,
|
|
831
|
+
offset: page.offset,
|
|
832
|
+
sortBy: {
|
|
833
|
+
field: "createdAt",
|
|
834
|
+
direction: "desc"
|
|
835
|
+
}
|
|
836
|
+
}), auth.adapter.count({
|
|
837
|
+
model: "sentinelBlock",
|
|
838
|
+
where
|
|
839
|
+
})]);
|
|
840
|
+
return {
|
|
841
|
+
blocks,
|
|
842
|
+
total
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
async removeBlock(id) {
|
|
846
|
+
const block = await auth.adapter.findOne({
|
|
847
|
+
model: "sentinelBlock",
|
|
848
|
+
where: [{
|
|
849
|
+
field: "id",
|
|
850
|
+
value: id
|
|
851
|
+
}]
|
|
852
|
+
});
|
|
853
|
+
if (!block) return null;
|
|
854
|
+
await auth.adapter.delete({
|
|
855
|
+
model: "sentinelBlock",
|
|
856
|
+
where: [{
|
|
857
|
+
field: "id",
|
|
858
|
+
value: id
|
|
859
|
+
}]
|
|
860
|
+
});
|
|
861
|
+
await this.reset(`${block.key}:blocked`);
|
|
862
|
+
return new Date(block.blockedUntil).getTime() > Date.now() ? block : null;
|
|
863
|
+
},
|
|
722
864
|
async reset(name) {
|
|
723
865
|
const key = PREFIX + name;
|
|
724
866
|
if (storage) {
|
|
@@ -1098,6 +1240,49 @@ const sentinel = (options) => {
|
|
|
1098
1240
|
}));
|
|
1099
1241
|
return Math.max(0, ...failures);
|
|
1100
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Blocks an attempt counter for the cooldown and clears its counts. The
|
|
1245
|
+
* block is also recorded so an admin can list and lift it; it holds
|
|
1246
|
+
* without the record.
|
|
1247
|
+
*/
|
|
1248
|
+
async function blockKey(auth, store, key, also = []) {
|
|
1249
|
+
if (!credentialStuffing) return;
|
|
1250
|
+
const { cooldownSeconds } = credentialStuffing;
|
|
1251
|
+
await store.increment(`${key}:blocked`, cooldownSeconds);
|
|
1252
|
+
await Promise.all([key, ...also].map((name) => store.reset(name)));
|
|
1253
|
+
await store.recordBlock({
|
|
1254
|
+
key,
|
|
1255
|
+
feature: "credential_stuffing",
|
|
1256
|
+
...describeAttemptKey(key),
|
|
1257
|
+
blockedUntil: new Date(Date.now() + cooldownSeconds * 1e3)
|
|
1258
|
+
}).catch((e) => {
|
|
1259
|
+
auth.logger.error("[sentinel] Failed to record block", e);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Turns the counters that passed the block threshold into blocks that last
|
|
1264
|
+
* the cooldown, so they end then and can be listed and lifted. `count`
|
|
1265
|
+
* includes the attempt just made.
|
|
1266
|
+
*/
|
|
1267
|
+
async function blockOverThreshold(auth, store, keys) {
|
|
1268
|
+
if (!credentialStuffing) return;
|
|
1269
|
+
const { thresholds } = credentialStuffing;
|
|
1270
|
+
for (const { scope, account } of keys) {
|
|
1271
|
+
const [total, succeeded] = await Promise.all([store.get(scope), store.get(`${scope}:succeeded`)]);
|
|
1272
|
+
if (total - succeeded - 1 >= thresholds.block) await blockKey(auth, store, scope, [`${scope}:succeeded`]);
|
|
1273
|
+
if (account && await store.get(account) - 1 >= thresholds.block) await blockKey(auth, store, account);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
/** What an attempt counter's key is for: `cs:ip:<ip>` or `cs:visitor:<id>`, plus `:account:<email>` */
|
|
1277
|
+
function describeAttemptKey(key) {
|
|
1278
|
+
const accountAt = key.indexOf(":account:");
|
|
1279
|
+
const [, kind = "visitor", ...identifier] = (accountAt === -1 ? key : key.slice(0, accountAt)).split(":");
|
|
1280
|
+
return {
|
|
1281
|
+
kind,
|
|
1282
|
+
identifier: identifier.join(":"),
|
|
1283
|
+
account: accountAt === -1 ? null : key.slice(accountAt + 9)
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1101
1286
|
function counterNames(keys) {
|
|
1102
1287
|
return keys.flatMap(({ scope, account }) => account ? [scope, account] : [scope]);
|
|
1103
1288
|
}
|
|
@@ -1299,6 +1484,10 @@ const sentinel = (options) => {
|
|
|
1299
1484
|
}));
|
|
1300
1485
|
}
|
|
1301
1486
|
}
|
|
1487
|
+
const adminRoles = opts.adminRoles ?? ["admin"];
|
|
1488
|
+
function requireAdmin(user) {
|
|
1489
|
+
if (!hasRole(user.role, adminRoles)) throw new APIError("FORBIDDEN", { message: SENTINEL_ERROR_CODES.SECURITY_ADMIN_REQUIRED });
|
|
1490
|
+
}
|
|
1302
1491
|
return {
|
|
1303
1492
|
id: "sentinel",
|
|
1304
1493
|
init(ctx) {
|
|
@@ -1425,7 +1614,10 @@ const sentinel = (options) => {
|
|
|
1425
1614
|
failedAttempts: failures,
|
|
1426
1615
|
thresholds
|
|
1427
1616
|
};
|
|
1428
|
-
if (blocked || failures >= thresholds.block)
|
|
1617
|
+
if (blocked || failures >= thresholds.block) {
|
|
1618
|
+
if (!blocked && credentialStuffing.action !== "log") await blockOverThreshold(ctx.context, store, keys);
|
|
1619
|
+
return act(ctx, store, details, credentialStuffing.action === "log" ? "log" : "block", SECURITY_EVENT_TYPES.CREDENTIAL_STUFFING, "credential_stuffing", metadata);
|
|
1620
|
+
}
|
|
1429
1621
|
if (failures >= thresholds.challenge) return act(ctx, store, details, credentialStuffing.action === "log" ? "log" : "challenge", SECURITY_EVENT_TYPES.CREDENTIAL_STUFFING, "credential_stuffing", metadata);
|
|
1430
1622
|
return { outcome: "allow" };
|
|
1431
1623
|
}));
|
|
@@ -1479,7 +1671,7 @@ const sentinel = (options) => {
|
|
|
1479
1671
|
try {
|
|
1480
1672
|
const store = createSentinelStore(ctx.context);
|
|
1481
1673
|
const keys = attemptKeys(await requestDetails(ctx));
|
|
1482
|
-
const { windowSeconds,
|
|
1674
|
+
const { windowSeconds, thresholds } = credentialStuffing;
|
|
1483
1675
|
if (status < 400) for (const { scope, account } of keys) {
|
|
1484
1676
|
const [total, succeeded, own] = await Promise.all([
|
|
1485
1677
|
store.get(scope),
|
|
@@ -1493,10 +1685,7 @@ const sentinel = (options) => {
|
|
|
1493
1685
|
} else await store.increment(`${scope}:succeeded`, windowSeconds);
|
|
1494
1686
|
}
|
|
1495
1687
|
else if (!SENTINEL_STATUSES.has(status) && status < 500) {
|
|
1496
|
-
const block =
|
|
1497
|
-
await store.increment(`${key}:blocked`, cooldownSeconds);
|
|
1498
|
-
await Promise.all([key, ...also].map((name) => store.reset(name)));
|
|
1499
|
-
};
|
|
1688
|
+
const block = (key, also = []) => blockKey(ctx.context, store, key, also);
|
|
1500
1689
|
for (const { scope, account } of keys) {
|
|
1501
1690
|
const [total, succeeded] = await Promise.all([store.get(scope), store.get(`${scope}:succeeded`)]);
|
|
1502
1691
|
if (total - succeeded >= thresholds.block) await block(scope, [`${scope}:succeeded`]);
|
|
@@ -1640,6 +1829,73 @@ const sentinel = (options) => {
|
|
|
1640
1829
|
...schema,
|
|
1641
1830
|
...normalizedEmailSchema
|
|
1642
1831
|
} : schema, opts.schema),
|
|
1832
|
+
endpoints: {
|
|
1833
|
+
listBlocks: createAuthEndpoint("/sentinel/blocks", {
|
|
1834
|
+
method: "GET",
|
|
1835
|
+
use: [sessionMiddleware],
|
|
1836
|
+
query: z$1.object({
|
|
1837
|
+
limit: z$1.string().or(z$1.number()).optional(),
|
|
1838
|
+
offset: z$1.string().or(z$1.number()).optional()
|
|
1839
|
+
}).optional(),
|
|
1840
|
+
metadata: { openapi: {
|
|
1841
|
+
operationId: "listSentinelBlocks",
|
|
1842
|
+
summary: "List active blocks",
|
|
1843
|
+
description: "List the visitors and IP addresses sentinel is blocking, for admins"
|
|
1844
|
+
} }
|
|
1845
|
+
}, async (ctx) => {
|
|
1846
|
+
requireAdmin(ctx.context.session.user);
|
|
1847
|
+
const limit = Math.min(Math.max(Math.trunc(Number(ctx.query?.limit) || 50), 1), 100);
|
|
1848
|
+
const offset = Math.max(Math.trunc(Number(ctx.query?.offset) || 0), 0);
|
|
1849
|
+
const { blocks, total } = await createSentinelStore(ctx.context).listBlocks({
|
|
1850
|
+
limit,
|
|
1851
|
+
offset
|
|
1852
|
+
});
|
|
1853
|
+
return ctx.json({
|
|
1854
|
+
blocks,
|
|
1855
|
+
total,
|
|
1856
|
+
limit,
|
|
1857
|
+
offset
|
|
1858
|
+
});
|
|
1859
|
+
}),
|
|
1860
|
+
unblock: createAuthEndpoint("/sentinel/unblock", {
|
|
1861
|
+
method: "POST",
|
|
1862
|
+
use: [sessionMiddleware],
|
|
1863
|
+
body: z$1.object({ id: z$1.string() }),
|
|
1864
|
+
metadata: { openapi: {
|
|
1865
|
+
operationId: "unblockSentinel",
|
|
1866
|
+
summary: "Lift a block",
|
|
1867
|
+
description: "Lift a block before its cooldown ends, for admins. Recorded in the audit log."
|
|
1868
|
+
} }
|
|
1869
|
+
}, async (ctx) => {
|
|
1870
|
+
const { user } = ctx.context.session;
|
|
1871
|
+
requireAdmin(user);
|
|
1872
|
+
const block = await createSentinelStore(ctx.context).removeBlock(ctx.body.id);
|
|
1873
|
+
if (!block) throw new APIError("NOT_FOUND", { message: SENTINEL_ERROR_CODES.BLOCK_NOT_FOUND });
|
|
1874
|
+
await getAuditRecorder(ctx.context)?.(ctx, {
|
|
1875
|
+
eventType: SECURITY_EVENT_TYPES.UNBLOCKED,
|
|
1876
|
+
status: "success",
|
|
1877
|
+
userId: user.id,
|
|
1878
|
+
identifier: user.email,
|
|
1879
|
+
eventData: {
|
|
1880
|
+
key: block.key,
|
|
1881
|
+
kind: block.kind,
|
|
1882
|
+
blocked: block.identifier,
|
|
1883
|
+
account: block.account
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
return ctx.json({ success: true });
|
|
1887
|
+
}),
|
|
1888
|
+
deleteExpired: createAuthEndpoint("/sentinel/delete-expired", {
|
|
1889
|
+
method: "POST",
|
|
1890
|
+
metadata: {
|
|
1891
|
+
SERVER_ONLY: true,
|
|
1892
|
+
client: false
|
|
1893
|
+
}
|
|
1894
|
+
}, async (ctx) => {
|
|
1895
|
+
await Promise.all([deleteExpiredBlocks(ctx.context, true), deleteAllExpiredRows(ctx.context, true)]);
|
|
1896
|
+
return ctx.json({ success: true });
|
|
1897
|
+
})
|
|
1898
|
+
},
|
|
1643
1899
|
$ERROR_CODES: SENTINEL_ERROR_CODES,
|
|
1644
1900
|
options: opts
|
|
1645
1901
|
};
|
|
@@ -74,6 +74,7 @@ export declare const SECURITY_EVENT_TYPES: {
|
|
|
74
74
|
readonly COMPROMISED_PASSWORD: "security_compromised_password";
|
|
75
75
|
readonly STALE_ACCOUNT: "security_stale_account";
|
|
76
76
|
readonly INVALID_EMAIL: "security_invalid_email";
|
|
77
|
+
readonly UNBLOCKED: "security_unblocked";
|
|
77
78
|
};
|
|
78
79
|
export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[keyof typeof AUDIT_EVENT_TYPES] | (typeof SECURITY_EVENT_TYPES)[keyof typeof SECURITY_EVENT_TYPES];
|
|
79
80
|
export declare const ALL_EVENT_TYPES: AuditEventType[];
|
|
@@ -527,6 +527,51 @@ export declare const auditLog: (options?: AuditLogOptions | undefined) => {
|
|
|
527
527
|
};
|
|
528
528
|
path: "/audit-log/get-event-types";
|
|
529
529
|
};
|
|
530
|
+
deleteOldEvents: {
|
|
531
|
+
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
|
|
532
|
+
body: {
|
|
533
|
+
olderThanDays: number;
|
|
534
|
+
};
|
|
535
|
+
} & {
|
|
536
|
+
method?: "POST" | undefined;
|
|
537
|
+
} & {
|
|
538
|
+
query?: Record<string, any> | undefined;
|
|
539
|
+
} & {
|
|
540
|
+
params?: Record<string, any>;
|
|
541
|
+
} & {
|
|
542
|
+
request?: Request;
|
|
543
|
+
} & {
|
|
544
|
+
headers?: HeadersInit;
|
|
545
|
+
} & {
|
|
546
|
+
asResponse?: boolean;
|
|
547
|
+
returnHeaders?: boolean;
|
|
548
|
+
use?: import("better-call").Middleware[];
|
|
549
|
+
path?: string;
|
|
550
|
+
} & {
|
|
551
|
+
asResponse?: AsResponse | undefined;
|
|
552
|
+
returnHeaders?: ReturnHeaders | undefined;
|
|
553
|
+
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
|
|
554
|
+
headers: Headers;
|
|
555
|
+
response: {
|
|
556
|
+
deleted: number;
|
|
557
|
+
};
|
|
558
|
+
} : {
|
|
559
|
+
deleted: number;
|
|
560
|
+
}>;
|
|
561
|
+
options: {
|
|
562
|
+
method: "POST";
|
|
563
|
+
body: z.ZodObject<{
|
|
564
|
+
olderThanDays: z.ZodNumber;
|
|
565
|
+
}, z.core.$strip>;
|
|
566
|
+
metadata: {
|
|
567
|
+
SERVER_ONLY: true;
|
|
568
|
+
client: boolean;
|
|
569
|
+
};
|
|
570
|
+
} & {
|
|
571
|
+
use: any[];
|
|
572
|
+
};
|
|
573
|
+
path: "/audit-log/delete-old-events";
|
|
574
|
+
};
|
|
530
575
|
};
|
|
531
576
|
schema: {
|
|
532
577
|
auditLog: {
|
|
@@ -39,6 +39,8 @@ export declare function executeAction(params: {
|
|
|
39
39
|
export declare const SENTINEL_ERROR_CODES: {
|
|
40
40
|
readonly POW_CHALLENGE_REQUIRED: "Challenge required";
|
|
41
41
|
readonly SECURITY_BLOCKED: "Request blocked for security reasons";
|
|
42
|
+
readonly SECURITY_ADMIN_REQUIRED: "Only an administrator can manage security blocks";
|
|
43
|
+
readonly BLOCK_NOT_FOUND: "That block doesn't exist or has ended";
|
|
42
44
|
};
|
|
43
45
|
/** The error for a decision, or null when the request is allowed */
|
|
44
46
|
export declare function errorForDecision(decision: Decision): ({
|