@prosopo/user-access-policy 3.5.19 → 3.5.28

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 (93) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/.export.js +21 -0
  3. package/dist/api/.export.js +11 -0
  4. package/dist/api/delete/.export.js +1 -0
  5. package/dist/api/{deleteAllRulesEndpoint.js → delete/deleteAllRules.js} +10 -9
  6. package/dist/api/delete/deleteRuleGroups.js +52 -0
  7. package/dist/api/delete/deleteRules.js +43 -0
  8. package/dist/api/read/.export.js +1 -0
  9. package/dist/api/read/fetchRules.js +43 -0
  10. package/dist/api/read/findRuleIds.js +50 -0
  11. package/dist/api/read/getMissingIds.js +41 -0
  12. package/dist/api/ruleApiRoutes.js +131 -0
  13. package/dist/api/rulesApiClient.js +93 -0
  14. package/dist/api/write/.export.js +1 -0
  15. package/dist/api/write/insertRules.js +102 -0
  16. package/dist/api/write/rehashRules.js +57 -0
  17. package/dist/cjs/.export.cjs +21 -0
  18. package/dist/cjs/api/.export.cjs +11 -0
  19. package/dist/cjs/api/delete/.export.cjs +1 -0
  20. package/dist/cjs/api/{deleteAllRulesEndpoint.cjs → delete/deleteAllRules.cjs} +9 -8
  21. package/dist/cjs/api/delete/deleteRuleGroups.cjs +52 -0
  22. package/dist/cjs/api/delete/deleteRules.cjs +43 -0
  23. package/dist/cjs/api/read/.export.cjs +1 -0
  24. package/dist/cjs/api/read/fetchRules.cjs +43 -0
  25. package/dist/cjs/api/read/findRuleIds.cjs +50 -0
  26. package/dist/cjs/api/read/getMissingIds.cjs +41 -0
  27. package/dist/cjs/api/ruleApiRoutes.cjs +131 -0
  28. package/dist/cjs/api/rulesApiClient.cjs +93 -0
  29. package/dist/cjs/api/write/.export.cjs +1 -0
  30. package/dist/cjs/api/write/insertRules.cjs +102 -0
  31. package/dist/cjs/api/write/rehashRules.cjs +57 -0
  32. package/dist/cjs/mongoose/.export.cjs +4 -0
  33. package/dist/cjs/mongoose/mongooseRuleSchema.cjs +36 -0
  34. package/dist/cjs/redis/.export.cjs +6 -0
  35. package/dist/cjs/redis/reader/redisAggregate.cjs +60 -0
  36. package/dist/cjs/redis/reader/redisRulesQuery.cjs +99 -0
  37. package/dist/cjs/redis/reader/redisRulesReader.cjs +230 -0
  38. package/dist/cjs/redis/redisClient.cjs +67 -0
  39. package/dist/cjs/redis/redisRuleIndex.cjs +50 -0
  40. package/dist/cjs/redis/redisRulesStorage.cjs +22 -9
  41. package/dist/cjs/redis/redisRulesWriter.cjs +91 -64
  42. package/dist/cjs/rule.cjs +8 -0
  43. package/dist/cjs/ruleInput/.export.cjs +9 -0
  44. package/dist/cjs/ruleInput/policyInput.cjs +25 -0
  45. package/dist/cjs/ruleInput/ruleInput.cjs +50 -0
  46. package/dist/cjs/ruleInput/userScopeInput.cjs +55 -0
  47. package/dist/cjs/ruleRecord.cjs +23 -0
  48. package/dist/cjs/rulesStorage.cjs +8 -0
  49. package/dist/cjs/transformRule.cjs +77 -0
  50. package/dist/mongoose/.export.js +4 -0
  51. package/dist/mongoose/mongooseRuleSchema.js +36 -0
  52. package/dist/redis/.export.js +6 -0
  53. package/dist/redis/reader/redisAggregate.js +60 -0
  54. package/dist/redis/reader/redisRulesQuery.js +99 -0
  55. package/dist/redis/reader/redisRulesReader.js +213 -0
  56. package/dist/redis/redisClient.js +67 -0
  57. package/dist/redis/redisRuleIndex.js +50 -0
  58. package/dist/redis/redisRulesStorage.js +23 -10
  59. package/dist/redis/redisRulesWriter.js +91 -64
  60. package/dist/rule.js +8 -0
  61. package/dist/ruleInput/.export.js +9 -0
  62. package/dist/ruleInput/policyInput.js +25 -0
  63. package/dist/ruleInput/ruleInput.js +50 -0
  64. package/dist/ruleInput/userScopeInput.js +55 -0
  65. package/dist/ruleRecord.js +23 -0
  66. package/dist/rulesStorage.js +8 -0
  67. package/dist/transformRule.js +77 -0
  68. package/entries.ts +20 -0
  69. package/package.json +34 -18
  70. package/vite.cjs.config.ts +4 -1
  71. package/vite.esm.config.ts +6 -1
  72. package/dist/accessPolicy.js +0 -80
  73. package/dist/accessPolicyResolver.js +0 -31
  74. package/dist/accessRules.js +0 -11
  75. package/dist/api/accessRuleApiRoutes.js +0 -79
  76. package/dist/api/accessRulesApiClient.js +0 -38
  77. package/dist/api/deleteRulesEndpoint.js +0 -34
  78. package/dist/api/insertRulesEndpoint.js +0 -62
  79. package/dist/cjs/accessPolicy.cjs +0 -80
  80. package/dist/cjs/accessPolicyResolver.cjs +0 -31
  81. package/dist/cjs/accessRules.cjs +0 -11
  82. package/dist/cjs/api/accessRuleApiRoutes.cjs +0 -79
  83. package/dist/cjs/api/accessRulesApiClient.cjs +0 -38
  84. package/dist/cjs/api/deleteRulesEndpoint.cjs +0 -34
  85. package/dist/cjs/api/insertRulesEndpoint.cjs +0 -62
  86. package/dist/cjs/index.cjs +0 -31
  87. package/dist/cjs/redis/redisRulesIndex.cjs +0 -138
  88. package/dist/cjs/redis/redisRulesReader.cjs +0 -142
  89. package/dist/cjs/util.cjs +0 -5
  90. package/dist/index.js +0 -32
  91. package/dist/redis/redisRulesIndex.js +0 -138
  92. package/dist/redis/redisRulesReader.js +0 -125
  93. package/dist/util.js +0 -5
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const search = require("@redis/search");
4
+ const transformRule = require("../transformRule.cjs");
5
+ const userIpRedisSchema = {
6
+ numericIpMaskMin: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
7
+ numericIpMaskMax: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
8
+ numericIp: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true }
9
+ };
10
+ const userAttributesRedisSchema = {
11
+ userId: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
12
+ ja4Hash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
13
+ headersHash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
14
+ userAgentHash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true }
15
+ };
16
+ const userScopeRedisSchema = {
17
+ ...userAttributesRedisSchema,
18
+ ...userIpRedisSchema
19
+ };
20
+ const policyScopeRedisSchema = {
21
+ clientId: {
22
+ type: search.SCHEMA_FIELD_TYPE.TAG,
23
+ INDEXMISSING: true
24
+ }
25
+ };
26
+ const accessRuleRedisSchema = {
27
+ ...policyScopeRedisSchema,
28
+ ...userScopeRedisSchema,
29
+ groupId: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true }
30
+ };
31
+ const ACCESS_RULES_REDIS_INDEX_NAME = "index:user-access-rules";
32
+ const ACCESS_RULE_REDIS_KEY_PREFIX = "uar:";
33
+ const accessRulesRedisIndex = {
34
+ name: ACCESS_RULES_REDIS_INDEX_NAME,
35
+ schema: accessRuleRedisSchema,
36
+ options: {
37
+ ON: "HASH",
38
+ PREFIX: [ACCESS_RULE_REDIS_KEY_PREFIX]
39
+ }
40
+ };
41
+ const getAccessRuleRedisKey = (rule) => ACCESS_RULE_REDIS_KEY_PREFIX + transformRule.makeAccessRuleHash(rule);
42
+ exports.ACCESS_RULES_REDIS_INDEX_NAME = ACCESS_RULES_REDIS_INDEX_NAME;
43
+ exports.ACCESS_RULE_REDIS_KEY_PREFIX = ACCESS_RULE_REDIS_KEY_PREFIX;
44
+ exports.accessRuleRedisSchema = accessRuleRedisSchema;
45
+ exports.accessRulesRedisIndex = accessRulesRedisIndex;
46
+ exports.getAccessRuleRedisKey = getAccessRuleRedisKey;
47
+ exports.policyScopeRedisSchema = policyScopeRedisSchema;
48
+ exports.userAttributesRedisSchema = userAttributesRedisSchema;
49
+ exports.userIpRedisSchema = userIpRedisSchema;
50
+ exports.userScopeRedisSchema = userScopeRedisSchema;
@@ -1,21 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const redisRulesReader = require("./redisRulesReader.cjs");
3
+ const redisRulesReader = require("./reader/redisRulesReader.cjs");
4
4
  const redisRulesWriter = require("./redisRulesWriter.cjs");
5
5
  const createRedisAccessRulesStorage = (connection, logger) => {
6
- const storage = {
7
- ...redisRulesReader.getDummyRedisRulesReader(logger),
8
- ...redisRulesWriter.getDummyRedisRulesWriter(logger)
9
- };
6
+ const storage = composeStorage(
7
+ new redisRulesReader.DummyRedisRulesReader(logger),
8
+ new redisRulesWriter.DummyRedisRulesWriter(logger)
9
+ );
10
10
  connection.getClient().then((client) => {
11
- Object.assign(storage, {
12
- ...redisRulesReader.createRedisRulesReader(client, logger),
13
- ...redisRulesWriter.createRedisRulesWriter(client)
14
- });
11
+ const realStorage = composeStorage(
12
+ new redisRulesReader.RedisRulesReader(client, logger),
13
+ new redisRulesWriter.RedisRulesWriter(client, logger)
14
+ );
15
+ Object.assign(storage, realStorage);
15
16
  logger.info(() => ({
16
17
  msg: "RedisAccessRules storage got a ready Redis client"
17
18
  }));
18
19
  });
19
20
  return storage;
20
21
  };
22
+ const composeStorage = (reader, writer) => ({
23
+ // reader
24
+ fetchRules: reader.fetchRules.bind(reader),
25
+ getMissingRuleIds: reader.getMissingRuleIds.bind(reader),
26
+ findRules: reader.findRules.bind(reader),
27
+ findRuleIds: reader.findRuleIds.bind(reader),
28
+ fetchAllRuleIds: reader.fetchAllRuleIds.bind(reader),
29
+ // writer
30
+ insertRules: writer.insertRules.bind(writer),
31
+ deleteRules: writer.deleteRules.bind(writer),
32
+ deleteAllRules: writer.deleteAllRules.bind(writer)
33
+ });
21
34
  exports.createRedisAccessRulesStorage = createRedisAccessRulesStorage;
@@ -1,73 +1,100 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const crypto = require("node:crypto");
4
- const redisRulesIndex = require("./redisRulesIndex.cjs");
5
- const redisRuleContentHashAlgorithm = "md5";
6
- const createRedisRulesWriter = (client) => {
7
- return {
8
- insertRule: async (rule, expirationTimestamp) => {
9
- const ruleKey = getRedisRuleKey(rule);
3
+ const common = require("@prosopo/common");
4
+ const redisClient = require("./redisClient.cjs");
5
+ const redisRuleIndex = require("./redisRuleIndex.cjs");
6
+ class RedisRulesWriter {
7
+ constructor(client, logger) {
8
+ this.client = client;
9
+ this.logger = logger;
10
+ }
11
+ async insertRules(ruleEntries) {
12
+ const entryBatches = common.chunkIntoBatches(ruleEntries, redisClient.REDIS_BATCH_SIZE);
13
+ const keyBatches = await common.executeBatchesSequentially(
14
+ entryBatches,
15
+ async (entriesBatch) => this.insertRuleEntries(entriesBatch)
16
+ );
17
+ return keyBatches.flatMap(
18
+ (ruleKey) => ruleKey.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)
19
+ );
20
+ }
21
+ async deleteRules(ruleIds) {
22
+ const ruleKeys = ruleIds.map(
23
+ (ruleId) => redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX + ruleId
24
+ );
25
+ const keyBatches = common.chunkIntoBatches(ruleKeys, redisClient.REDIS_BATCH_SIZE);
26
+ await common.executeBatchesSequentially(keyBatches, async (keysBatch) => {
27
+ const queries = this.client.multi();
28
+ for (const ruleKey of keysBatch) {
29
+ queries.del(ruleKey);
30
+ }
31
+ await queries.exec();
32
+ });
33
+ }
34
+ async deleteAllRules() {
35
+ let cursor = "0";
36
+ let total = 0;
37
+ do {
38
+ const reply = await this.client.scan(cursor, {
39
+ MATCH: `${redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX}*`,
40
+ COUNT: redisClient.REDIS_BATCH_SIZE
41
+ });
42
+ const ids = reply.keys.map(
43
+ (key) => key.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)
44
+ );
45
+ await this.deleteRules(ids);
46
+ total += ids.length;
47
+ cursor = reply.cursor;
48
+ } while ("0" !== cursor);
49
+ return total;
50
+ }
51
+ async insertRuleEntries(ruleEntries) {
52
+ const queries = this.client.multi();
53
+ const ruleKeys = ruleEntries.map((ruleEntry) => {
54
+ const { rule, expiresUnixTimestamp } = ruleEntry;
55
+ const ruleKey = redisRuleIndex.getAccessRuleRedisKey(rule);
10
56
  const ruleValue = getRedisRuleValue(rule);
11
- await client.hSet(ruleKey, ruleValue);
12
- if (expirationTimestamp) {
13
- const expiryDate = new Date(expirationTimestamp);
14
- if (expiryDate.getUTCFullYear() === 1970) {
15
- await client.expireAt(ruleKey, expirationTimestamp);
16
- } else {
17
- const timestampInSeconds = Math.floor(expirationTimestamp / 1e3);
18
- await client.expireAt(ruleKey, timestampInSeconds);
19
- }
57
+ queries.hSet(ruleKey, ruleValue);
58
+ if (expiresUnixTimestamp) {
59
+ queries.expireAt(ruleKey, expiresUnixTimestamp);
20
60
  }
21
61
  return ruleKey;
22
- },
23
- deleteRules: async (ruleIds) => void await client.del(ruleIds),
24
- deleteAllRules: async () => {
25
- const keys = await client.keys(`${redisRulesIndex.redisRuleKeyPrefix}*`);
26
- if (keys.length === 0) return 0;
27
- return await client.del(keys);
28
- }
29
- };
30
- };
31
- const getDummyRedisRulesWriter = (logger) => {
32
- return {
33
- insertRule: async (rule, expirationTimestamp) => {
34
- logger.info(() => ({
35
- msg: "Dummy insertRule() has no effect (redis is not ready)",
36
- data: {
37
- rule
38
- }
39
- }));
40
- return "";
41
- },
42
- deleteRules: async (ruleIds) => {
43
- logger.info(() => ({
44
- msg: "Dummy deleteRules() has no effect (redis is not ready)",
45
- data: {
46
- ruleIds
47
- }
48
- }));
49
- },
50
- deleteAllRules: async () => {
51
- logger.info(() => ({
52
- msg: "Dummy deleteAllRules() has no effect (redis is not ready)"
53
- }));
54
- return 0;
55
- }
56
- };
57
- };
58
- const getRedisRuleKey = (rule) => redisRulesIndex.redisRuleKeyPrefix + crypto.createHash(redisRuleContentHashAlgorithm).update(
59
- JSON.stringify(
60
- rule,
61
- (key, value) => (
62
- // JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
63
- "bigint" === typeof value ? value.toString() : value
64
- )
65
- )
66
- ).digest("hex");
62
+ });
63
+ await queries.exec();
64
+ return ruleKeys;
65
+ }
66
+ }
67
67
  const getRedisRuleValue = (rule) => Object.fromEntries(
68
68
  Object.entries(rule).map(([key, value]) => [key, String(value)])
69
69
  );
70
- exports.createRedisRulesWriter = createRedisRulesWriter;
71
- exports.getDummyRedisRulesWriter = getDummyRedisRulesWriter;
72
- exports.getRedisRuleKey = getRedisRuleKey;
70
+ class DummyRedisRulesWriter {
71
+ constructor(logger) {
72
+ this.logger = logger;
73
+ }
74
+ async insertRules(ruleEntries) {
75
+ this.logger.info(() => ({
76
+ msg: "Dummy insertRules() has no effect (redis is not ready)",
77
+ data: {
78
+ ruleEntries
79
+ }
80
+ }));
81
+ return [];
82
+ }
83
+ async deleteRules(ruleIds) {
84
+ this.logger.info(() => ({
85
+ msg: "Dummy deleteRules() has no effect (redis is not ready)",
86
+ data: {
87
+ ruleIds
88
+ }
89
+ }));
90
+ }
91
+ async deleteAllRules() {
92
+ this.logger.info(() => ({
93
+ msg: "Dummy deleteAllRules() has no effect (redis is not ready)"
94
+ }));
95
+ return 0;
96
+ }
97
+ }
98
+ exports.DummyRedisRulesWriter = DummyRedisRulesWriter;
99
+ exports.RedisRulesWriter = RedisRulesWriter;
73
100
  exports.getRedisRuleValue = getRedisRuleValue;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ var AccessPolicyType = /* @__PURE__ */ ((AccessPolicyType2) => {
4
+ AccessPolicyType2["Block"] = "block";
5
+ AccessPolicyType2["Restrict"] = "restrict";
6
+ return AccessPolicyType2;
7
+ })(AccessPolicyType || {});
8
+ exports.AccessPolicyType = AccessPolicyType;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const ruleInput = require("./ruleInput.cjs");
4
+ const policyInput = require("./policyInput.cjs");
5
+ const userScopeInput = require("./userScopeInput.cjs");
6
+ exports.accessRuleInput = ruleInput.accessRuleInput;
7
+ exports.accessPolicyInput = policyInput.accessPolicyInput;
8
+ exports.policyScopeInput = policyInput.policyScopeInput;
9
+ exports.userScopeInput = userScopeInput.userScopeInput;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const types = require("@prosopo/types");
4
+ const zod = require("zod");
5
+ const rule = require("../rule.cjs");
6
+ const accessPolicyInput = zod.z.object({
7
+ type: zod.z.nativeEnum(rule.AccessPolicyType),
8
+ captchaType: types.CaptchaTypeSchema.optional(),
9
+ description: zod.z.coerce.string().optional(),
10
+ // Redis stores values as strings, so coerce is needed to parse properly
11
+ solvedImagesCount: zod.z.coerce.number().optional(),
12
+ // the percentage of image panels that must be solved per image CAPTCHA
13
+ imageThreshold: zod.z.coerce.number().optional(),
14
+ // the Proof-of-Work difficulty level
15
+ powDifficulty: zod.z.coerce.number().optional(),
16
+ // the number of unsolved image CAPTCHA challenges to serve
17
+ unsolvedImagesCount: zod.z.coerce.number().optional(),
18
+ // used to increase the user's score
19
+ frictionlessScore: zod.z.coerce.number().optional()
20
+ });
21
+ const policyScopeInput = zod.z.object({
22
+ clientId: zod.z.coerce.string().optional()
23
+ });
24
+ exports.accessPolicyInput = accessPolicyInput;
25
+ exports.policyScopeInput = policyScopeInput;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const zod = require("zod");
4
+ const rulesStorage = require("../rulesStorage.cjs");
5
+ const policyInput = require("./policyInput.cjs");
6
+ const userScopeInput = require("./userScopeInput.cjs");
7
+ const ruleGroupInput = zod.z.object({
8
+ groupId: zod.z.coerce.string().optional(),
9
+ ruleGroupId: zod.z.coerce.string().optional()
10
+ }).transform((ruleGroupInput2) => {
11
+ const { ruleGroupId, ...ruleGroup } = ruleGroupInput2;
12
+ if ("string" === typeof ruleGroupId) {
13
+ ruleGroup.groupId = ruleGroupId;
14
+ }
15
+ return ruleGroup;
16
+ });
17
+ const accessRuleInput = zod.z.object({
18
+ ...policyInput.accessPolicyInput.shape,
19
+ ...policyInput.policyScopeInput.shape
20
+ }).and(userScopeInput.userScopeInput).and(ruleGroupInput).transform((ruleInput) => ruleInput);
21
+ const ruleEntryInput = zod.z.object({
22
+ rule: accessRuleInput,
23
+ expiresUnixTimestamp: zod.z.coerce.number().optional()
24
+ });
25
+ const accessRulesFilterInput = zod.z.object({
26
+ policyScope: policyInput.policyScopeInput.optional(),
27
+ policyScopes: zod.z.array(policyInput.policyScopeInput).optional(),
28
+ policyScopeMatch: zod.z.nativeEnum(rulesStorage.FilterScopeMatch).default(rulesStorage.FilterScopeMatch.Exact),
29
+ userScope: userScopeInput.userScopeInput.optional(),
30
+ userScopeMatch: zod.z.nativeEnum(rulesStorage.FilterScopeMatch).default(rulesStorage.FilterScopeMatch.Exact),
31
+ groupId: zod.z.string().optional()
32
+ });
33
+ const getAccessRuleFiltersFromInput = (filterInput) => {
34
+ const { policyScopes, policyScope, ...filterBase } = filterInput;
35
+ const allPolicyScopes = policyScopes || [];
36
+ if (policyScope) {
37
+ allPolicyScopes.push(policyScope);
38
+ }
39
+ if (allPolicyScopes.length > 0) {
40
+ return allPolicyScopes.map((policyScope2) => ({
41
+ ...filterBase,
42
+ policyScope: policyScope2
43
+ }));
44
+ }
45
+ return [filterBase];
46
+ };
47
+ exports.accessRuleInput = accessRuleInput;
48
+ exports.accessRulesFilterInput = accessRulesFilterInput;
49
+ exports.getAccessRuleFiltersFromInput = getAccessRuleFiltersFromInput;
50
+ exports.ruleEntryInput = ruleEntryInput;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const crypto = require("node:crypto");
4
+ const util = require("@prosopo/util");
5
+ const ipAddress = require("ip-address");
6
+ const zod = require("zod");
7
+ const userAttributesSchema = zod.z.object({
8
+ // coerce is used for safety, as e.g., incoming userId can be digital
9
+ userId: zod.z.coerce.string().optional(),
10
+ ja4Hash: zod.z.coerce.string().optional(),
11
+ headersHash: zod.z.coerce.string().optional(),
12
+ userAgentHash: zod.z.coerce.string().optional()
13
+ });
14
+ const userAttributesInput = zod.z.object({
15
+ ...userAttributesSchema.shape,
16
+ userAgent: zod.z.coerce.string().optional()
17
+ }).transform((userAttributesInput2) => {
18
+ const { userAgent, ...userScope } = userAttributesInput2;
19
+ if ("string" === typeof userAgent) {
20
+ userScope.userAgentHash = hashUserAgent(userAgent);
21
+ }
22
+ return userScope;
23
+ });
24
+ const hashUserAgent = (userAgent) => crypto.createHash("sha256").update(userAgent).digest("hex");
25
+ const userIpSchema = zod.z.object({
26
+ numericIp: zod.z.coerce.bigint().optional(),
27
+ numericIpMaskMin: zod.z.coerce.bigint().optional(),
28
+ numericIpMaskMax: zod.z.coerce.bigint().optional()
29
+ });
30
+ const userIpInput = zod.z.object({
31
+ ...userIpSchema.shape,
32
+ ip: zod.z.string().optional(),
33
+ ipMask: zod.z.string().optional()
34
+ }).transform((userIpInput2) => {
35
+ const { ip, ipMask, ...numericUserIp } = userIpInput2;
36
+ if ("string" === typeof ip) {
37
+ numericUserIp.numericIp = util.getIPAddress(ip).bigInt();
38
+ }
39
+ if ("string" === typeof ipMask) {
40
+ const ipObject = new ipAddress.Address4(ipMask);
41
+ numericUserIp.numericIpMaskMin = ipObject.startAddress().bigInt();
42
+ numericUserIp.numericIpMaskMax = ipObject.endAddress().bigInt();
43
+ }
44
+ return numericUserIp;
45
+ });
46
+ const userScopeSchema = zod.z.object({
47
+ ...userIpSchema.shape,
48
+ ...userAttributesSchema.shape
49
+ });
50
+ const userScopeInput = zod.z.object({}).and(userIpInput).and(userAttributesInput).transform(
51
+ // transform is used for type safety only - plain "satisfies ZodType<x>" doesn't work after ".and()"
52
+ (userScopeInput2) => userScopeInput2
53
+ );
54
+ exports.userScopeInput = userScopeInput;
55
+ exports.userScopeSchema = userScopeSchema;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const userAttributesRecordFields = [
4
+ "userId",
5
+ "ja4Hash",
6
+ "headersHash",
7
+ "userAgent"
8
+ ];
9
+ const userIpRecordFields = [
10
+ "ip",
11
+ "ipMask"
12
+ ];
13
+ const userScopeRecordFields = [
14
+ ...userAttributesRecordFields,
15
+ ...userIpRecordFields
16
+ ];
17
+ const getUserScopeRecordFromAccessRuleRecord = (ruleRecord) => Object.fromEntries(
18
+ userScopeRecordFields.map((field) => [field, ruleRecord[field]]).filter(([, value]) => value !== void 0)
19
+ );
20
+ exports.getUserScopeRecordFromAccessRuleRecord = getUserScopeRecordFromAccessRuleRecord;
21
+ exports.userAttributesRecordFields = userAttributesRecordFields;
22
+ exports.userIpRecordFields = userIpRecordFields;
23
+ exports.userScopeRecordFields = userScopeRecordFields;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ var FilterScopeMatch = /* @__PURE__ */ ((FilterScopeMatch2) => {
4
+ FilterScopeMatch2["Exact"] = "exact";
5
+ FilterScopeMatch2["Greedy"] = "greedy";
6
+ return FilterScopeMatch2;
7
+ })(FilterScopeMatch || {});
8
+ exports.FilterScopeMatch = FilterScopeMatch;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const crypto = require("node:crypto");
4
+ const cidrCalc = require("cidr-calc");
5
+ const ipAddress = require("ip-address");
6
+ const zod = require("zod");
7
+ const policyInput = require("./ruleInput/policyInput.cjs");
8
+ const ruleInput = require("./ruleInput/ruleInput.cjs");
9
+ const userScopeInput = require("./ruleInput/userScopeInput.cjs");
10
+ const RULE_HASH_ALGORITHM = "md5";
11
+ const makeAccessRuleHash = (rule) => {
12
+ const valueProperties = Object.entries(rule).filter(
13
+ ([key, value]) => "undefined" !== typeof value
14
+ );
15
+ const orderedProperties = valueProperties.sort();
16
+ const objectToHash = Object.fromEntries(orderedProperties);
17
+ return hashObject(objectToHash, RULE_HASH_ALGORITHM);
18
+ };
19
+ const transformAccessRuleRecordIntoRule = (ruleRecord) => (
20
+ // accessRuleInput does all the record field transformations
21
+ ruleInput.accessRuleInput.parse(ruleRecord)
22
+ );
23
+ const transformAccessRuleIntoRecord = (rule) => accessRuleToRecordScheme.parse(rule);
24
+ const accessRuleToRecordScheme = zod.z.object({
25
+ ...policyInput.accessPolicyInput.shape,
26
+ ...policyInput.policyScopeInput.shape,
27
+ ...userScopeInput.userScopeSchema.shape,
28
+ groupId: zod.z.coerce.string().optional()
29
+ }).transform((ruleInput2) => {
30
+ const {
31
+ groupId,
32
+ numericIp,
33
+ numericIpMaskMin,
34
+ numericIpMaskMax,
35
+ userAgentHash,
36
+ ...rule
37
+ } = ruleInput2;
38
+ const record = rule;
39
+ if ("string" === typeof groupId) {
40
+ record.ruleGroupId = groupId;
41
+ }
42
+ if ("string" === typeof userAgentHash) {
43
+ record.userAgent = userAgentHash;
44
+ }
45
+ if ("bigint" === typeof numericIp) {
46
+ record.ip = getStringIpFromNumeric(numericIp);
47
+ }
48
+ if ("bigint" === typeof numericIpMaskMin && "bigint" === typeof numericIpMaskMax) {
49
+ record.ipMask = getCidrFromNumericIpRange(
50
+ numericIpMaskMin,
51
+ numericIpMaskMax
52
+ );
53
+ }
54
+ return record;
55
+ });
56
+ const hashObject = (object, algorithm) => crypto.createHash(algorithm).update(
57
+ JSON.stringify(
58
+ object,
59
+ (key, value) => (
60
+ // JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
61
+ "bigint" === typeof value ? value.toString() : value
62
+ )
63
+ )
64
+ ).digest("hex");
65
+ const getStringIpFromNumeric = (numericIp) => ipAddress.Address4.fromInteger(Number(numericIp)).address;
66
+ const getCidrFromNumericIpRange = (startIp, endIp) => {
67
+ const ipRange = new cidrCalc.IpRange(
68
+ cidrCalc.IpAddress.of(getStringIpFromNumeric(startIp)),
69
+ cidrCalc.IpAddress.of(getStringIpFromNumeric(endIp))
70
+ );
71
+ const cidr = ipRange.toCidrs()[0];
72
+ return cidr ? `${cidr.prefix.toString()}/${cidr.prefixLen}` : void 0;
73
+ };
74
+ exports.getCidrFromNumericIpRange = getCidrFromNumericIpRange;
75
+ exports.makeAccessRuleHash = makeAccessRuleHash;
76
+ exports.transformAccessRuleIntoRecord = transformAccessRuleIntoRecord;
77
+ exports.transformAccessRuleRecordIntoRule = transformAccessRuleRecordIntoRule;
@@ -0,0 +1,4 @@
1
+ import { accessRuleMongooseSchema } from "./mongooseRuleSchema.js";
2
+ export {
3
+ accessRuleMongooseSchema
4
+ };
@@ -0,0 +1,36 @@
1
+ const userAttributesSchema = {
2
+ userId: { type: String, required: false },
3
+ ja4Hash: { type: String, required: false },
4
+ userAgent: { type: String, required: false },
5
+ headersHash: { type: String, required: false }
6
+ };
7
+ const userIpSchema = {
8
+ ip: { type: String, required: false },
9
+ ipMask: { type: String, required: false }
10
+ };
11
+ const userScopeSchema = {
12
+ ...userAttributesSchema,
13
+ ...userIpSchema
14
+ };
15
+ const policyScopeSchema = {
16
+ clientId: { type: String, required: false }
17
+ };
18
+ const accessPolicySchema = {
19
+ type: { type: String, required: true },
20
+ captchaType: { type: String, required: false },
21
+ description: { type: String, required: false },
22
+ solvedImagesCount: { type: Number, required: false },
23
+ imageThreshold: { type: Number, required: false },
24
+ powDifficulty: { type: Number, required: false },
25
+ unsolvedImagesCount: { type: Number, required: false },
26
+ frictionlessScore: { type: Number, required: false }
27
+ };
28
+ const accessRuleMongooseSchema = {
29
+ ...accessPolicySchema,
30
+ ...policyScopeSchema,
31
+ ...userScopeSchema,
32
+ ruleGroupId: { type: String, required: false }
33
+ };
34
+ export {
35
+ accessRuleMongooseSchema
36
+ };
@@ -0,0 +1,6 @@
1
+ import { createRedisAccessRulesStorage } from "./redisRulesStorage.js";
2
+ import { accessRulesRedisIndex } from "./redisRuleIndex.js";
3
+ export {
4
+ accessRulesRedisIndex,
5
+ createRedisAccessRulesStorage
6
+ };
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { REDIS_QUERY_DIALECT } from "./redisRulesQuery.js";
3
+ import { parseRedisRecords, REDIS_BATCH_SIZE } from "../redisClient.js";
4
+ import { ACCESS_RULES_REDIS_INDEX_NAME } from "../redisRuleIndex.js";
5
+ const aggregateRedisKeys = async (client, query, logger, batchHandler) => {
6
+ const keyField = "__key";
7
+ const recordSchema = z.object({
8
+ // it's a reserved name for the record key
9
+ [keyField]: z.string()
10
+ });
11
+ const foundKeys = [];
12
+ const addRecordKeys = async (records) => {
13
+ const parsedRecords = parseRedisRecords(records, recordSchema, logger);
14
+ const recordKeys = parsedRecords.map((record) => record[keyField]);
15
+ if (batchHandler) {
16
+ await batchHandler(recordKeys);
17
+ } else {
18
+ foundKeys.push(...recordKeys);
19
+ logger.debug(() => ({
20
+ msg: "Processed aggregation batch",
21
+ data: {
22
+ size: recordKeys.length
23
+ }
24
+ }));
25
+ }
26
+ };
27
+ await executeAggregation(
28
+ client,
29
+ query,
30
+ {
31
+ // #2 is a required option when the 'ismissing()' function is in the query body
32
+ DIALECT: REDIS_QUERY_DIALECT,
33
+ COUNT: REDIS_BATCH_SIZE,
34
+ LOAD: `@${keyField}`
35
+ },
36
+ addRecordKeys
37
+ );
38
+ return foundKeys;
39
+ };
40
+ const executeAggregation = async (client, query, aggregateOptions, handleBatch) => {
41
+ const initialReply = await client.ft.aggregateWithCursor(
42
+ ACCESS_RULES_REDIS_INDEX_NAME,
43
+ query,
44
+ aggregateOptions
45
+ );
46
+ await handleBatch(initialReply.results);
47
+ let cursor = initialReply.cursor;
48
+ while (0 !== cursor) {
49
+ const batchReply = await client.ft.cursorRead(
50
+ ACCESS_RULES_REDIS_INDEX_NAME,
51
+ cursor,
52
+ { COUNT: aggregateOptions.COUNT }
53
+ );
54
+ await handleBatch(batchReply.results);
55
+ cursor = batchReply.cursor;
56
+ }
57
+ };
58
+ export {
59
+ aggregateRedisKeys
60
+ };