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