@gatwech/auth 1.0.0-beta.21 → 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.
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/prisma-adapter/index.mjs +1 -1
- package/dist/{admin-BzUJL6tr.mjs → admin-rd-xHhnP.mjs} +1 -1
- package/dist/{anonymous-BjlTk4gl.mjs → anonymous-qSasVeIe.mjs} +1 -1
- package/dist/api/index.d.mts +1 -1
- package/dist/api/index.mjs +1 -1
- package/dist/{api-Dk8Yj_J5.mjs → api-BV65Y-Rk.mjs} +5 -0
- package/dist/{audit-log-DgqHJ-uv.mjs → audit-log-DhRRCMrv.mjs} +21 -6
- package/dist/{auth-BoEA9bN7.mjs → auth-DtOjarwu.mjs} +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/plugins/index.d.mts +46 -46
- package/dist/client/react/index.d.mts +15 -15
- package/dist/{client-Da7tq8m6.d.mts → client-CDaFOX7A.d.mts} +4 -4
- package/dist/{client-B00gkznv.d.mts → client-D8XKNpua.d.mts} +1 -1
- package/dist/cookies/index.d.mts +2 -2
- package/dist/db/index.d.mts +1 -1
- package/dist/{email-otp-Dw0DypjQ.mjs → email-otp-BmgAht1S.mjs} +1 -1
- package/dist/{generic-oauth-DFj27JY8.mjs → generic-oauth-C5_AXj2z.mjs} +1 -1
- package/dist/{haveibeenpwned-CzNda7Tn.mjs → haveibeenpwned-_3a5amT-.mjs} +1 -1
- package/dist/{index-C-IDh5yy.d.mts → index-382PsmkK.d.mts} +2 -2
- package/dist/{index-BjCQ3jmE.d.mts → index-3lfPQHhX.d.mts} +118 -118
- package/dist/{index-CMG4Pnen.d.mts → index-B4aWSJEd.d.mts} +6 -6
- package/dist/{index-BWYXZVsV.d.mts → index-B5r4oL5q.d.mts} +5 -5
- package/dist/{index-BKy-8t2n.d.mts → index-B7iZf_Dl.d.mts} +449 -112
- package/dist/{index-VUyESU4p.d.mts → index-BhYE1_9j.d.mts} +1 -1
- package/dist/{index-IX1to9TB.d.mts → index-Blq8KrTy.d.mts} +33 -33
- package/dist/{index-oNlp4SvD.d.mts → index-C-z4TCHD.d.mts} +10 -10
- package/dist/{index-D1OZ2e4m.d.mts → index-C8F7PsET.d.mts} +13 -13
- package/dist/{index-TrOnY3pM.d.mts → index-CExSHSFz.d.mts} +169 -169
- package/dist/{index-iRoaTsOj.d.mts → index-CIxFDcfQ.d.mts} +2 -2
- package/dist/{index-BECNG13R.d.mts → index-COFPq-mg.d.mts} +13 -13
- package/dist/{index-YeXSxzhX.d.mts → index-CWwQ1Q7t.d.mts} +17 -17
- package/dist/{index-CbBQdS-u.d.mts → index-CWxKeTtE.d.mts} +6 -6
- package/dist/{index-cPt-00xm.d.mts → index-D0SgItjY.d.mts} +6 -6
- package/dist/{index-Bhc37y9F.d.mts → index-DCliG2n6.d.mts} +3 -3
- package/dist/{index-CM1rFdwV.d.mts → index-DPVukjga.d.mts} +9 -9
- package/dist/{index-WV7VzW2H.d.mts → index-DUu0ieh6.d.mts} +8 -8
- package/dist/{index-Bxive_YT.d.mts → index-De2a2OyC.d.mts} +9 -9
- package/dist/{index-CpH-1sse.d.mts → index-Dg6Ftb6f.d.mts} +9 -9
- package/dist/{index-Dt2I0G4X.d.mts → index-Dgyj1fTk.d.mts} +7 -7
- package/dist/{index-CTqynuxm.d.mts → index-DxIqV3rU.d.mts} +4 -4
- package/dist/{index-jEkJ2Zfk.d.mts → index-DxRAXHrZ.d.mts} +6 -6
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- 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/{jwt-DBN2mu0r.mjs → jwt-Czv-v097.mjs} +1 -1
- package/dist/{magic-link-D9Em1aOR.mjs → magic-link-CJgE0Ngn.mjs} +1 -1
- package/dist/{multi-session-Dno_8DxC.mjs → multi-session-bveCXtQW.mjs} +1 -1
- package/dist/{oauth-proxy-DW-zt8wv.mjs → oauth-proxy-69n1xA8L.mjs} +1 -1
- package/dist/oauth2/index.d.mts +1 -1
- package/dist/oauth2/index.mjs +1 -1
- package/dist/{one-tap-CodeyQb8.mjs → one-tap-Cm1eHuqi.mjs} +1 -1
- package/dist/{organization-D4QMpChF.mjs → organization-BK3ks97y.mjs} +1 -1
- package/dist/plugins/admin/index.d.mts +2 -2
- package/dist/plugins/admin/index.mjs +2 -2
- package/dist/plugins/anonymous/index.d.mts +2 -2
- package/dist/plugins/anonymous/index.mjs +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 +3 -3
- 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/custom-session/index.mjs +1 -1
- package/dist/plugins/device-authorization/index.d.mts +1 -1
- package/dist/plugins/email-otp/index.d.mts +1 -1
- package/dist/plugins/email-otp/index.mjs +2 -2
- package/dist/plugins/generic-oauth/index.d.mts +2 -2
- package/dist/plugins/generic-oauth/index.mjs +2 -2
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +2 -2
- package/dist/plugins/index.d.mts +22 -22
- package/dist/plugins/index.mjs +17 -17
- package/dist/plugins/jwt/index.d.mts +2 -2
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/magic-link/index.d.mts +1 -1
- package/dist/plugins/magic-link/index.mjs +2 -2
- package/dist/plugins/multi-session/index.d.mts +1 -1
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.d.mts +1 -1
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/one-tap/index.d.mts +1 -1
- package/dist/plugins/one-tap/index.mjs +2 -2
- package/dist/plugins/one-time-token/index.d.mts +2 -2
- package/dist/plugins/one-time-token/index.mjs +17 -17
- package/dist/plugins/organization/index.d.mts +2 -2
- package/dist/plugins/organization/index.mjs +2 -2
- package/dist/plugins/passkey/index.d.mts +2 -2
- package/dist/plugins/passkey/index.mjs +1 -1
- package/dist/plugins/phone-number/index.d.mts +2 -2
- package/dist/plugins/phone-number/index.mjs +1 -1
- package/dist/plugins/sentinel/client.d.mts +23 -23
- package/dist/plugins/sentinel/index.d.mts +22 -22
- package/dist/plugins/sentinel/index.mjs +4 -4
- package/dist/plugins/two-factor/index.d.mts +2 -2
- package/dist/plugins/two-factor/index.mjs +1 -1
- package/dist/plugins/username/index.d.mts +1 -1
- package/dist/plugins/username/index.mjs +2 -2
- package/dist/{plugins-yF8CFUDR.mjs → plugins-RSPSU3ro.mjs} +1 -1
- package/dist/{registry-CqG_3Qnx.mjs → registry-GztLf62p.mjs} +14 -2
- package/dist/{sentinel-B4Wo79HK.mjs → sentinel-CYtaI3eO.mjs} +264 -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/geo.d.ts +11 -0
- package/dist/src/plugins/sentinel/index.d.ts +248 -3
- 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 +1044 -1044
- package/dist/test-utils/index.mjs +18 -18
- package/dist/types/index.d.mts +1 -1
- package/dist/{username-BHNvcvsY.mjs → username-DCp6yugu.mjs} +1 -1
- package/package.json +3 -3
|
@@ -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 {
|
|
6
|
-
import { t as
|
|
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";
|
|
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) {
|
|
@@ -285,6 +289,28 @@ function looksGenerated(email) {
|
|
|
285
289
|
//#region src/plugins/sentinel/geo.ts
|
|
286
290
|
const EARTH_RADIUS_KM = 6371;
|
|
287
291
|
const toRadians = (degrees) => degrees * Math.PI / 180;
|
|
292
|
+
/** Stored coordinates are whole numbers of millionths of a degree */
|
|
293
|
+
const COORDINATE_SCALE = 1e6;
|
|
294
|
+
/** The largest whole-degree value, which tells an older, unscaled row apart */
|
|
295
|
+
const MAX_DEGREES = 360;
|
|
296
|
+
/**
|
|
297
|
+
* A coordinate as stored. The database column is an integer (the schema has
|
|
298
|
+
* no float type), so degrees are scaled to millionths of a degree, which is
|
|
299
|
+
* about 11 cm.
|
|
300
|
+
*/
|
|
301
|
+
function toStoredCoordinate(degrees) {
|
|
302
|
+
if (degrees == null) return null;
|
|
303
|
+
const scaled = Math.round(degrees * COORDINATE_SCALE);
|
|
304
|
+
return scaled !== 0 && Math.abs(scaled) <= MAX_DEGREES ? Math.sign(scaled) * (MAX_DEGREES + 1) : scaled;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A stored coordinate in degrees. Rows written before coordinates were scaled
|
|
308
|
+
* hold whole degrees, which are never more than 360 in size.
|
|
309
|
+
*/
|
|
310
|
+
function fromStoredCoordinate(stored) {
|
|
311
|
+
if (stored == null) return null;
|
|
312
|
+
return Math.abs(stored) <= MAX_DEGREES ? stored : stored / COORDINATE_SCALE;
|
|
313
|
+
}
|
|
288
314
|
/** Great-circle distance between two coordinates, in kilometres */
|
|
289
315
|
function haversineDistanceKm(from, to) {
|
|
290
316
|
const dLat = toRadians(to.latitude - from.latitude);
|
|
@@ -498,6 +524,37 @@ const schema = {
|
|
|
498
524
|
required: true
|
|
499
525
|
}
|
|
500
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
|
+
} },
|
|
501
558
|
sentinelSignIn: { fields: {
|
|
502
559
|
userId: {
|
|
503
560
|
type: "string",
|
|
@@ -610,6 +667,27 @@ async function deleteAllExpiredRows(auth, byPassLastCheckTime = false) {
|
|
|
610
667
|
auth.logger.error("[sentinel] Failed to delete expired rows:", error);
|
|
611
668
|
});
|
|
612
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
|
+
}
|
|
613
691
|
/**
|
|
614
692
|
* Counters and challenges, kept in secondary storage when it's configured
|
|
615
693
|
* and in the database otherwise
|
|
@@ -697,6 +775,92 @@ function createSentinelStore(auth) {
|
|
|
697
775
|
if (!row || new Date(row.expiresAt).getTime() <= Date.now()) return 0;
|
|
698
776
|
return row.count;
|
|
699
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
|
+
},
|
|
700
864
|
async reset(name) {
|
|
701
865
|
const key = PREFIX + name;
|
|
702
866
|
if (storage) {
|
|
@@ -1076,6 +1240,16 @@ const sentinel = (options) => {
|
|
|
1076
1240
|
}));
|
|
1077
1241
|
return Math.max(0, ...failures);
|
|
1078
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
|
+
}
|
|
1079
1253
|
function counterNames(keys) {
|
|
1080
1254
|
return keys.flatMap(({ scope, account }) => account ? [scope, account] : [scope]);
|
|
1081
1255
|
}
|
|
@@ -1143,10 +1317,12 @@ const sentinel = (options) => {
|
|
|
1143
1317
|
}
|
|
1144
1318
|
/** The travel speed from the previous sign-in, when both have coordinates */
|
|
1145
1319
|
function travel(previous, geo) {
|
|
1146
|
-
|
|
1320
|
+
const previousLatitude = fromStoredCoordinate(previous?.latitude);
|
|
1321
|
+
const previousLongitude = fromStoredCoordinate(previous?.longitude);
|
|
1322
|
+
if (!previous || previousLatitude == null || previousLongitude == null || geo?.latitude == null || geo.longitude == null) return null;
|
|
1147
1323
|
const speedKmh = travelSpeedKmh({
|
|
1148
|
-
latitude:
|
|
1149
|
-
longitude:
|
|
1324
|
+
latitude: previousLatitude,
|
|
1325
|
+
longitude: previousLongitude,
|
|
1150
1326
|
at: new Date(previous.createdAt)
|
|
1151
1327
|
}, {
|
|
1152
1328
|
latitude: geo.latitude,
|
|
@@ -1275,6 +1451,10 @@ const sentinel = (options) => {
|
|
|
1275
1451
|
}));
|
|
1276
1452
|
}
|
|
1277
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
|
+
}
|
|
1278
1458
|
return {
|
|
1279
1459
|
id: "sentinel",
|
|
1280
1460
|
init(ctx) {
|
|
@@ -1472,6 +1652,14 @@ const sentinel = (options) => {
|
|
|
1472
1652
|
const block = async (key, also = []) => {
|
|
1473
1653
|
await store.increment(`${key}:blocked`, cooldownSeconds);
|
|
1474
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
|
+
});
|
|
1475
1663
|
};
|
|
1476
1664
|
for (const { scope, account } of keys) {
|
|
1477
1665
|
const [total, succeeded] = await Promise.all([store.get(scope), store.get(`${scope}:succeeded`)]);
|
|
@@ -1599,8 +1787,8 @@ const sentinel = (options) => {
|
|
|
1599
1787
|
userAgent: details.userAgent,
|
|
1600
1788
|
country: geo?.country ?? null,
|
|
1601
1789
|
city: geo?.city ?? null,
|
|
1602
|
-
latitude: geo?.latitude
|
|
1603
|
-
longitude: geo?.longitude
|
|
1790
|
+
latitude: toStoredCoordinate(geo?.latitude),
|
|
1791
|
+
longitude: toStoredCoordinate(geo?.longitude),
|
|
1604
1792
|
createdAt: /* @__PURE__ */ new Date()
|
|
1605
1793
|
}
|
|
1606
1794
|
});
|
|
@@ -1616,6 +1804,73 @@ const sentinel = (options) => {
|
|
|
1616
1804
|
...schema,
|
|
1617
1805
|
...normalizedEmailSchema
|
|
1618
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
|
+
},
|
|
1619
1874
|
$ERROR_CODES: SENTINEL_ERROR_CODES,
|
|
1620
1875
|
options: opts
|
|
1621
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): ({
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A coordinate as stored. The database column is an integer (the schema has
|
|
3
|
+
* no float type), so degrees are scaled to millionths of a degree, which is
|
|
4
|
+
* about 11 cm.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toStoredCoordinate(degrees: number | null | undefined): number | null;
|
|
7
|
+
/**
|
|
8
|
+
* A stored coordinate in degrees. Rows written before coordinates were scaled
|
|
9
|
+
* hold whole degrees, which are never more than 360 in size.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fromStoredCoordinate(stored: number | null | undefined): number | null;
|
|
1
12
|
/** Great-circle distance between two coordinates, in kilometres */
|
|
2
13
|
export declare function haversineDistanceKm(from: {
|
|
3
14
|
latitude: number;
|