@prosopo/user-access-policy 3.12.8 → 3.12.9

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 (84) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +45 -41
  2. package/.turbo/turbo-build$colon$tsc.log +14 -14
  3. package/.turbo/turbo-build.log +46 -42
  4. package/CHANGELOG.md +18 -0
  5. package/dist/.export.js +7 -21
  6. package/dist/_virtual/_rolldown/runtime.js +3 -0
  7. package/dist/api/.export.js +2 -9
  8. package/dist/api/accessRulesApiClient.js +1 -1
  9. package/dist/api/delete/.export.js +2 -1
  10. package/dist/api/delete/deleteAllRules.js +22 -24
  11. package/dist/api/delete/deleteRuleGroups.js +39 -51
  12. package/dist/api/delete/deleteRules.js +37 -41
  13. package/dist/api/read/.export.js +2 -1
  14. package/dist/api/read/fetchRules.js +34 -42
  15. package/dist/api/read/findRuleIds.js +35 -48
  16. package/dist/api/read/getMissingIds.js +36 -40
  17. package/dist/api/ruleApiRoutes.js +62 -120
  18. package/dist/api/rulesApiClient.js +51 -87
  19. package/dist/api/write/.export.js +2 -1
  20. package/dist/api/write/insertRules.js +84 -102
  21. package/dist/api/write/rehashRules.js +48 -56
  22. package/dist/cjs/.export.cjs +21 -23
  23. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  24. package/dist/cjs/api/.export.cjs +6 -10
  25. package/dist/cjs/api/accessRulesApiClient.cjs +0 -0
  26. package/dist/cjs/api/delete/.export.cjs +0 -1
  27. package/dist/cjs/api/delete/deleteAllRules.cjs +22 -24
  28. package/dist/cjs/api/delete/deleteRuleGroups.cjs +39 -51
  29. package/dist/cjs/api/delete/deleteRules.cjs +41 -42
  30. package/dist/cjs/api/read/.export.cjs +0 -1
  31. package/dist/cjs/api/read/fetchRules.cjs +34 -41
  32. package/dist/cjs/api/read/findRuleIds.cjs +37 -48
  33. package/dist/cjs/api/read/getMissingIds.cjs +36 -39
  34. package/dist/cjs/api/ruleApiRoutes.cjs +69 -125
  35. package/dist/cjs/api/rulesApiClient.cjs +54 -90
  36. package/dist/cjs/api/write/.export.cjs +0 -1
  37. package/dist/cjs/api/write/insertRules.cjs +85 -103
  38. package/dist/cjs/api/write/rehashRules.cjs +48 -56
  39. package/dist/cjs/classifyOs.cjs +46 -26
  40. package/dist/cjs/mongoose/.export.cjs +2 -3
  41. package/dist/cjs/mongoose/mongooseRuleSchema.cjs +97 -36
  42. package/dist/cjs/redis/.export.cjs +4 -5
  43. package/dist/cjs/redis/reader/redisAggregate.cjs +59 -84
  44. package/dist/cjs/redis/reader/redisRulesQuery.cjs +74 -116
  45. package/dist/cjs/redis/reader/redisRulesReader.cjs +322 -503
  46. package/dist/cjs/redis/reader/redisRulesSplitQuery.cjs +94 -72
  47. package/dist/cjs/redis/redisClient.cjs +53 -58
  48. package/dist/cjs/redis/redisRuleIndex.cjs +93 -49
  49. package/dist/cjs/redis/redisRulesStorage.cjs +21 -30
  50. package/dist/cjs/redis/redisRulesWriter.cjs +86 -112
  51. package/dist/cjs/rule.cjs +8 -7
  52. package/dist/cjs/ruleInput/.export.cjs +7 -8
  53. package/dist/cjs/ruleInput/policyInput.cjs +35 -58
  54. package/dist/cjs/ruleInput/ruleInput.cjs +37 -42
  55. package/dist/cjs/ruleInput/userScopeInput.cjs +47 -55
  56. package/dist/cjs/ruleRecord.cjs +15 -22
  57. package/dist/cjs/rulesStorage.cjs +7 -6
  58. package/dist/cjs/transformRule.cjs +36 -70
  59. package/dist/classifyOs.js +48 -29
  60. package/dist/mongoose/.export.js +2 -3
  61. package/dist/mongoose/mongooseRuleSchema.js +99 -38
  62. package/dist/redis/.export.js +3 -5
  63. package/dist/redis/reader/redisAggregate.js +60 -86
  64. package/dist/redis/reader/redisRulesQuery.js +75 -118
  65. package/dist/redis/reader/redisRulesReader.js +312 -487
  66. package/dist/redis/reader/redisRulesSplitQuery.js +96 -75
  67. package/dist/redis/redisClient.js +54 -62
  68. package/dist/redis/redisRuleIndex.js +93 -58
  69. package/dist/redis/redisRulesStorage.js +21 -30
  70. package/dist/redis/redisRulesWriter.js +82 -113
  71. package/dist/rule.js +10 -10
  72. package/dist/ruleInput/.export.js +3 -7
  73. package/dist/ruleInput/policyInput.js +35 -61
  74. package/dist/ruleInput/ruleInput.js +36 -44
  75. package/dist/ruleInput/userScopeInput.js +44 -55
  76. package/dist/ruleRecord.js +17 -27
  77. package/dist/rulesStorage.js +9 -8
  78. package/dist/tests/redis/redisRulesStorage.integration.test.js +1 -3
  79. package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
  80. package/dist/transformRule.js +34 -73
  81. package/entries.ts +25 -6
  82. package/package.json +13 -12
  83. package/src/tests/redis/redisRulesStorage.integration.test.ts +191 -195
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -1,511 +1,330 @@
1
- "use strict";
2
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const util = require("node:util");
4
- const common = require("@prosopo/common");
5
- const redisRulesQuery = require("./redisRulesQuery.cjs");
6
- const redisRulesSplitQuery = require("./redisRulesSplitQuery.cjs");
7
- const redisClient = require("../redisClient.cjs");
8
- const redisRuleIndex = require("../redisRuleIndex.cjs");
9
- const rule = require("../../rule.cjs");
10
- const ruleInput = require("../../ruleInput/ruleInput.cjs");
11
- const redisAggregate = require("./redisAggregate.cjs");
12
- function _interopNamespaceDefault(e) {
13
- const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
14
- if (e) {
15
- for (const k in e) {
16
- if (k !== "default") {
17
- const d = Object.getOwnPropertyDescriptor(e, k);
18
- Object.defineProperty(n, k, d.get ? d : {
19
- enumerable: true,
20
- get: () => e[k]
21
- });
22
- }
23
- }
24
- }
25
- n.default = e;
26
- return Object.freeze(n);
27
- }
28
- const util__namespace = /* @__PURE__ */ _interopNamespaceDefault(util);
29
- const SERVER_SIDE_RANK_TOP_N = 20;
30
- const GREEDY_MAX_CANDIDATES = redisClient.REDIS_BATCH_SIZE * 10;
31
- const SPLIT_MAX_CANDIDATES_PER_SUB = 500;
32
- const SPECIFICITY_EXPR = [
33
- "exists(@clientId)",
34
- "exists(@userId)",
35
- "exists(@ja4Hash)",
36
- "exists(@headersHash)",
37
- "exists(@userAgentHash)",
38
- "exists(@headHash)",
39
- "exists(@coords)",
40
- "exists(@countryCode)",
41
- "exists(@asn)",
42
- "exists(@os)",
43
- "exists(@numericIp)",
44
- "exists(@numericIpMaskMin)"
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_rule = require("../../rule.cjs");
4
+ const require_ruleInput_ruleInput = require("../../ruleInput/ruleInput.cjs");
5
+ const require_redis_reader_redisRulesQuery = require("./redisRulesQuery.cjs");
6
+ const require_redis_reader_redisRulesSplitQuery = require("./redisRulesSplitQuery.cjs");
7
+ const require_redis_redisClient = require("../redisClient.cjs");
8
+ const require_redis_redisRuleIndex = require("../redisRuleIndex.cjs");
9
+ const require_redis_reader_redisAggregate = require("./redisAggregate.cjs");
10
+ let _prosopo_common = require("@prosopo/common");
11
+ let node_util = require("node:util");
12
+ node_util = require_runtime.__toESM(node_util, 1);
13
+ //#region src/redis/reader/redisRulesReader.ts
14
+ var SERVER_SIDE_RANK_TOP_N = 20;
15
+ var GREEDY_MAX_CANDIDATES = require_redis_redisClient.REDIS_BATCH_SIZE * 10;
16
+ var SPLIT_MAX_CANDIDATES_PER_SUB = 500;
17
+ var SPECIFICITY_EXPR = [
18
+ "exists(@clientId)",
19
+ "exists(@userId)",
20
+ "exists(@ja4Hash)",
21
+ "exists(@headersHash)",
22
+ "exists(@userAgentHash)",
23
+ "exists(@headHash)",
24
+ "exists(@coords)",
25
+ "exists(@countryCode)",
26
+ "exists(@asn)",
27
+ "exists(@os)",
28
+ "exists(@numericIp)",
29
+ "exists(@numericIpMaskMin)"
45
30
  ].join(" + ");
46
- const SEVERITY_EXPR = `(@type == "${rule.AccessPolicyType.Block}")`;
47
- const RANK_EXPR = "(@_spec * 2) + @_sev";
48
- const RULE_LOAD_FIELDS = [
49
- "@__key",
50
- "@type",
51
- "@captchaType",
52
- "@description",
53
- "@solvedImagesCount",
54
- "@imageThreshold",
55
- "@powDifficulty",
56
- "@unsolvedImagesCount",
57
- "@frictionlessScore",
58
- "@deferToVerify",
59
- "@clientId",
60
- "@groupId",
61
- "@userId",
62
- "@ja4Hash",
63
- "@headersHash",
64
- "@userAgentHash",
65
- "@headHash",
66
- "@coords",
67
- "@countryCode",
68
- "@asn",
69
- "@os",
70
- "@numericIp",
71
- "@numericIpMaskMin",
72
- "@numericIpMaskMax"
31
+ var SEVERITY_EXPR = `(@type == "${require_rule.AccessPolicyType.Block}")`;
32
+ var RANK_EXPR = "(@_spec * 2) + @_sev";
33
+ var RULE_LOAD_FIELDS = [
34
+ "@__key",
35
+ "@type",
36
+ "@captchaType",
37
+ "@description",
38
+ "@solvedImagesCount",
39
+ "@imageThreshold",
40
+ "@powDifficulty",
41
+ "@unsolvedImagesCount",
42
+ "@frictionlessScore",
43
+ "@deferToVerify",
44
+ "@clientId",
45
+ "@groupId",
46
+ "@userId",
47
+ "@ja4Hash",
48
+ "@headersHash",
49
+ "@userAgentHash",
50
+ "@headHash",
51
+ "@coords",
52
+ "@countryCode",
53
+ "@asn",
54
+ "@os",
55
+ "@numericIp",
56
+ "@numericIpMaskMin",
57
+ "@numericIpMaskMax"
73
58
  ];
74
- const readerSpecificity = (rule2) => {
75
- let score = 0;
76
- if (rule2.clientId !== void 0) score++;
77
- if (rule2.userId !== void 0) score++;
78
- if (rule2.ja4Hash !== void 0) score++;
79
- if (rule2.headersHash !== void 0) score++;
80
- if (rule2.userAgentHash !== void 0) score++;
81
- if (rule2.headHash !== void 0) score++;
82
- if (rule2.coords !== void 0) score++;
83
- if (rule2.countryCode !== void 0) score++;
84
- if (rule2.asn !== void 0) score++;
85
- if (rule2.os !== void 0) score++;
86
- if (rule2.numericIp !== void 0) score++;
87
- if (rule2.numericIpMaskMin !== void 0) score++;
88
- return score;
59
+ var readerSpecificity = (rule) => {
60
+ let score = 0;
61
+ if (rule.clientId !== void 0) score++;
62
+ if (rule.userId !== void 0) score++;
63
+ if (rule.ja4Hash !== void 0) score++;
64
+ if (rule.headersHash !== void 0) score++;
65
+ if (rule.userAgentHash !== void 0) score++;
66
+ if (rule.headHash !== void 0) score++;
67
+ if (rule.coords !== void 0) score++;
68
+ if (rule.countryCode !== void 0) score++;
69
+ if (rule.asn !== void 0) score++;
70
+ if (rule.os !== void 0) score++;
71
+ if (rule.numericIp !== void 0) score++;
72
+ if (rule.numericIpMaskMin !== void 0) score++;
73
+ return score;
89
74
  };
90
- const sortByReaderRank = (rules) => [...rules].sort((a, b) => {
91
- const specDelta = readerSpecificity(b) - readerSpecificity(a);
92
- if (specDelta !== 0) return specDelta;
93
- const aBlock = a.type === rule.AccessPolicyType.Block ? 1 : 0;
94
- const bBlock = b.type === rule.AccessPolicyType.Block ? 1 : 0;
95
- return bBlock - aBlock;
75
+ var sortByReaderRank = (rules) => [...rules].sort((a, b) => {
76
+ const specDelta = readerSpecificity(b) - readerSpecificity(a);
77
+ if (specDelta !== 0) return specDelta;
78
+ const aBlock = a.type === require_rule.AccessPolicyType.Block ? 1 : 0;
79
+ return (b.type === require_rule.AccessPolicyType.Block ? 1 : 0) - aBlock;
96
80
  });
97
- class RedisRulesReader {
98
- constructor(client, logger) {
99
- this.client = client;
100
- this.logger = logger;
101
- }
102
- async getMissingRuleIds(ruleIds) {
103
- const ruleKeys = this.getRuleKeys(ruleIds);
104
- const keyBatches = common.chunkIntoBatches(ruleKeys, redisClient.REDIS_BATCH_SIZE);
105
- const missingKeyBatches = await common.executeBatchesSequentially(
106
- keyBatches,
107
- async (keysBatch) => redisClient.getMissingRedisKeys(this.client, keysBatch)
108
- );
109
- return missingKeyBatches.flat().map((ruleKey) => ruleKey.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length));
110
- }
111
- async fetchRules(ruleIds) {
112
- const ruleKeys = this.getRuleKeys(ruleIds);
113
- const keyBatches = common.chunkIntoBatches(ruleKeys, redisClient.REDIS_BATCH_SIZE);
114
- const entryBatches = await common.executeBatchesSequentially(
115
- keyBatches,
116
- (keysBatch) => this.fetchRuleEntries(keysBatch)
117
- );
118
- return entryBatches.flat();
119
- }
120
- async findRules(filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) {
121
- const query = redisRulesQuery.getRulesRedisQuery(filter, matchingFieldsOnly);
122
- if (skipEmptyUserScopes && // Sentinel query shape emitted by getPolicyScopeQuery when the
123
- // caller passed neither a userScope nor a clientId — i.e. an
124
- // empty filter that would otherwise resolve to "all global
125
- // rules". Guard so an accidentally-empty filter can't return
126
- // the full global rule set. The exact literal must stay in
127
- // sync with GLOBAL_MATCH_CLAUSE_INNER over there.
128
- query === "( @clientId:{global} | ismissing(@clientId) )") {
129
- return [];
130
- }
131
- if (matchingFieldsOnly && filter.userScope !== void 0) {
132
- return this.findRulesSplit(filter);
133
- }
134
- return this.findRulesGreedy(filter, query);
135
- }
136
- // Union candidate keys from each split sub-query, de-dupe, HGETALL,
137
- // parse, hand back to `getPrioritisedAccessRule` for JS-side ranking.
138
- // Ranking stays in JS because rankCandidateRules already encodes the
139
- // strict "every populated rule field matches request" semantics —
140
- // FT can't express that shape cheaply and the previous server-side
141
- // APPLY / SORTBY imposed the linear-per-rule cost we're eliminating.
142
- //
143
- // Handles both blockOnly (hard-block middleware) and non-blockOnly
144
- // (frictionless access-policy lookup) callers via the `blockOnly`
145
- // flag on the sub-query builder.
146
- async findRulesSplit(filter) {
147
- const userScope = filter.userScope;
148
- if (userScope === void 0) {
149
- return [];
150
- }
151
- const clientId = filter.policyScope?.clientId;
152
- const subQueries = redisRulesSplitQuery.buildScopedRulesSubQueries(userScope, clientId, {
153
- ...filter.blockOnly === true && { blockOnly: true }
154
- });
155
- try {
156
- const keyLists = await Promise.all(
157
- subQueries.map(
158
- (sub) => (
159
- // FT.SEARCH NOCONTENT (via searchRedisKeysBounded) rather
160
- // than FT.AGGREGATE with LOAD @__key. The aggregate reply
161
- // path in `@redis/client` 5.x calls transformTuplesReply
162
- // on every result row; when the RediSearch coordinator
163
- // returns a null tuple (observed continuously on
164
- // field:ja4Hash probes in prod), that throws
165
- // `Cannot read properties of null (reading 'length')` and
166
- // the whole sub-query resolves empty via the .catch below.
167
- // NOCONTENT returns a flat `[total, key1, key2, ...]`
168
- // array so there is no nested tuple that can be null.
169
- redisAggregate.searchRedisKeysBounded(
170
- this.client,
171
- sub.query,
172
- // Per-sub-query cap. Each probe hits a discriminating
173
- // index (e.g. numericIp posting list), so the natural
174
- // cardinality is tiny — this cap only fires on
175
- // pathological rule-set shapes and mirrors
176
- // SPLIT_MAX_CANDIDATES_PER_SUB below.
177
- SPLIT_MAX_CANDIDATES_PER_SUB
178
- ).catch((err) => {
179
- this.logger.error(() => ({
180
- err,
181
- data: {
182
- inspect: util__namespace.inspect(
183
- { subKind: sub.kind, query: sub.query },
184
- { depth: null }
185
- )
186
- },
187
- msg: "failed to execute split rule sub-query"
188
- }));
189
- return [];
190
- })
191
- )
192
- )
193
- );
194
- const uniqueKeys = /* @__PURE__ */ new Set();
195
- for (const list of keyLists) {
196
- for (const key of list) {
197
- uniqueKeys.add(key);
198
- }
199
- }
200
- if (uniqueKeys.size === 0) {
201
- return [];
202
- }
203
- const ruleKeys = [...uniqueKeys];
204
- this.logger.debug(() => ({
205
- msg: "Executed split rule sub-queries",
206
- data: {
207
- inspect: util__namespace.inspect(
208
- {
209
- subQueryCount: subQueries.length,
210
- uniqueCandidates: ruleKeys.length,
211
- perSubQueryCounts: keyLists.map((l, i) => ({
212
- kind: subQueries[i]?.kind,
213
- found: l.length
214
- })),
215
- filter
216
- },
217
- { depth: null }
218
- )
219
- }
220
- }));
221
- const { records } = await redisClient.fetchRedisHashRecords(
222
- this.client,
223
- ruleKeys,
224
- this.logger
225
- );
226
- const nonEmptyRecords = records.filter(
227
- (record) => Object.keys(record).length > 0
228
- );
229
- const parsedRules = redisClient.parseRedisRecords(
230
- nonEmptyRecords,
231
- ruleInput.accessRuleInput,
232
- this.logger
233
- );
234
- return sortByReaderRank(parsedRules);
235
- } catch (e) {
236
- this.logger.error(() => ({
237
- err: e,
238
- data: {
239
- inspect: util__namespace.inspect({ filter }, { depth: null })
240
- },
241
- msg: "failed to execute split rule query set"
242
- }));
243
- return [];
244
- }
245
- }
246
- async findRulesRanked(filter, query) {
247
- try {
248
- const reply = await this.client.ft.aggregate(
249
- redisRuleIndex.ACCESS_RULES_REDIS_INDEX_NAME,
250
- query,
251
- {
252
- DIALECT: redisRulesQuery.REDIS_QUERY_DIALECT,
253
- LOAD: [...RULE_LOAD_FIELDS],
254
- STEPS: [
255
- // Drop candidates whose hash is missing `type` before
256
- // any APPLY runs. SEVERITY_EXPR dereferences `@type`
257
- // directly, and RediSearch throws
258
- // "Could not find the value for a parameter name,
259
- // consider using EXISTS if applicable for type"
260
- // when the LOAD pulls an undefined @type, aborting
261
- // the whole aggregate. The undefined @type can come
262
- // from a partial-write race in the writer or a
263
- // stale RediSearch index entry pointing at a hash
264
- // whose `type` field has been removed; in both
265
- // cases the doc is malformed and not a valid
266
- // AccessRule, so dropping it is correct.
267
- { type: "FILTER", expression: "exists(@type)" },
268
- { type: "APPLY", expression: SPECIFICITY_EXPR, AS: "_spec" },
269
- { type: "APPLY", expression: SEVERITY_EXPR, AS: "_sev" },
270
- { type: "APPLY", expression: RANK_EXPR, AS: "_rank" },
271
- {
272
- type: "SORTBY",
273
- BY: [{ BY: "@_rank", DIRECTION: "DESC" }]
274
- // SORTBY MAX would let RediSearch keep only the
275
- // top-N in a bounded heap rather than fully
276
- // sorting, but the @redis/search client
277
- // serialises MAX *before* DESC and RediSearch
278
- // rejects that order ("MISSING ASC or DESC
279
- // after sort field (MAX)"). LIMIT below still
280
- // trims; SORTBY does a full sort. Acceptable
281
- // because the strict-match filter already
282
- // keeps the candidate set small in practice.
283
- },
284
- { type: "LIMIT", from: 0, size: SERVER_SIDE_RANK_TOP_N }
285
- ]
286
- }
287
- );
288
- if (reply.results.length === 0) {
289
- return [];
290
- }
291
- this.logger.debug(() => ({
292
- msg: "Executed ranked search query",
293
- data: {
294
- inspect: util__namespace.inspect(
295
- {
296
- filter,
297
- foundCount: reply.results.length,
298
- query
299
- },
300
- { depth: null }
301
- )
302
- }
303
- }));
304
- const ruleKeys = [];
305
- for (const result of reply.results) {
306
- const key = result.__key;
307
- if (typeof key === "string") {
308
- ruleKeys.push(key);
309
- }
310
- }
311
- if (ruleKeys.length === 0) {
312
- return [];
313
- }
314
- const { records } = await redisClient.fetchRedisHashRecords(
315
- this.client,
316
- ruleKeys,
317
- this.logger
318
- );
319
- const nonEmptyRecords = records.filter(
320
- (record) => Object.keys(record).length > 0
321
- );
322
- return redisClient.parseRedisRecords(nonEmptyRecords, ruleInput.accessRuleInput, this.logger);
323
- } catch (e) {
324
- this.logger.error(() => ({
325
- err: e,
326
- data: {
327
- inspect: util__namespace.inspect({ query, filter }, { depth: null })
328
- },
329
- msg: "failed to execute ranked search query"
330
- }));
331
- return [];
332
- }
333
- }
334
- // Used only by admin / internal callers (matchingFieldsOnly=false).
335
- // Not on the per-request blockMiddleware path any more, so the CPU
336
- // cost of the HGETALL fanout is acceptable. Same shape as #2689's
337
- // aggregate-with-cursor: returns every candidate up to
338
- // GREEDY_MAX_CANDIDATES so we don't silently drop matches that
339
- // admin tools depend on (e.g. ASN-wide rule listings).
340
- async findRulesGreedy(filter, query) {
341
- try {
342
- const ruleKeys = await redisAggregate.aggregateRedisKeys(
343
- this.client,
344
- query,
345
- this.logger,
346
- void 0,
347
- GREEDY_MAX_CANDIDATES
348
- );
349
- if (ruleKeys.length === 0) {
350
- return [];
351
- }
352
- this.logger.debug(() => ({
353
- msg: "Executed greedy search query",
354
- data: {
355
- inspect: util__namespace.inspect(
356
- {
357
- filter,
358
- foundCount: ruleKeys.length,
359
- query
360
- },
361
- { depth: null }
362
- )
363
- }
364
- }));
365
- const { records } = await redisClient.fetchRedisHashRecords(
366
- this.client,
367
- ruleKeys,
368
- this.logger
369
- );
370
- const nonEmptyRecords = records.filter(
371
- (record) => Object.keys(record).length > 0
372
- );
373
- return redisClient.parseRedisRecords(nonEmptyRecords, ruleInput.accessRuleInput, this.logger);
374
- } catch (e) {
375
- this.logger.error(() => ({
376
- err: e,
377
- data: {
378
- inspect: util__namespace.inspect({ query, filter }, { depth: null })
379
- },
380
- msg: "failed to execute greedy search query"
381
- }));
382
- return [];
383
- }
384
- }
385
- async findRuleIds(filter, matchingFieldsOnly = false) {
386
- const query = redisRulesQuery.getRulesRedisQuery(filter, matchingFieldsOnly);
387
- let ruleIds = [];
388
- try {
389
- const ruleKeys = await redisAggregate.aggregateRedisKeys(
390
- this.client,
391
- query,
392
- this.logger
393
- );
394
- ruleIds = ruleKeys.map(
395
- (ruleKey) => ruleKey.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)
396
- );
397
- } catch (e) {
398
- this.logger.error(() => ({
399
- err: e,
400
- data: {
401
- inspect: util__namespace.inspect(
402
- {
403
- query,
404
- filter
405
- },
406
- {
407
- depth: null
408
- }
409
- )
410
- },
411
- msg: "Failed to execute search query for rule IDs"
412
- }));
413
- return [];
414
- }
415
- this.logger.debug(() => ({
416
- msg: "Executed search query for rule IDs",
417
- data: {
418
- query,
419
- foundCount: ruleIds.length,
420
- foundIds: ruleIds
421
- }
422
- }));
423
- return ruleIds;
424
- }
425
- async fetchAllRuleIds(batchHandler) {
426
- const keysBatchHandler = async (keys) => {
427
- const ids = keys.map(
428
- (ruleKey) => ruleKey.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)
429
- );
430
- await batchHandler(ids);
431
- };
432
- await redisAggregate.aggregateRedisKeys(this.client, "*", this.logger, keysBatchHandler);
433
- }
434
- async fetchRuleEntries(keys) {
435
- const { records, expirations } = await redisClient.fetchRedisHashRecords(
436
- this.client,
437
- keys,
438
- this.logger
439
- );
440
- const entries = [];
441
- for (const [index, ruleData] of records.entries()) {
442
- const isRulePresent = Object.keys(ruleData).length > 0;
443
- if (isRulePresent) {
444
- const rule2 = redisClient.parseRedisRecords(
445
- [ruleData],
446
- ruleInput.accessRuleInput,
447
- this.logger
448
- )[0];
449
- if (rule2) {
450
- entries.push({
451
- rule: rule2,
452
- expiresUnixTimestamp: expirations[index]
453
- });
454
- }
455
- }
456
- }
457
- return entries;
458
- }
459
- getRuleKeys(ruleIds) {
460
- return ruleIds.map((id) => `${redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX}${id}`);
461
- }
462
- }
463
- class DummyRedisRulesReader {
464
- constructor(logger) {
465
- this.logger = logger;
466
- }
467
- async getMissingRuleIds(ruleIds) {
468
- this.logger.info(() => ({
469
- msg: "Dummy getMissingRuleIds() has no effect (redis is not ready)",
470
- data: {
471
- ruleIds
472
- }
473
- }));
474
- return [];
475
- }
476
- async fetchRules(ruleIds) {
477
- this.logger.info(() => ({
478
- msg: "Dummy fetchRule() has no effect (redis is not ready)",
479
- data: {
480
- ruleIds
481
- }
482
- }));
483
- return [];
484
- }
485
- async findRules(filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) {
486
- this.logger.info(() => ({
487
- msg: "Dummy findRules() has no effect (redis is not ready)",
488
- data: {
489
- filter
490
- }
491
- }));
492
- return [];
493
- }
494
- async findRuleIds(filter, matchingFieldsOnly = false) {
495
- this.logger.info(() => ({
496
- msg: "Dummy findRuleIds() has no effect (redis is not ready)",
497
- data: {
498
- filter
499
- }
500
- }));
501
- return [];
502
- }
503
- async fetchAllRuleIds(batchHandler) {
504
- this.logger.info(() => ({
505
- msg: "Dummy fetchAllRuleIds() has no effect (redis is not ready)"
506
- }));
507
- }
508
- }
81
+ var RedisRulesReader = class {
82
+ constructor(client, logger) {
83
+ this.client = client;
84
+ this.logger = logger;
85
+ }
86
+ async getMissingRuleIds(ruleIds) {
87
+ const ruleKeys = this.getRuleKeys(ruleIds);
88
+ const keyBatches = (0, _prosopo_common.chunkIntoBatches)(ruleKeys, require_redis_redisClient.REDIS_BATCH_SIZE);
89
+ return (await (0, _prosopo_common.executeBatchesSequentially)(keyBatches, async (keysBatch) => require_redis_redisClient.getMissingRedisKeys(this.client, keysBatch))).flat().map((ruleKey) => ruleKey.slice(require_redis_redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length));
90
+ }
91
+ async fetchRules(ruleIds) {
92
+ const ruleKeys = this.getRuleKeys(ruleIds);
93
+ const keyBatches = (0, _prosopo_common.chunkIntoBatches)(ruleKeys, require_redis_redisClient.REDIS_BATCH_SIZE);
94
+ return (await (0, _prosopo_common.executeBatchesSequentially)(keyBatches, (keysBatch) => this.fetchRuleEntries(keysBatch))).flat();
95
+ }
96
+ async findRules(filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) {
97
+ const query = require_redis_reader_redisRulesQuery.getRulesRedisQuery(filter, matchingFieldsOnly);
98
+ if (skipEmptyUserScopes && query === "( @clientId:{global} | ismissing(@clientId) )") return [];
99
+ if (matchingFieldsOnly && filter.userScope !== void 0) return this.findRulesSplit(filter);
100
+ return this.findRulesGreedy(filter, query);
101
+ }
102
+ async findRulesSplit(filter) {
103
+ const userScope = filter.userScope;
104
+ if (userScope === void 0) return [];
105
+ const clientId = filter.policyScope?.clientId;
106
+ const subQueries = require_redis_reader_redisRulesSplitQuery.buildScopedRulesSubQueries(userScope, clientId, { ...filter.blockOnly === true && { blockOnly: true } });
107
+ try {
108
+ const keyLists = await Promise.all(subQueries.map((sub) => require_redis_reader_redisAggregate.searchRedisKeysBounded(this.client, sub.query, SPLIT_MAX_CANDIDATES_PER_SUB).catch((err) => {
109
+ this.logger.error(() => ({
110
+ err,
111
+ data: { inspect: node_util.inspect({
112
+ subKind: sub.kind,
113
+ query: sub.query
114
+ }, { depth: null }) },
115
+ msg: "failed to execute split rule sub-query"
116
+ }));
117
+ return [];
118
+ })));
119
+ const uniqueKeys = /* @__PURE__ */ new Set();
120
+ for (const list of keyLists) for (const key of list) uniqueKeys.add(key);
121
+ if (uniqueKeys.size === 0) return [];
122
+ const ruleKeys = [...uniqueKeys];
123
+ this.logger.debug(() => ({
124
+ msg: "Executed split rule sub-queries",
125
+ data: { inspect: node_util.inspect({
126
+ subQueryCount: subQueries.length,
127
+ uniqueCandidates: ruleKeys.length,
128
+ perSubQueryCounts: keyLists.map((l, i) => ({
129
+ kind: subQueries[i]?.kind,
130
+ found: l.length
131
+ })),
132
+ filter
133
+ }, { depth: null }) }
134
+ }));
135
+ const { records } = await require_redis_redisClient.fetchRedisHashRecords(this.client, ruleKeys, this.logger);
136
+ const nonEmptyRecords = records.filter((record) => Object.keys(record).length > 0);
137
+ return sortByReaderRank(require_redis_redisClient.parseRedisRecords(nonEmptyRecords, require_ruleInput_ruleInput.accessRuleInput, this.logger));
138
+ } catch (e) {
139
+ this.logger.error(() => ({
140
+ err: e,
141
+ data: { inspect: node_util.inspect({ filter }, { depth: null }) },
142
+ msg: "failed to execute split rule query set"
143
+ }));
144
+ return [];
145
+ }
146
+ }
147
+ async findRulesRanked(filter, query) {
148
+ try {
149
+ const reply = await this.client.ft.aggregate(require_redis_redisRuleIndex.ACCESS_RULES_REDIS_INDEX_NAME, query, {
150
+ DIALECT: 2,
151
+ LOAD: [...RULE_LOAD_FIELDS],
152
+ STEPS: [
153
+ {
154
+ type: "FILTER",
155
+ expression: "exists(@type)"
156
+ },
157
+ {
158
+ type: "APPLY",
159
+ expression: SPECIFICITY_EXPR,
160
+ AS: "_spec"
161
+ },
162
+ {
163
+ type: "APPLY",
164
+ expression: SEVERITY_EXPR,
165
+ AS: "_sev"
166
+ },
167
+ {
168
+ type: "APPLY",
169
+ expression: RANK_EXPR,
170
+ AS: "_rank"
171
+ },
172
+ {
173
+ type: "SORTBY",
174
+ BY: [{
175
+ BY: "@_rank",
176
+ DIRECTION: "DESC"
177
+ }]
178
+ },
179
+ {
180
+ type: "LIMIT",
181
+ from: 0,
182
+ size: 20
183
+ }
184
+ ]
185
+ });
186
+ if (reply.results.length === 0) return [];
187
+ this.logger.debug(() => ({
188
+ msg: "Executed ranked search query",
189
+ data: { inspect: node_util.inspect({
190
+ filter,
191
+ foundCount: reply.results.length,
192
+ query
193
+ }, { depth: null }) }
194
+ }));
195
+ const ruleKeys = [];
196
+ for (const result of reply.results) {
197
+ const key = result.__key;
198
+ if (typeof key === "string") ruleKeys.push(key);
199
+ }
200
+ if (ruleKeys.length === 0) return [];
201
+ const { records } = await require_redis_redisClient.fetchRedisHashRecords(this.client, ruleKeys, this.logger);
202
+ const nonEmptyRecords = records.filter((record) => Object.keys(record).length > 0);
203
+ return require_redis_redisClient.parseRedisRecords(nonEmptyRecords, require_ruleInput_ruleInput.accessRuleInput, this.logger);
204
+ } catch (e) {
205
+ this.logger.error(() => ({
206
+ err: e,
207
+ data: { inspect: node_util.inspect({
208
+ query,
209
+ filter
210
+ }, { depth: null }) },
211
+ msg: "failed to execute ranked search query"
212
+ }));
213
+ return [];
214
+ }
215
+ }
216
+ async findRulesGreedy(filter, query) {
217
+ try {
218
+ const ruleKeys = await require_redis_reader_redisAggregate.aggregateRedisKeys(this.client, query, this.logger, void 0, GREEDY_MAX_CANDIDATES);
219
+ if (ruleKeys.length === 0) return [];
220
+ this.logger.debug(() => ({
221
+ msg: "Executed greedy search query",
222
+ data: { inspect: node_util.inspect({
223
+ filter,
224
+ foundCount: ruleKeys.length,
225
+ query
226
+ }, { depth: null }) }
227
+ }));
228
+ const { records } = await require_redis_redisClient.fetchRedisHashRecords(this.client, ruleKeys, this.logger);
229
+ const nonEmptyRecords = records.filter((record) => Object.keys(record).length > 0);
230
+ return require_redis_redisClient.parseRedisRecords(nonEmptyRecords, require_ruleInput_ruleInput.accessRuleInput, this.logger);
231
+ } catch (e) {
232
+ this.logger.error(() => ({
233
+ err: e,
234
+ data: { inspect: node_util.inspect({
235
+ query,
236
+ filter
237
+ }, { depth: null }) },
238
+ msg: "failed to execute greedy search query"
239
+ }));
240
+ return [];
241
+ }
242
+ }
243
+ async findRuleIds(filter, matchingFieldsOnly = false) {
244
+ const query = require_redis_reader_redisRulesQuery.getRulesRedisQuery(filter, matchingFieldsOnly);
245
+ let ruleIds = [];
246
+ try {
247
+ ruleIds = (await require_redis_reader_redisAggregate.aggregateRedisKeys(this.client, query, this.logger)).map((ruleKey) => ruleKey.slice(require_redis_redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length));
248
+ } catch (e) {
249
+ this.logger.error(() => ({
250
+ err: e,
251
+ data: { inspect: node_util.inspect({
252
+ query,
253
+ filter
254
+ }, { depth: null }) },
255
+ msg: "Failed to execute search query for rule IDs"
256
+ }));
257
+ return [];
258
+ }
259
+ this.logger.debug(() => ({
260
+ msg: "Executed search query for rule IDs",
261
+ data: {
262
+ query,
263
+ foundCount: ruleIds.length,
264
+ foundIds: ruleIds
265
+ }
266
+ }));
267
+ return ruleIds;
268
+ }
269
+ async fetchAllRuleIds(batchHandler) {
270
+ const keysBatchHandler = async (keys) => {
271
+ await batchHandler(keys.map((ruleKey) => ruleKey.slice(require_redis_redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)));
272
+ };
273
+ await require_redis_reader_redisAggregate.aggregateRedisKeys(this.client, "*", this.logger, keysBatchHandler);
274
+ }
275
+ async fetchRuleEntries(keys) {
276
+ const { records, expirations } = await require_redis_redisClient.fetchRedisHashRecords(this.client, keys, this.logger);
277
+ const entries = [];
278
+ for (const [index, ruleData] of records.entries()) if (Object.keys(ruleData).length > 0) {
279
+ const rule = require_redis_redisClient.parseRedisRecords([ruleData], require_ruleInput_ruleInput.accessRuleInput, this.logger)[0];
280
+ if (rule) entries.push({
281
+ rule,
282
+ expiresUnixTimestamp: expirations[index]
283
+ });
284
+ }
285
+ return entries;
286
+ }
287
+ getRuleKeys(ruleIds) {
288
+ return ruleIds.map((id) => `${require_redis_redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX}${id}`);
289
+ }
290
+ };
291
+ var DummyRedisRulesReader = class {
292
+ constructor(logger) {
293
+ this.logger = logger;
294
+ }
295
+ async getMissingRuleIds(ruleIds) {
296
+ this.logger.info(() => ({
297
+ msg: "Dummy getMissingRuleIds() has no effect (redis is not ready)",
298
+ data: { ruleIds }
299
+ }));
300
+ return [];
301
+ }
302
+ async fetchRules(ruleIds) {
303
+ this.logger.info(() => ({
304
+ msg: "Dummy fetchRule() has no effect (redis is not ready)",
305
+ data: { ruleIds }
306
+ }));
307
+ return [];
308
+ }
309
+ async findRules(filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) {
310
+ this.logger.info(() => ({
311
+ msg: "Dummy findRules() has no effect (redis is not ready)",
312
+ data: { filter }
313
+ }));
314
+ return [];
315
+ }
316
+ async findRuleIds(filter, matchingFieldsOnly = false) {
317
+ this.logger.info(() => ({
318
+ msg: "Dummy findRuleIds() has no effect (redis is not ready)",
319
+ data: { filter }
320
+ }));
321
+ return [];
322
+ }
323
+ async fetchAllRuleIds(batchHandler) {
324
+ this.logger.info(() => ({ msg: "Dummy fetchAllRuleIds() has no effect (redis is not ready)" }));
325
+ }
326
+ };
327
+ //#endregion
509
328
  exports.DummyRedisRulesReader = DummyRedisRulesReader;
510
329
  exports.RedisRulesReader = RedisRulesReader;
511
330
  exports.SERVER_SIDE_RANK_TOP_N = SERVER_SIDE_RANK_TOP_N;