@gatwech/auth 1.0.0-beta.22 → 1.0.0-beta.23

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.
Files changed (81) hide show
  1. package/dist/adapters/index.d.mts +1 -1
  2. package/dist/api/index.d.mts +1 -1
  3. package/dist/{audit-log-DgqHJ-uv.mjs → audit-log-DhRRCMrv.mjs} +21 -6
  4. package/dist/client/index.d.mts +1 -1
  5. package/dist/client/plugins/index.d.mts +24 -24
  6. package/dist/client/react/index.d.mts +15 -15
  7. package/dist/{client-DO9MgP_0.d.mts → client-CDaFOX7A.d.mts} +4 -4
  8. package/dist/{client-ukvqpcbI.d.mts → client-D8XKNpua.d.mts} +1 -1
  9. package/dist/cookies/index.d.mts +2 -2
  10. package/dist/db/index.d.mts +1 -1
  11. package/dist/{index-DlS-HqZR.d.mts → index-382PsmkK.d.mts} +2 -2
  12. package/dist/{index-DngQlUSI.d.mts → index-3lfPQHhX.d.mts} +113 -113
  13. package/dist/{index-CRiKKdkb.d.mts → index-B4aWSJEd.d.mts} +6 -6
  14. package/dist/{index-hP8gvyp-.d.mts → index-B5r4oL5q.d.mts} +5 -5
  15. package/dist/{index-AiKdOFSd.d.mts → index-B7iZf_Dl.d.mts} +387 -49
  16. package/dist/{index-CTSeVJJB.d.mts → index-BhYE1_9j.d.mts} +1 -1
  17. package/dist/{index-D3uQsW8T.d.mts → index-Blq8KrTy.d.mts} +30 -30
  18. package/dist/{index-Dkm27DLN.d.mts → index-C-z4TCHD.d.mts} +10 -10
  19. package/dist/{index-Do8AV4PF.d.mts → index-C8F7PsET.d.mts} +13 -13
  20. package/dist/{index-DSoiUc1C.d.mts → index-CExSHSFz.d.mts} +166 -166
  21. package/dist/{index-CxhQu1HM.d.mts → index-CIxFDcfQ.d.mts} +2 -2
  22. package/dist/{index-eOfQxlQV.d.mts → index-COFPq-mg.d.mts} +13 -13
  23. package/dist/{index-B4hJVVRn.d.mts → index-CWwQ1Q7t.d.mts} +19 -19
  24. package/dist/{index-BzzhaGLe.d.mts → index-CWxKeTtE.d.mts} +6 -6
  25. package/dist/{index-Dl0bD1_6.d.mts → index-D0SgItjY.d.mts} +6 -6
  26. package/dist/{index-BC0aAKsT.d.mts → index-DCliG2n6.d.mts} +3 -3
  27. package/dist/{index-D-rjuF7p.d.mts → index-DPVukjga.d.mts} +9 -9
  28. package/dist/{index-SYRh5FW7.d.mts → index-DUu0ieh6.d.mts} +8 -8
  29. package/dist/{index-pdl9MzQu.d.mts → index-De2a2OyC.d.mts} +9 -9
  30. package/dist/{index-BfLofiXO.d.mts → index-Dg6Ftb6f.d.mts} +9 -9
  31. package/dist/{index-CRNh-5Tc.d.mts → index-Dgyj1fTk.d.mts} +7 -7
  32. package/dist/{index-DKirx_GW.d.mts → index-DxIqV3rU.d.mts} +4 -4
  33. package/dist/{index-CBGNX7W1.d.mts → index-DxRAXHrZ.d.mts} +6 -6
  34. package/dist/index.d.mts +1 -1
  35. package/dist/integrations/next-js.d.mts +4 -4
  36. package/dist/integrations/node.d.mts +1 -1
  37. package/dist/integrations/react-start.d.mts +4 -4
  38. package/dist/oauth2/index.d.mts +1 -1
  39. package/dist/plugins/admin/index.d.mts +2 -2
  40. package/dist/plugins/anonymous/index.d.mts +2 -2
  41. package/dist/plugins/audit-log/client.d.mts +23 -23
  42. package/dist/plugins/audit-log/index.d.mts +22 -22
  43. package/dist/plugins/audit-log/index.mjs +2 -2
  44. package/dist/plugins/bearer/index.d.mts +1 -1
  45. package/dist/plugins/captcha/index.d.mts +1 -1
  46. package/dist/plugins/custom-session/index.d.mts +2 -2
  47. package/dist/plugins/device-authorization/index.d.mts +1 -1
  48. package/dist/plugins/email-otp/index.d.mts +1 -1
  49. package/dist/plugins/generic-oauth/index.d.mts +2 -2
  50. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  51. package/dist/plugins/index.d.mts +22 -22
  52. package/dist/plugins/index.mjs +3 -3
  53. package/dist/plugins/jwt/index.d.mts +2 -2
  54. package/dist/plugins/magic-link/index.d.mts +1 -1
  55. package/dist/plugins/multi-session/index.d.mts +1 -1
  56. package/dist/plugins/oauth-proxy/index.d.mts +1 -1
  57. package/dist/plugins/one-tap/index.d.mts +1 -1
  58. package/dist/plugins/one-time-token/index.d.mts +2 -2
  59. package/dist/plugins/one-time-token/index.mjs +3 -3
  60. package/dist/plugins/organization/index.d.mts +2 -2
  61. package/dist/plugins/passkey/index.d.mts +2 -2
  62. package/dist/plugins/phone-number/index.d.mts +2 -2
  63. package/dist/plugins/sentinel/client.d.mts +23 -23
  64. package/dist/plugins/sentinel/index.d.mts +22 -22
  65. package/dist/plugins/sentinel/index.mjs +2 -2
  66. package/dist/plugins/two-factor/index.d.mts +2 -2
  67. package/dist/plugins/username/index.d.mts +1 -1
  68. package/dist/{registry-CqG_3Qnx.mjs → registry-GztLf62p.mjs} +14 -2
  69. package/dist/{sentinel-B-2jmmZh.mjs → sentinel-CYtaI3eO.mjs} +234 -3
  70. package/dist/src/plugins/audit-log/events.d.ts +1 -0
  71. package/dist/src/plugins/audit-log/index.d.ts +45 -0
  72. package/dist/src/plugins/sentinel/actions.d.ts +2 -0
  73. package/dist/src/plugins/sentinel/index.d.ts +247 -0
  74. package/dist/src/plugins/sentinel/schema.d.ts +37 -0
  75. package/dist/src/plugins/sentinel/store.d.ts +34 -0
  76. package/dist/src/plugins/sentinel/types.d.ts +7 -0
  77. package/dist/src/utils/has-role.d.ts +5 -0
  78. package/dist/test-utils/index.d.mts +273 -273
  79. package/dist/test-utils/index.mjs +3 -3
  80. package/dist/types/index.d.mts +1 -1
  81. package/package.json +3 -3
@@ -1,29 +1,29 @@
1
1
  import "../../helper-BgOIGHsu.mjs";
2
- import "../../index-DngQlUSI.mjs";
2
+ import "../../index-3lfPQHhX.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-D3uQsW8T.mjs";
8
- import "../../index-CRiKKdkb.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-AiKdOFSd.mjs";
10
- import "../../index-BzzhaGLe.mjs";
11
- import "../../index-DlS-HqZR.mjs";
12
- import "../../index-Dl0bD1_6.mjs";
13
- import "../../index-CBGNX7W1.mjs";
14
- import "../../index-Do8AV4PF.mjs";
15
- import "../../index-SYRh5FW7.mjs";
16
- import "../../index-BC0aAKsT.mjs";
17
- import "../../index-D-rjuF7p.mjs";
18
- import "../../index-DKirx_GW.mjs";
19
- import "../../index-BfLofiXO.mjs";
20
- import "../../index-Dkm27DLN.mjs";
21
- import "../../index-CxhQu1HM.mjs";
22
- import "../../index-hP8gvyp-.mjs";
7
+ import "../../index-Blq8KrTy.mjs";
8
+ import "../../index-B4aWSJEd.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-B7iZf_Dl.mjs";
10
+ import "../../index-CWxKeTtE.mjs";
11
+ import "../../index-382PsmkK.mjs";
12
+ import "../../index-D0SgItjY.mjs";
13
+ import "../../index-DxRAXHrZ.mjs";
14
+ import "../../index-C8F7PsET.mjs";
15
+ import "../../index-DUu0ieh6.mjs";
16
+ import "../../index-DCliG2n6.mjs";
17
+ import "../../index-DPVukjga.mjs";
18
+ import "../../index-DxIqV3rU.mjs";
19
+ import "../../index-Dg6Ftb6f.mjs";
20
+ import "../../index-C-z4TCHD.mjs";
21
+ import "../../index-CIxFDcfQ.mjs";
22
+ import "../../index-B5r4oL5q.mjs";
23
23
  import "../../index-QmocqGM-.mjs";
24
- import "../../index-DSoiUc1C.mjs";
25
- import "../../index-CRNh-5Tc.mjs";
26
- import "../../index-B4hJVVRn.mjs";
27
- import "../../index-eOfQxlQV.mjs";
28
- import "../../index-pdl9MzQu.mjs";
24
+ import "../../index-CExSHSFz.mjs";
25
+ import "../../index-Dgyj1fTk.mjs";
26
+ import "../../index-CWwQ1Q7t.mjs";
27
+ import "../../index-COFPq-mg.mjs";
28
+ import "../../index-De2a2OyC.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-CqG_3Qnx.mjs";
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-B-2jmmZh.mjs";
20
+ import { i as SENTINEL_ERROR_CODES, n as haversineDistanceKm, r as normalizeEmail, t as sentinel } from "../../sentinel-CYtaI3eO.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-DngQlUSI.mjs";
2
+ import "../../index-3lfPQHhX.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-B4hJVVRn.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-CWwQ1Q7t.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-pdl9MzQu.mjs";
3
+ import { n as username, r as USERNAME_ERROR_CODES, t as UsernameOptions } from "../../index-De2a2OyC.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 { a as CREDENTIAL_SIGN_IN_PATHS, c as SIGN_IN_PATHS, l as VERIFICATION_PATHS, s as SECURITY_EVENT_TYPES, t as getAuditRecorder } from "./registry-CqG_3Qnx.mjs";
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,16 @@ const sentinel = (options) => {
1098
1240
  }));
1099
1241
  return Math.max(0, ...failures);
1100
1242
  }
1243
+ /** What an attempt counter's key is for: `cs:ip:<ip>` or `cs:visitor:<id>`, plus `:account:<email>` */
1244
+ function describeAttemptKey(key) {
1245
+ const accountAt = key.indexOf(":account:");
1246
+ const [, kind = "visitor", ...identifier] = (accountAt === -1 ? key : key.slice(0, accountAt)).split(":");
1247
+ return {
1248
+ kind,
1249
+ identifier: identifier.join(":"),
1250
+ account: accountAt === -1 ? null : key.slice(accountAt + 9)
1251
+ };
1252
+ }
1101
1253
  function counterNames(keys) {
1102
1254
  return keys.flatMap(({ scope, account }) => account ? [scope, account] : [scope]);
1103
1255
  }
@@ -1299,6 +1451,10 @@ const sentinel = (options) => {
1299
1451
  }));
1300
1452
  }
1301
1453
  }
1454
+ const adminRoles = opts.adminRoles ?? ["admin"];
1455
+ function requireAdmin(user) {
1456
+ if (!hasRole(user.role, adminRoles)) throw new APIError("FORBIDDEN", { message: SENTINEL_ERROR_CODES.SECURITY_ADMIN_REQUIRED });
1457
+ }
1302
1458
  return {
1303
1459
  id: "sentinel",
1304
1460
  init(ctx) {
@@ -1496,6 +1652,14 @@ const sentinel = (options) => {
1496
1652
  const block = async (key, also = []) => {
1497
1653
  await store.increment(`${key}:blocked`, cooldownSeconds);
1498
1654
  await Promise.all([key, ...also].map((name) => store.reset(name)));
1655
+ await store.recordBlock({
1656
+ key,
1657
+ feature: "credential_stuffing",
1658
+ ...describeAttemptKey(key),
1659
+ blockedUntil: new Date(Date.now() + cooldownSeconds * 1e3)
1660
+ }).catch((e) => {
1661
+ ctx.context.logger.error("[sentinel] Failed to record block", e);
1662
+ });
1499
1663
  };
1500
1664
  for (const { scope, account } of keys) {
1501
1665
  const [total, succeeded] = await Promise.all([store.get(scope), store.get(`${scope}:succeeded`)]);
@@ -1640,6 +1804,73 @@ const sentinel = (options) => {
1640
1804
  ...schema,
1641
1805
  ...normalizedEmailSchema
1642
1806
  } : schema, opts.schema),
1807
+ endpoints: {
1808
+ listBlocks: createAuthEndpoint("/sentinel/blocks", {
1809
+ method: "GET",
1810
+ use: [sessionMiddleware],
1811
+ query: z$1.object({
1812
+ limit: z$1.string().or(z$1.number()).optional(),
1813
+ offset: z$1.string().or(z$1.number()).optional()
1814
+ }).optional(),
1815
+ metadata: { openapi: {
1816
+ operationId: "listSentinelBlocks",
1817
+ summary: "List active blocks",
1818
+ description: "List the visitors and IP addresses sentinel is blocking, for admins"
1819
+ } }
1820
+ }, async (ctx) => {
1821
+ requireAdmin(ctx.context.session.user);
1822
+ const limit = Math.min(Math.max(Math.trunc(Number(ctx.query?.limit) || 50), 1), 100);
1823
+ const offset = Math.max(Math.trunc(Number(ctx.query?.offset) || 0), 0);
1824
+ const { blocks, total } = await createSentinelStore(ctx.context).listBlocks({
1825
+ limit,
1826
+ offset
1827
+ });
1828
+ return ctx.json({
1829
+ blocks,
1830
+ total,
1831
+ limit,
1832
+ offset
1833
+ });
1834
+ }),
1835
+ unblock: createAuthEndpoint("/sentinel/unblock", {
1836
+ method: "POST",
1837
+ use: [sessionMiddleware],
1838
+ body: z$1.object({ id: z$1.string() }),
1839
+ metadata: { openapi: {
1840
+ operationId: "unblockSentinel",
1841
+ summary: "Lift a block",
1842
+ description: "Lift a block before its cooldown ends, for admins. Recorded in the audit log."
1843
+ } }
1844
+ }, async (ctx) => {
1845
+ const { user } = ctx.context.session;
1846
+ requireAdmin(user);
1847
+ const block = await createSentinelStore(ctx.context).removeBlock(ctx.body.id);
1848
+ if (!block) throw new APIError("NOT_FOUND", { message: SENTINEL_ERROR_CODES.BLOCK_NOT_FOUND });
1849
+ await getAuditRecorder(ctx.context)?.(ctx, {
1850
+ eventType: SECURITY_EVENT_TYPES.UNBLOCKED,
1851
+ status: "success",
1852
+ userId: user.id,
1853
+ identifier: user.email,
1854
+ eventData: {
1855
+ key: block.key,
1856
+ kind: block.kind,
1857
+ blocked: block.identifier,
1858
+ account: block.account
1859
+ }
1860
+ });
1861
+ return ctx.json({ success: true });
1862
+ }),
1863
+ deleteExpired: createAuthEndpoint("/sentinel/delete-expired", {
1864
+ method: "POST",
1865
+ metadata: {
1866
+ SERVER_ONLY: true,
1867
+ client: false
1868
+ }
1869
+ }, async (ctx) => {
1870
+ await Promise.all([deleteExpiredBlocks(ctx.context, true), deleteAllExpiredRows(ctx.context, true)]);
1871
+ return ctx.json({ success: true });
1872
+ })
1873
+ },
1643
1874
  $ERROR_CODES: SENTINEL_ERROR_CODES,
1644
1875
  options: opts
1645
1876
  };
@@ -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): ({