@prosopo/user-access-policy 3.9.0 → 3.10.0

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 (27) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +35 -35
  2. package/.turbo/turbo-build$colon$tsc.log +14 -14
  3. package/.turbo/turbo-build.log +4 -4
  4. package/CHANGELOG.md +21 -0
  5. package/dist/cjs/redis/reader/redisAggregate.cjs +22 -4
  6. package/dist/cjs/redis/reader/redisRulesReader.cjs +133 -35
  7. package/dist/redis/reader/redisAggregate.d.ts +1 -1
  8. package/dist/redis/reader/redisAggregate.d.ts.map +1 -1
  9. package/dist/redis/reader/redisAggregate.js +22 -4
  10. package/dist/redis/reader/redisAggregate.js.map +1 -1
  11. package/dist/redis/reader/redisRulesReader.d.ts +4 -1
  12. package/dist/redis/reader/redisRulesReader.d.ts.map +1 -1
  13. package/dist/redis/reader/redisRulesReader.js +132 -34
  14. package/dist/redis/reader/redisRulesReader.js.map +1 -1
  15. package/dist/tests/redis/redisRulesReaderRank.benchmark.integration.test.d.ts +2 -0
  16. package/dist/tests/redis/redisRulesReaderRank.benchmark.integration.test.d.ts.map +1 -0
  17. package/dist/tests/redis/redisRulesReaderRank.benchmark.integration.test.js +189 -0
  18. package/dist/tests/redis/redisRulesReaderRank.benchmark.integration.test.js.map +1 -0
  19. package/dist/tests/redis/redisRulesStorage.integration.test.js +31 -0
  20. package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
  21. package/package.json +3 -3
  22. package/src/redis/reader/redisAggregate.ts +27 -1
  23. package/src/redis/reader/redisRulesReader.ts +197 -37
  24. package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +338 -0
  25. package/src/tests/redis/redisRulesStorage.integration.test.ts +52 -0
  26. package/tsconfig.tsbuildinfo +1 -1
  27. package/vite.test.config.ts +12 -1
@@ -1,5 +1,5 @@
1
1
 
2
- > @prosopo/user-access-policy@3.9.0 build:cjs
2
+ > @prosopo/user-access-policy@3.10.0 build:cjs
3
3
  > NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
4
4
 
5
5
  ViteCommonJSConfig: .
@@ -35,37 +35,37 @@ transforming...
35
35
  Build end
36
36
  ✓ 33 modules transformed.
37
37
  rendering chunks...
38
- dist/cjs/api/delete/.export.cjs 0.01 kB
39
- dist/cjs/api/read/.export.cjs 0.01 kB
40
- dist/cjs/api/write/.export.cjs 0.01 kB
41
- dist/cjs/mongoose/.export.cjs 0.23 kB
42
- dist/cjs/rulesStorage.cjs 0.34 kB
43
- dist/cjs/rule.cjs 0.34 kB
44
- dist/cjs/redis/.export.cjs 0.36 kB
45
- dist/cjs/ruleInput/.export.cjs 0.46 kB
46
- dist/cjs/api/.export.cjs 0.57 kB
47
- dist/cjs/api/delete/deleteAllRules.cjs 0.77 kB
48
- dist/cjs/ruleRecord.cjs 0.83 kB
49
- dist/cjs/api/read/getMissingIds.cjs 1.15 kB
50
- dist/cjs/api/read/fetchRules.cjs 1.22 kB
51
- dist/cjs/.export.cjs 1.24 kB
52
- dist/cjs/redis/redisRulesStorage.cjs 1.35 kB
53
- dist/cjs/mongoose/mongooseRuleSchema.cjs 1.48 kB
54
- dist/cjs/api/delete/deleteRules.cjs 1.51 kB
55
- dist/cjs/api/delete/deleteRuleGroups.cjs 1.57 kB
56
- dist/cjs/api/write/rehashRules.cjs 1.64 kB
57
- dist/cjs/api/read/findRuleIds.cjs 1.65 kB
58
- dist/cjs/ruleInput/policyInput.cjs 1.70 kB
59
- dist/cjs/redis/reader/redisAggregate.cjs 1.95 kB
60
- dist/cjs/ruleInput/ruleInput.cjs 2.07 kB
61
- dist/cjs/redis/redisClient.cjs 2.09 kB
62
- dist/cjs/ruleInput/userScopeInput.cjs 2.45 kB
63
- dist/cjs/redis/redisRuleIndex.cjs 2.46 kB
64
- dist/cjs/api/rulesApiClient.cjs 2.60 kB
65
- dist/cjs/transformRule.cjs 3.05 kB
66
- dist/cjs/api/write/insertRules.cjs 3.38 kB
67
- dist/cjs/redis/redisRulesWriter.cjs 3.44 kB
68
- dist/cjs/redis/reader/redisRulesQuery.cjs 4.34 kB
69
- dist/cjs/api/ruleApiRoutes.cjs 4.55 kB
70
- dist/cjs/redis/reader/redisRulesReader.cjs 7.01 kB
71
- ✓ built in 451ms
38
+ dist/cjs/api/delete/.export.cjs  0.01 kB
39
+ dist/cjs/api/read/.export.cjs  0.01 kB
40
+ dist/cjs/api/write/.export.cjs  0.01 kB
41
+ dist/cjs/mongoose/.export.cjs  0.23 kB
42
+ dist/cjs/rulesStorage.cjs  0.34 kB
43
+ dist/cjs/rule.cjs  0.34 kB
44
+ dist/cjs/redis/.export.cjs  0.36 kB
45
+ dist/cjs/ruleInput/.export.cjs  0.46 kB
46
+ dist/cjs/api/.export.cjs  0.57 kB
47
+ dist/cjs/api/delete/deleteAllRules.cjs  0.77 kB
48
+ dist/cjs/ruleRecord.cjs  0.83 kB
49
+ dist/cjs/api/read/getMissingIds.cjs  1.15 kB
50
+ dist/cjs/api/read/fetchRules.cjs  1.22 kB
51
+ dist/cjs/.export.cjs  1.24 kB
52
+ dist/cjs/redis/redisRulesStorage.cjs  1.35 kB
53
+ dist/cjs/mongoose/mongooseRuleSchema.cjs  1.48 kB
54
+ dist/cjs/api/delete/deleteRules.cjs  1.51 kB
55
+ dist/cjs/api/delete/deleteRuleGroups.cjs  1.57 kB
56
+ dist/cjs/api/write/rehashRules.cjs  1.64 kB
57
+ dist/cjs/api/read/findRuleIds.cjs  1.65 kB
58
+ dist/cjs/ruleInput/policyInput.cjs  1.70 kB
59
+ dist/cjs/ruleInput/ruleInput.cjs  2.07 kB
60
+ dist/cjs/redis/redisClient.cjs  2.09 kB
61
+ dist/cjs/ruleInput/userScopeInput.cjs  2.45 kB
62
+ dist/cjs/redis/redisRuleIndex.cjs  2.46 kB
63
+ dist/cjs/api/rulesApiClient.cjs  2.60 kB
64
+ dist/cjs/redis/reader/redisAggregate.cjs  2.61 kB
65
+ dist/cjs/transformRule.cjs  3.05 kB
66
+ dist/cjs/api/write/insertRules.cjs  3.38 kB
67
+ dist/cjs/redis/redisRulesWriter.cjs  3.44 kB
68
+ dist/cjs/redis/reader/redisRulesQuery.cjs  4.34 kB
69
+ dist/cjs/api/ruleApiRoutes.cjs  4.55 kB
70
+ dist/cjs/redis/reader/redisRulesReader.cjs 10.28 kB
71
+ ✓ built in 465ms
@@ -1,8 +1,8 @@
1
1
 
2
- > @prosopo/user-access-policy@3.9.0 build:tsc
2
+ > @prosopo/user-access-policy@3.10.0 build:tsc
3
3
  > tsc --build --verbose
4
4
 
5
- 11:34:14 AM - Projects in this build:
5
+ 2:03:13 PM - Projects in this build:
6
6
  * ../../dev/config/tsconfig.json
7
7
  * ../locale/tsconfig.json
8
8
  * ../util/tsconfig.json
@@ -15,27 +15,27 @@
15
15
  * ../api/tsconfig.json
16
16
  * tsconfig.json
17
17
 
18
- 11:34:14 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
18
+ 2:03:13 PM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
19
19
 
20
- 11:34:14 AM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
20
+ 2:03:13 PM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/util.ts' is older than output '../locale/tsconfig.tsbuildinfo'
21
21
 
22
- 11:34:14 AM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
22
+ 2:03:13 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
23
23
 
24
- 11:34:14 AM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
24
+ 2:03:13 PM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
25
25
 
26
- 11:34:14 AM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
26
+ 2:03:13 PM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
27
27
 
28
- 11:34:14 AM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/procaptcha/api.ts' is older than output '../types/tsconfig.tsbuildinfo'
28
+ 2:03:14 PM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/provider/api.ts' is older than output '../types/tsconfig.tsbuildinfo'
29
29
 
30
- 11:34:14 AM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/tests/utils/batches.unit.test.ts' is older than output '../common/tsconfig.tsbuildinfo'
30
+ 2:03:14 PM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
31
31
 
32
- 11:34:14 AM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
32
+ 2:03:14 PM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
33
33
 
34
- 11:34:14 AM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/tests/testRedisConnection.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
34
+ 2:03:14 PM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/index.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
35
35
 
36
- 11:34:14 AM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/index.ts' is older than output '../api/tsconfig.tsbuildinfo'
36
+ 2:03:14 PM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/index.ts' is older than output '../api/tsconfig.tsbuildinfo'
37
37
 
38
- 11:34:14 AM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
38
+ 2:03:14 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
39
39
 
40
- 11:34:14 AM - Building project '/home/runner/work/captcha/captcha/packages/user-access-policy/tsconfig.json'...
40
+ 2:03:14 PM - Building project '/home/runner/work/captcha/captcha/packages/user-access-policy/tsconfig.json'...
41
41
 
@@ -1,9 +1,9 @@
1
1
 
2
- > @prosopo/user-access-policy@3.9.0 build
2
+ > @prosopo/user-access-policy@3.10.0 build
3
3
  > npm run build:cross-env -- --mode ${NODE_ENV:-development}
4
4
 
5
5
 
6
- > @prosopo/user-access-policy@3.9.0 build:cross-env
6
+ > @prosopo/user-access-policy@3.10.0 build:cross-env
7
7
  > vite build --config vite.esm.config.ts --mode production
8
8
 
9
9
  ViteEsmConfig: .
@@ -61,15 +61,15 @@ rendering chunks...
61
61
  dist/api/read/findRuleIds.js 1.54 kB
62
62
  dist/api/write/rehashRules.js 1.54 kB
63
63
  dist/ruleInput/ruleInput.js 1.72 kB
64
- dist/redis/reader/redisAggregate.js 1.80 kB
65
64
  dist/redis/redisClient.js 1.90 kB
66
65
  dist/redis/redisRuleIndex.js 2.01 kB
67
66
  dist/ruleInput/userScopeInput.js 2.22 kB
68
67
  dist/api/rulesApiClient.js 2.36 kB
68
+ dist/redis/reader/redisAggregate.js 2.46 kB
69
69
  dist/transformRule.js 2.76 kB
70
70
  dist/api/write/insertRules.js 3.22 kB
71
71
  dist/redis/redisRulesWriter.js 3.22 kB
72
72
  dist/redis/reader/redisRulesQuery.js 4.15 kB
73
73
  dist/api/ruleApiRoutes.js 4.33 kB
74
- dist/redis/reader/redisRulesReader.js 6.23 kB
74
+ dist/redis/reader/redisRulesReader.js 9.40 kB
75
75
  ✓ built in 499ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @prosopo/user-access-policy
2
2
 
3
+ ## 3.10.0
4
+ ### Minor Changes
5
+
6
+ - c1c7998: Server-side specificity rank for the access-rule lookup. Strict-match callers (the `blockMiddleware` and the verify-time `checkForHardBlock`) now issue one `FT.AGGREGATE` with `APPLY exists()` for specificity, `APPLY @type == "block"` for the severity tiebreak, `SORTBY @_rank DESC`, and `LIMIT 0 20`. Node receives at most 20 fully-populated rules — no follow-up HGETALL per candidate, no JS-side rank, no silent truncation past the LIMIT (which only applies after Redis has scored every candidate the strict filter returned).
7
+
8
+ Supersedes both the v3.6.38 regression (`b520cd94c` — FT.AGGREGATE WITHCURSOR materialising ~1190 hashes per request, pegged provider1 at ~125% CPU on pronode10) and the 3.6.38-hotfix1 shape that reverted to FT.SEARCH (re-opened the 1000-candidate silent-truncation bug). The greedy/admin path (`matchingFieldsOnly=false`) keeps the FT.AGGREGATE+CURSOR approach with a generous `GREEDY_MAX_CANDIDATES` cap since those callers do not run on the per-request hot path.
9
+
10
+ `packages/provider/src/api/blacklistRequestInspector.ts` flips `getPrioritisedAccessRule` to `matchingFieldsOnly: true` to engage the new path. The defensive JS `rankCandidateRules` is kept so any drift between the Redis-side score and the JS semantics surfaces as ordering, not as letting traffic through. New benchmark integration test seeds 10k rules across a realistic specificity distribution and asserts p50 < 80ms / p99 < 250ms over 200 lookups; local measurement is steady at p50 ≈ 20ms, p99 ≈ 24ms.
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [9b91e85]
15
+ - Updated dependencies [c80a05b]
16
+ - @prosopo/types@4.5.0
17
+ - @prosopo/api@3.4.12
18
+
19
+ ## 3.9.1
20
+ ### Patch Changes
21
+
22
+ - b520cd9: Paginate the greedy `findRules` RediSearch query via `FT.AGGREGATE WITHCURSOR` so the candidate set is no longer truncated at `REDIS_BATCH_SIZE` (1000). Under high-volume bot traffic, a single popular ja4 fingerprint can be carried by thousands of rules; the OR-style greedy query returned > 1000 candidates and `FT.SEARCH`'s LIMIT silently dropped the tail — block rules emitted by less-frequent detectors sat past offset 1000 and never reached the JS-side specificity sort, letting matching requests through. Aggregation cursors return the full result set, so ranking sees every candidate.
23
+
3
24
  ## 3.9.0
4
25
  ### Minor Changes
5
26
 
@@ -4,19 +4,36 @@ const zod = require("zod");
4
4
  const redisRulesQuery = require("./redisRulesQuery.cjs");
5
5
  const redisClient = require("../redisClient.cjs");
6
6
  const redisRuleIndex = require("../redisRuleIndex.cjs");
7
- const aggregateRedisKeys = async (client, query, logger, batchHandler) => {
7
+ const aggregateRedisKeys = async (client, query, logger, batchHandler, maxKeys) => {
8
8
  const keyField = "__key";
9
9
  const recordSchema = zod.z.object({
10
10
  // it's a reserved name for the record key
11
11
  [keyField]: zod.z.string()
12
12
  });
13
13
  const foundKeys = [];
14
+ let stopRequested = false;
14
15
  const addRecordKeys = async (records) => {
16
+ if (stopRequested) {
17
+ return;
18
+ }
15
19
  const parsedRecords = redisClient.parseRedisRecords(records, recordSchema, logger);
16
20
  const recordKeys = parsedRecords.map((record) => record[keyField]);
17
21
  if (batchHandler) {
18
22
  await batchHandler(recordKeys);
19
23
  } else {
24
+ if (maxKeys !== void 0 && foundKeys.length + recordKeys.length > maxKeys) {
25
+ const remaining = Math.max(0, maxKeys - foundKeys.length);
26
+ foundKeys.push(...recordKeys.slice(0, remaining));
27
+ stopRequested = true;
28
+ logger.warn(() => ({
29
+ msg: "Redis aggregation candidate cap hit; truncating result set. This can suppress less-frequent rules and should be investigated.",
30
+ data: {
31
+ maxKeys,
32
+ query
33
+ }
34
+ }));
35
+ return;
36
+ }
20
37
  foundKeys.push(...recordKeys);
21
38
  logger.debug(() => ({
22
39
  msg: "Processed aggregation batch",
@@ -35,11 +52,12 @@ const aggregateRedisKeys = async (client, query, logger, batchHandler) => {
35
52
  COUNT: redisClient.REDIS_BATCH_SIZE,
36
53
  LOAD: `@${keyField}`
37
54
  },
38
- addRecordKeys
55
+ addRecordKeys,
56
+ () => stopRequested
39
57
  );
40
58
  return foundKeys;
41
59
  };
42
- const executeAggregation = async (client, query, aggregateOptions, handleBatch) => {
60
+ const executeAggregation = async (client, query, aggregateOptions, handleBatch, shouldStop) => {
43
61
  const initialReply = await client.ft.aggregateWithCursor(
44
62
  redisRuleIndex.ACCESS_RULES_REDIS_INDEX_NAME,
45
63
  query,
@@ -47,7 +65,7 @@ const executeAggregation = async (client, query, aggregateOptions, handleBatch)
47
65
  );
48
66
  await handleBatch(initialReply.results);
49
67
  let cursor = initialReply.cursor;
50
- while (0 !== cursor) {
68
+ while (0 !== cursor && !shouldStop?.()) {
51
69
  const batchReply = await client.ft.cursorRead(
52
70
  redisRuleIndex.ACCESS_RULES_REDIS_INDEX_NAME,
53
71
  cursor,
@@ -5,6 +5,7 @@ const common = require("@prosopo/common");
5
5
  const redisRulesQuery = require("./redisRulesQuery.cjs");
6
6
  const redisClient = require("../redisClient.cjs");
7
7
  const redisRuleIndex = require("../redisRuleIndex.cjs");
8
+ const rule = require("../../rule.cjs");
8
9
  const ruleInput = require("../../ruleInput/ruleInput.cjs");
9
10
  const redisAggregate = require("./redisAggregate.cjs");
10
11
  function _interopNamespaceDefault(e) {
@@ -24,6 +25,48 @@ function _interopNamespaceDefault(e) {
24
25
  return Object.freeze(n);
25
26
  }
26
27
  const util__namespace = /* @__PURE__ */ _interopNamespaceDefault(util);
28
+ const SERVER_SIDE_RANK_TOP_N = 20;
29
+ const GREEDY_MAX_CANDIDATES = redisClient.REDIS_BATCH_SIZE * 10;
30
+ const SPECIFICITY_EXPR = [
31
+ "exists(@clientId)",
32
+ "exists(@userId)",
33
+ "exists(@ja4Hash)",
34
+ "exists(@headersHash)",
35
+ "exists(@userAgentHash)",
36
+ "exists(@headHash)",
37
+ "exists(@coords)",
38
+ "exists(@countryCode)",
39
+ "exists(@asn)",
40
+ "exists(@numericIp)",
41
+ "exists(@numericIpMaskMin)"
42
+ ].join(" + ");
43
+ const SEVERITY_EXPR = `(@type == "${rule.AccessPolicyType.Block}")`;
44
+ const RANK_EXPR = "(@_spec * 2) + @_sev";
45
+ const RULE_LOAD_FIELDS = [
46
+ "@__key",
47
+ "@type",
48
+ "@captchaType",
49
+ "@description",
50
+ "@solvedImagesCount",
51
+ "@imageThreshold",
52
+ "@powDifficulty",
53
+ "@unsolvedImagesCount",
54
+ "@frictionlessScore",
55
+ "@deferToVerify",
56
+ "@clientId",
57
+ "@groupId",
58
+ "@userId",
59
+ "@ja4Hash",
60
+ "@headersHash",
61
+ "@userAgentHash",
62
+ "@headHash",
63
+ "@coords",
64
+ "@countryCode",
65
+ "@asn",
66
+ "@numericIp",
67
+ "@numericIpMaskMin",
68
+ "@numericIpMaskMax"
69
+ ];
27
70
  class RedisRulesReader {
28
71
  constructor(client, logger) {
29
72
  this.client = client;
@@ -52,40 +95,102 @@ class RedisRulesReader {
52
95
  if (skipEmptyUserScopes && query === "ismissing(@clientId)") {
53
96
  return [];
54
97
  }
98
+ if (matchingFieldsOnly) {
99
+ return this.findRulesRanked(filter, query);
100
+ }
101
+ return this.findRulesGreedy(filter, query);
102
+ }
103
+ async findRulesRanked(filter, query) {
55
104
  try {
56
- const searchReply = await this.client.ft.searchNoContent(
105
+ const reply = await this.client.ft.aggregate(
57
106
  redisRuleIndex.ACCESS_RULES_REDIS_INDEX_NAME,
58
107
  query,
59
108
  {
60
109
  DIALECT: redisRulesQuery.REDIS_QUERY_DIALECT,
61
- // FT.search doesn't support "unlimited" selects
62
- LIMIT: {
63
- from: 0,
64
- size: redisClient.REDIS_BATCH_SIZE
65
- }
110
+ LOAD: [...RULE_LOAD_FIELDS],
111
+ STEPS: [
112
+ { type: "APPLY", expression: SPECIFICITY_EXPR, AS: "_spec" },
113
+ { type: "APPLY", expression: SEVERITY_EXPR, AS: "_sev" },
114
+ { type: "APPLY", expression: RANK_EXPR, AS: "_rank" },
115
+ {
116
+ type: "SORTBY",
117
+ BY: [{ BY: "@_rank", DIRECTION: "DESC" }]
118
+ // SORTBY MAX would let RediSearch keep only the
119
+ // top-N in a bounded heap rather than fully
120
+ // sorting, but the @redis/search client
121
+ // serialises MAX *before* DESC and RediSearch
122
+ // rejects that order ("MISSING ASC or DESC
123
+ // after sort field (MAX)"). LIMIT below still
124
+ // trims; SORTBY does a full sort. Acceptable
125
+ // because the strict-match filter already
126
+ // keeps the candidate set small in practice.
127
+ },
128
+ { type: "LIMIT", from: 0, size: SERVER_SIDE_RANK_TOP_N }
129
+ ]
66
130
  }
67
131
  );
68
- if (searchReply.total > 0) {
69
- this.logger.debug(() => ({
70
- msg: "Executed search query",
71
- data: {
72
- inspect: util__namespace.inspect(
73
- {
74
- filter,
75
- searchReply,
76
- query
77
- },
78
- { depth: null }
79
- )
80
- }
81
- }));
132
+ if (reply.results.length === 0) {
133
+ return [];
82
134
  }
83
- if (searchReply.documents.length === 0) {
135
+ this.logger.debug(() => ({
136
+ msg: "Executed ranked search query",
137
+ data: {
138
+ inspect: util__namespace.inspect(
139
+ {
140
+ filter,
141
+ foundCount: reply.results.length,
142
+ query
143
+ },
144
+ { depth: null }
145
+ )
146
+ }
147
+ }));
148
+ return redisClient.parseRedisRecords(reply.results, ruleInput.accessRuleInput, this.logger);
149
+ } catch (e) {
150
+ this.logger.error(() => ({
151
+ err: e,
152
+ data: {
153
+ inspect: util__namespace.inspect({ query, filter }, { depth: null })
154
+ },
155
+ msg: "failed to execute ranked search query"
156
+ }));
157
+ return [];
158
+ }
159
+ }
160
+ // Used only by admin / internal callers (matchingFieldsOnly=false).
161
+ // Not on the per-request blockMiddleware path any more, so the CPU
162
+ // cost of the HGETALL fanout is acceptable. Same shape as #2689's
163
+ // aggregate-with-cursor: returns every candidate up to
164
+ // GREEDY_MAX_CANDIDATES so we don't silently drop matches that
165
+ // admin tools depend on (e.g. ASN-wide rule listings).
166
+ async findRulesGreedy(filter, query) {
167
+ try {
168
+ const ruleKeys = await redisAggregate.aggregateRedisKeys(
169
+ this.client,
170
+ query,
171
+ this.logger,
172
+ void 0,
173
+ GREEDY_MAX_CANDIDATES
174
+ );
175
+ if (ruleKeys.length === 0) {
84
176
  return [];
85
177
  }
178
+ this.logger.debug(() => ({
179
+ msg: "Executed greedy search query",
180
+ data: {
181
+ inspect: util__namespace.inspect(
182
+ {
183
+ filter,
184
+ foundCount: ruleKeys.length,
185
+ query
186
+ },
187
+ { depth: null }
188
+ )
189
+ }
190
+ }));
86
191
  const { records } = await redisClient.fetchRedisHashRecords(
87
192
  this.client,
88
- searchReply.documents,
193
+ ruleKeys,
89
194
  this.logger
90
195
  );
91
196
  const nonEmptyRecords = records.filter(
@@ -96,17 +201,9 @@ class RedisRulesReader {
96
201
  this.logger.error(() => ({
97
202
  err: e,
98
203
  data: {
99
- inspect: util__namespace.inspect(
100
- {
101
- query,
102
- filter
103
- },
104
- {
105
- depth: null
106
- }
107
- )
204
+ inspect: util__namespace.inspect({ query, filter }, { depth: null })
108
205
  },
109
- msg: "failed to execute search query"
206
+ msg: "failed to execute greedy search query"
110
207
  }));
111
208
  return [];
112
209
  }
@@ -170,14 +267,14 @@ class RedisRulesReader {
170
267
  for (const [index, ruleData] of records.entries()) {
171
268
  const isRulePresent = Object.keys(ruleData).length > 0;
172
269
  if (isRulePresent) {
173
- const rule = redisClient.parseRedisRecords(
270
+ const rule2 = redisClient.parseRedisRecords(
174
271
  [ruleData],
175
272
  ruleInput.accessRuleInput,
176
273
  this.logger
177
274
  )[0];
178
- if (rule) {
275
+ if (rule2) {
179
276
  entries.push({
180
- rule,
277
+ rule: rule2,
181
278
  expiresUnixTimestamp: expirations[index]
182
279
  });
183
280
  }
@@ -237,3 +334,4 @@ class DummyRedisRulesReader {
237
334
  }
238
335
  exports.DummyRedisRulesReader = DummyRedisRulesReader;
239
336
  exports.RedisRulesReader = RedisRulesReader;
337
+ exports.SERVER_SIDE_RANK_TOP_N = SERVER_SIDE_RANK_TOP_N;
@@ -1,4 +1,4 @@
1
1
  import type { Logger } from "@prosopo/logger";
2
2
  import type { RedisClientType } from "redis";
3
- export declare const aggregateRedisKeys: (client: RedisClientType, query: string, logger: Logger, batchHandler?: (keys: string[]) => Promise<void>) => Promise<string[]>;
3
+ export declare const aggregateRedisKeys: (client: RedisClientType, query: string, logger: Logger, batchHandler?: (keys: string[]) => Promise<void>, maxKeys?: number) => Promise<string[]>;
4
4
  //# sourceMappingURL=redisAggregate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redisAggregate.d.ts","sourceRoot":"","sources":["../../../src/redis/reader/redisAggregate.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAU7C,eAAO,MAAM,kBAAkB,WACtB,eAAe,SAChB,MAAM,UACL,MAAM,iBACC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,KAC9C,OAAO,CAAC,MAAM,EAAE,CA0ClB,CAAC"}
1
+ {"version":3,"file":"redisAggregate.d.ts","sourceRoot":"","sources":["../../../src/redis/reader/redisAggregate.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAU7C,eAAO,MAAM,kBAAkB,WACtB,eAAe,SAChB,MAAM,UACL,MAAM,iBACC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,YACtC,MAAM,KACd,OAAO,CAAC,MAAM,EAAE,CAkElB,CAAC"}
@@ -2,19 +2,36 @@ import { z } from "zod";
2
2
  import { REDIS_QUERY_DIALECT } from "./redisRulesQuery.js";
3
3
  import { parseRedisRecords, REDIS_BATCH_SIZE } from "../redisClient.js";
4
4
  import { ACCESS_RULES_REDIS_INDEX_NAME } from "../redisRuleIndex.js";
5
- const aggregateRedisKeys = async (client, query, logger, batchHandler) => {
5
+ const aggregateRedisKeys = async (client, query, logger, batchHandler, maxKeys) => {
6
6
  const keyField = "__key";
7
7
  const recordSchema = z.object({
8
8
  // it's a reserved name for the record key
9
9
  [keyField]: z.string()
10
10
  });
11
11
  const foundKeys = [];
12
+ let stopRequested = false;
12
13
  const addRecordKeys = async (records) => {
14
+ if (stopRequested) {
15
+ return;
16
+ }
13
17
  const parsedRecords = parseRedisRecords(records, recordSchema, logger);
14
18
  const recordKeys = parsedRecords.map((record) => record[keyField]);
15
19
  if (batchHandler) {
16
20
  await batchHandler(recordKeys);
17
21
  } else {
22
+ if (maxKeys !== void 0 && foundKeys.length + recordKeys.length > maxKeys) {
23
+ const remaining = Math.max(0, maxKeys - foundKeys.length);
24
+ foundKeys.push(...recordKeys.slice(0, remaining));
25
+ stopRequested = true;
26
+ logger.warn(() => ({
27
+ msg: "Redis aggregation candidate cap hit; truncating result set. This can suppress less-frequent rules and should be investigated.",
28
+ data: {
29
+ maxKeys,
30
+ query
31
+ }
32
+ }));
33
+ return;
34
+ }
18
35
  foundKeys.push(...recordKeys);
19
36
  logger.debug(() => ({
20
37
  msg: "Processed aggregation batch",
@@ -33,11 +50,12 @@ const aggregateRedisKeys = async (client, query, logger, batchHandler) => {
33
50
  COUNT: REDIS_BATCH_SIZE,
34
51
  LOAD: `@${keyField}`
35
52
  },
36
- addRecordKeys
53
+ addRecordKeys,
54
+ () => stopRequested
37
55
  );
38
56
  return foundKeys;
39
57
  };
40
- const executeAggregation = async (client, query, aggregateOptions, handleBatch) => {
58
+ const executeAggregation = async (client, query, aggregateOptions, handleBatch, shouldStop) => {
41
59
  const initialReply = await client.ft.aggregateWithCursor(
42
60
  ACCESS_RULES_REDIS_INDEX_NAME,
43
61
  query,
@@ -45,7 +63,7 @@ const executeAggregation = async (client, query, aggregateOptions, handleBatch)
45
63
  );
46
64
  await handleBatch(initialReply.results);
47
65
  let cursor = initialReply.cursor;
48
- while (0 !== cursor) {
66
+ while (0 !== cursor && !shouldStop?.()) {
49
67
  const batchReply = await client.ft.cursorRead(
50
68
  ACCESS_RULES_REDIS_INDEX_NAME,
51
69
  cursor,
@@ -1 +1 @@
1
- {"version":3,"file":"redisAggregate.js","sourceRoot":"","sources":["../../../src/redis/reader/redisAggregate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAC9E,OAAO,EACN,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAGhF,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACtC,MAAuB,EACvB,KAAa,EACb,MAAc,EACd,YAAgD,EAC5B,EAAE;IACtB,MAAM,QAAQ,GAAG,OAAO,CAAC;IAEzB,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;QAE7B,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC,CAAC;IAEH,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,MAAM,aAAa,GAAG,KAAK,EAAE,OAAiB,EAAE,EAAE;QACjD,MAAM,aAAa,GAAG,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;QAEvE,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAEnE,IAAI,YAAY,EAAE,CAAC;YAClB,MAAM,YAAY,CAAC,UAAU,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACP,SAAS,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;YAE9B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;gBACnB,GAAG,EAAE,6BAA6B;gBAClC,IAAI,EAAE;oBACL,IAAI,EAAE,UAAU,CAAC,MAAM;iBACvB;aACD,CAAC,CAAC,CAAC;QACL,CAAC;IACF,CAAC,CAAC;IAEF,MAAM,kBAAkB,CACvB,MAAM,EACN,KAAK,EACL;QAEC,OAAO,EAAE,mBAAmB;QAC5B,KAAK,EAAE,gBAAgB;QACvB,IAAI,EAAE,IAAI,QAAQ,EAAE;KACpB,EACD,aAAa,CACb,CAAC;IAEF,OAAO,SAAS,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,KAAK,EAC/B,MAAuB,EACvB,KAAa,EACb,gBAA8C,EAC9C,WAAiD,EACjC,EAAE;IAClB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,mBAAmB,CACvD,6BAA6B,EAC7B,KAAK,EACL,gBAAgB,CAChB,CAAC;IAEF,MAAM,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAExC,IAAI,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC;IAEjC,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC;QACrB,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,UAAU,CAC5C,6BAA6B,EAC7B,MAAM,EACN,EAAE,KAAK,EAAE,gBAAgB,CAAC,KAAK,EAAE,CACjC,CAAC;QAEF,MAAM,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAEtC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,CAAC;AACF,CAAC,CAAC"}
1
+ {"version":3,"file":"redisAggregate.js","sourceRoot":"","sources":["../../../src/redis/reader/redisAggregate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAC9E,OAAO,EACN,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAGhF,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACtC,MAAuB,EACvB,KAAa,EACb,MAAc,EACd,YAAgD,EAChD,OAAgB,EACI,EAAE;IACtB,MAAM,QAAQ,GAAG,OAAO,CAAC;IAEzB,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;QAE7B,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC,CAAC;IAEH,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,aAAa,GAAG,KAAK,CAAC;IAE1B,MAAM,aAAa,GAAG,KAAK,EAAE,OAAiB,EAAE,EAAE;QACjD,IAAI,aAAa,EAAE,CAAC;YACnB,OAAO;QACR,CAAC;QAED,MAAM,aAAa,GAAG,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;QAEvE,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAEnE,IAAI,YAAY,EAAE,CAAC;YAClB,MAAM,YAAY,CAAC,UAAU,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACP,IACC,OAAO,KAAK,SAAS;gBACrB,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,EAC7C,CAAC;gBACF,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC1D,SAAS,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;gBAClD,aAAa,GAAG,IAAI,CAAC;gBAErB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;oBAClB,GAAG,EAAE,+HAA+H;oBACpI,IAAI,EAAE;wBACL,OAAO;wBACP,KAAK;qBACL;iBACD,CAAC,CAAC,CAAC;gBACJ,OAAO;YACR,CAAC;YAED,SAAS,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;YAE9B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;gBACnB,GAAG,EAAE,6BAA6B;gBAClC,IAAI,EAAE;oBACL,IAAI,EAAE,UAAU,CAAC,MAAM;iBACvB;aACD,CAAC,CAAC,CAAC;QACL,CAAC;IACF,CAAC,CAAC;IAEF,MAAM,kBAAkB,CACvB,MAAM,EACN,KAAK,EACL;QAEC,OAAO,EAAE,mBAAmB;QAC5B,KAAK,EAAE,gBAAgB;QACvB,IAAI,EAAE,IAAI,QAAQ,EAAE;KACpB,EACD,aAAa,EACb,GAAG,EAAE,CAAC,aAAa,CACnB,CAAC;IAEF,OAAO,SAAS,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,KAAK,EAC/B,MAAuB,EACvB,KAAa,EACb,gBAA8C,EAC9C,WAAiD,EACjD,UAA0B,EACV,EAAE;IAClB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,mBAAmB,CACvD,6BAA6B,EAC7B,KAAK,EACL,gBAAgB,CAChB,CAAC;IAEF,MAAM,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAExC,IAAI,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC;IAEjC,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,EAAE,CAAC;QACxC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,UAAU,CAC5C,6BAA6B,EAC7B,MAAM,EACN,EAAE,KAAK,EAAE,gBAAgB,CAAC,KAAK,EAAE,CACjC,CAAC;QAEF,MAAM,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAEtC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,CAAC;AACF,CAAC,CAAC"}
@@ -1,7 +1,8 @@
1
1
  import type { Logger } from "@prosopo/logger";
2
2
  import type { RedisClientType } from "redis";
3
- import type { AccessRule } from "#policy/rule.js";
3
+ import { type AccessRule } from "#policy/rule.js";
4
4
  import type { AccessRuleEntry, AccessRulesFilter, AccessRulesReader } from "#policy/rulesStorage.js";
5
+ export declare const SERVER_SIDE_RANK_TOP_N = 20;
5
6
  export declare class RedisRulesReader implements AccessRulesReader {
6
7
  private readonly client;
7
8
  private readonly logger;
@@ -9,6 +10,8 @@ export declare class RedisRulesReader implements AccessRulesReader {
9
10
  getMissingRuleIds(ruleIds: string[]): Promise<string[]>;
10
11
  fetchRules(ruleIds: string[]): Promise<AccessRuleEntry[]>;
11
12
  findRules(filter: AccessRulesFilter, matchingFieldsOnly?: boolean, skipEmptyUserScopes?: boolean): Promise<AccessRule[]>;
13
+ private findRulesRanked;
14
+ private findRulesGreedy;
12
15
  findRuleIds(filter: AccessRulesFilter, matchingFieldsOnly?: boolean): Promise<string[]>;
13
16
  fetchAllRuleIds(batchHandler: (ruleIds: string[]) => Promise<void>): Promise<void>;
14
17
  protected fetchRuleEntries(keys: string[]): Promise<AccessRuleEntry[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"redisRulesReader.d.ts","sourceRoot":"","sources":["../../../src/redis/reader/redisRulesReader.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAe7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EACX,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AAGjC,qBAAa,gBAAiB,YAAW,iBAAiB;IAExD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM;IAG1B,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAcvD,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAazD,SAAS,CACd,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,EAC1B,mBAAmB,UAAO,GACxB,OAAO,CAAC,UAAU,EAAE,CAAC;IA8ElB,WAAW,CAChB,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,GACxB,OAAO,CAAC,MAAM,EAAE,CAAC;IAgDd,eAAe,CACpB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;cAYA,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA8B5E,SAAS,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;CAGlD;AAED,qBAAa,qBAAsB,YAAW,iBAAiB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAErC,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAWvD,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAWzD,SAAS,CACd,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,EAC1B,mBAAmB,UAAO,GACxB,OAAO,CAAC,UAAU,EAAE,CAAC;IAWlB,WAAW,CAChB,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,GACxB,OAAO,CAAC,MAAM,EAAE,CAAC;IAWd,eAAe,CACpB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;CAKhB"}
1
+ {"version":3,"file":"redisRulesReader.d.ts","sourceRoot":"","sources":["../../../src/redis/reader/redisRulesReader.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAe7C,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEpE,OAAO,KAAK,EACX,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AAoBjC,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAoEzC,qBAAa,gBAAiB,YAAW,iBAAiB;IAExD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM;IAG1B,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAcvD,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAazD,SAAS,CACd,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,EAC1B,mBAAmB,UAAO,GACxB,OAAO,CAAC,UAAU,EAAE,CAAC;YAuBV,eAAe;YA2Ef,eAAe;IAuDvB,WAAW,CAChB,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,GACxB,OAAO,CAAC,MAAM,EAAE,CAAC;IAgDd,eAAe,CACpB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;cAYA,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA8B5E,SAAS,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;CAGlD;AAED,qBAAa,qBAAsB,YAAW,iBAAiB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAErC,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAWvD,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAWzD,SAAS,CACd,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,EAC1B,mBAAmB,UAAO,GACxB,OAAO,CAAC,UAAU,EAAE,CAAC;IAWlB,WAAW,CAChB,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,GACxB,OAAO,CAAC,MAAM,EAAE,CAAC;IAWd,eAAe,CACpB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;CAKhB"}