@prosopo/user-access-policy 3.6.0 → 3.7.11

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 (145) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +21 -19
  2. package/.turbo/turbo-build$colon$tsc.log +16 -13
  3. package/.turbo/turbo-build.log +22 -20
  4. package/CHANGELOG.md +332 -0
  5. package/dist/api/delete/deleteAllRules.d.ts +2 -2
  6. package/dist/api/delete/deleteAllRules.d.ts.map +1 -1
  7. package/dist/api/delete/deleteAllRules.js +3 -2
  8. package/dist/api/delete/deleteAllRules.js.map +1 -1
  9. package/dist/api/delete/deleteRuleGroups.d.ts +2 -2
  10. package/dist/api/delete/deleteRuleGroups.d.ts.map +1 -1
  11. package/dist/api/delete/deleteRuleGroups.js +3 -2
  12. package/dist/api/delete/deleteRuleGroups.js.map +1 -1
  13. package/dist/api/delete/deleteRules.d.ts +2 -2
  14. package/dist/api/delete/deleteRules.d.ts.map +1 -1
  15. package/dist/api/delete/deleteRules.js +3 -2
  16. package/dist/api/delete/deleteRules.js.map +1 -1
  17. package/dist/api/read/fetchRules.d.ts +2 -2
  18. package/dist/api/read/fetchRules.d.ts.map +1 -1
  19. package/dist/api/read/fetchRules.js +4 -3
  20. package/dist/api/read/fetchRules.js.map +1 -1
  21. package/dist/api/read/findRuleIds.d.ts +2 -2
  22. package/dist/api/read/findRuleIds.d.ts.map +1 -1
  23. package/dist/api/read/findRuleIds.js +3 -2
  24. package/dist/api/read/findRuleIds.js.map +1 -1
  25. package/dist/api/read/getMissingIds.d.ts +2 -2
  26. package/dist/api/read/getMissingIds.d.ts.map +1 -1
  27. package/dist/api/read/getMissingIds.js +4 -3
  28. package/dist/api/read/getMissingIds.js.map +1 -1
  29. package/dist/api/ruleApiRoutes.d.ts +1 -1
  30. package/dist/api/ruleApiRoutes.d.ts.map +1 -1
  31. package/dist/api/ruleApiRoutes.js.map +1 -1
  32. package/dist/api/rulesApiClient.d.ts +9 -9
  33. package/dist/api/rulesApiClient.d.ts.map +1 -1
  34. package/dist/api/rulesApiClient.js +18 -19
  35. package/dist/api/rulesApiClient.js.map +1 -1
  36. package/dist/api/write/insertRules.d.ts +2 -2
  37. package/dist/api/write/insertRules.d.ts.map +1 -1
  38. package/dist/api/write/insertRules.js +7 -6
  39. package/dist/api/write/insertRules.js.map +1 -1
  40. package/dist/api/write/rehashRules.d.ts +2 -2
  41. package/dist/api/write/rehashRules.d.ts.map +1 -1
  42. package/dist/api/write/rehashRules.js +7 -6
  43. package/dist/api/write/rehashRules.js.map +1 -1
  44. package/dist/cjs/api/delete/deleteAllRules.cjs +3 -2
  45. package/dist/cjs/api/delete/deleteRuleGroups.cjs +3 -2
  46. package/dist/cjs/api/delete/deleteRules.cjs +3 -2
  47. package/dist/cjs/api/read/fetchRules.cjs +4 -3
  48. package/dist/cjs/api/read/findRuleIds.cjs +3 -2
  49. package/dist/cjs/api/read/getMissingIds.cjs +4 -3
  50. package/dist/cjs/api/rulesApiClient.cjs +18 -19
  51. package/dist/cjs/api/write/insertRules.cjs +9 -8
  52. package/dist/cjs/api/write/rehashRules.cjs +7 -6
  53. package/dist/cjs/mongoose/mongooseRuleSchema.cjs +2 -1
  54. package/dist/cjs/redis/reader/redisRulesQuery.cjs +6 -0
  55. package/dist/cjs/redis/reader/redisRulesReader.cjs +13 -4
  56. package/dist/cjs/redis/redisRuleIndex.cjs +2 -1
  57. package/dist/cjs/ruleInput/userScopeInput.cjs +2 -1
  58. package/dist/cjs/ruleRecord.cjs +2 -1
  59. package/dist/mongoose/mongooseRuleSchema.d.ts.map +1 -1
  60. package/dist/mongoose/mongooseRuleSchema.js +2 -1
  61. package/dist/mongoose/mongooseRuleSchema.js.map +1 -1
  62. package/dist/redis/reader/redisAggregate.d.ts +1 -1
  63. package/dist/redis/reader/redisRulesQuery.d.ts.map +1 -1
  64. package/dist/redis/reader/redisRulesQuery.js +6 -0
  65. package/dist/redis/reader/redisRulesQuery.js.map +1 -1
  66. package/dist/redis/reader/redisRulesReader.d.ts +1 -1
  67. package/dist/redis/reader/redisRulesReader.d.ts.map +1 -1
  68. package/dist/redis/reader/redisRulesReader.js +14 -5
  69. package/dist/redis/reader/redisRulesReader.js.map +1 -1
  70. package/dist/redis/redisClient.d.ts +1 -1
  71. package/dist/redis/redisRuleIndex.d.ts.map +1 -1
  72. package/dist/redis/redisRuleIndex.js +2 -1
  73. package/dist/redis/redisRuleIndex.js.map +1 -1
  74. package/dist/redis/redisRulesStorage.d.ts +1 -1
  75. package/dist/redis/redisRulesWriter.d.ts +1 -1
  76. package/dist/redis/redisRulesWriter.d.ts.map +1 -1
  77. package/dist/redis/redisRulesWriter.js.map +1 -1
  78. package/dist/rule.d.ts +1 -0
  79. package/dist/rule.d.ts.map +1 -1
  80. package/dist/ruleInput/ruleInput.d.ts +6 -0
  81. package/dist/ruleInput/ruleInput.d.ts.map +1 -1
  82. package/dist/ruleInput/userScopeInput.d.ts +8 -0
  83. package/dist/ruleInput/userScopeInput.d.ts.map +1 -1
  84. package/dist/ruleInput/userScopeInput.js +2 -1
  85. package/dist/ruleInput/userScopeInput.js.map +1 -1
  86. package/dist/ruleRecord.d.ts +2 -2
  87. package/dist/ruleRecord.d.ts.map +1 -1
  88. package/dist/ruleRecord.js +2 -1
  89. package/dist/ruleRecord.js.map +1 -1
  90. package/dist/tests/insertRulesEndpoint.unit.test.d.ts +2 -0
  91. package/dist/tests/insertRulesEndpoint.unit.test.d.ts.map +1 -0
  92. package/dist/tests/insertRulesEndpoint.unit.test.js +57 -0
  93. package/dist/tests/insertRulesEndpoint.unit.test.js.map +1 -0
  94. package/dist/tests/redis/reader/redisRulesQuery.unit.test.js +42 -3
  95. package/dist/tests/redis/reader/redisRulesQuery.unit.test.js.map +1 -1
  96. package/dist/tests/redis/redisRulesStorage.integration.test.js +126 -1
  97. package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
  98. package/dist/tests/testLogger.d.ts +1 -1
  99. package/dist/tests/transformRule.unit.test.js +1 -0
  100. package/dist/tests/transformRule.unit.test.js.map +1 -1
  101. package/package.json +15 -10
  102. package/src/.export.ts +44 -0
  103. package/src/api/.export.ts +25 -0
  104. package/src/api/accessRulesApiClient.ts +13 -0
  105. package/src/api/delete/.export.ts +18 -0
  106. package/src/api/delete/deleteAllRules.ts +47 -0
  107. package/src/api/delete/deleteRuleGroups.ts +96 -0
  108. package/src/api/delete/deleteRules.ts +81 -0
  109. package/src/api/read/.export.ts +25 -0
  110. package/src/api/read/fetchRules.ts +88 -0
  111. package/src/api/read/findRuleIds.ts +95 -0
  112. package/src/api/read/getMissingIds.ts +81 -0
  113. package/src/api/ruleApiRoutes.ts +146 -0
  114. package/src/api/rulesApiClient.ts +154 -0
  115. package/src/api/write/.export.ts +15 -0
  116. package/src/api/write/insertRules.ts +183 -0
  117. package/src/api/write/rehashRules.ts +85 -0
  118. package/src/mongoose/.export.ts +15 -0
  119. package/src/mongoose/mongooseRuleSchema.ts +65 -0
  120. package/src/redis/.export.ts +17 -0
  121. package/src/redis/reader/redisAggregate.ts +103 -0
  122. package/src/redis/reader/redisRulesQuery.ts +217 -0
  123. package/src/redis/reader/redisRulesReader.ts +318 -0
  124. package/src/redis/redisClient.ts +120 -0
  125. package/src/redis/redisRuleIndex.ts +85 -0
  126. package/src/redis/redisRulesStorage.ts +68 -0
  127. package/src/redis/redisRulesWriter.ts +158 -0
  128. package/src/rule.ts +59 -0
  129. package/src/ruleInput/.export.ts +19 -0
  130. package/src/ruleInput/policyInput.ts +51 -0
  131. package/src/ruleInput/ruleInput.ts +103 -0
  132. package/src/ruleInput/userScopeInput.ts +108 -0
  133. package/src/ruleRecord.ts +69 -0
  134. package/src/rulesStorage.ts +72 -0
  135. package/src/tests/insertRulesEndpoint.unit.test.ts +89 -0
  136. package/src/tests/policyInput.unit.test.ts +150 -0
  137. package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +284 -0
  138. package/src/tests/redis/redisRulesStorage.integration.test.ts +1156 -0
  139. package/src/tests/testLogger.ts +38 -0
  140. package/src/tests/transformRule.unit.test.ts +255 -0
  141. package/src/transformRule.ts +128 -0
  142. package/tsconfig.cjs.json +41 -0
  143. package/tsconfig.json +47 -0
  144. package/tsconfig.tsbuildinfo +1 -0
  145. package/tsconfig.types.json +9 -0
@@ -0,0 +1,89 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { Logger } from "@prosopo/logger";
16
+ import { describe, expect, it, vi } from "vitest";
17
+ import { InsertRulesEndpoint } from "#policy/api/write/insertRules.js";
18
+ import { AccessPolicyType } from "#policy/rule.js";
19
+ import type { AccessRulesWriter } from "#policy/rulesStorage.js";
20
+
21
+ const makeMockLogger = (): Logger =>
22
+ ({
23
+ trace: vi.fn(),
24
+ debug: vi.fn(),
25
+ info: vi.fn(),
26
+ warn: vi.fn(),
27
+ error: vi.fn(),
28
+ fatal: vi.fn(),
29
+ log: vi.fn(),
30
+ setLogLevel: vi.fn(),
31
+ getLogLevel: vi.fn().mockReturnValue("info"),
32
+ with: vi.fn().mockReturnThis(),
33
+ getScope: vi.fn().mockReturnValue("test"),
34
+ getPretty: vi.fn().mockReturnValue(false),
35
+ setPretty: vi.fn(),
36
+ getPrintStack: vi.fn().mockReturnValue(false),
37
+ setPrintStack: vi.fn(),
38
+ getFormat: vi.fn().mockReturnValue("json"),
39
+ setFormat: vi.fn(),
40
+ }) satisfies Logger;
41
+
42
+ const makeWriter = (): AccessRulesWriter => ({
43
+ insertRules: vi.fn().mockResolvedValue(["rule-1", "rule-2"]),
44
+ deleteRules: vi.fn().mockResolvedValue(undefined),
45
+ deleteAllRules: vi.fn().mockResolvedValue(0),
46
+ });
47
+
48
+ const sampleArgs = [
49
+ {
50
+ accessPolicy: { type: AccessPolicyType.Block },
51
+ userScopes: [{ userId: "user-1" }, { userId: "user-2" }],
52
+ },
53
+ ];
54
+
55
+ describe("InsertRulesEndpoint logger selection", () => {
56
+ it("uses the per-request logger when one is provided to processRequest", async () => {
57
+ const ctorLogger = makeMockLogger();
58
+ const requestLogger = makeMockLogger();
59
+ const endpoint = new InsertRulesEndpoint(makeWriter(), ctorLogger);
60
+
61
+ // processRequest races against a 5s timeout and resolves PROCESSING first;
62
+ // the actual insert + log happens after. Wait long enough for the .then()
63
+ // chain to fire.
64
+ await endpoint.processRequest(sampleArgs, requestLogger);
65
+ await new Promise((r) => setImmediate(r));
66
+
67
+ expect(requestLogger.info).toHaveBeenCalled();
68
+ const calls = (requestLogger.info as ReturnType<typeof vi.fn>).mock.calls;
69
+ const msgs = calls.map(([fn]) => (fn as () => { msg: string })().msg);
70
+ expect(msgs).toContain("Endpoint inserted access rules");
71
+
72
+ // Constructor logger must not have been touched for the request-path log.
73
+ expect(ctorLogger.info).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it("falls back to the constructor logger when no request logger is provided", async () => {
77
+ const ctorLogger = makeMockLogger();
78
+ const endpoint = new InsertRulesEndpoint(makeWriter(), ctorLogger);
79
+
80
+ await endpoint.processRequest(sampleArgs);
81
+ await new Promise((r) => setImmediate(r));
82
+
83
+ expect(ctorLogger.info).toHaveBeenCalled();
84
+ const msgs = (ctorLogger.info as ReturnType<typeof vi.fn>).mock.calls.map(
85
+ ([fn]) => (fn as () => { msg: string })().msg,
86
+ );
87
+ expect(msgs).toContain("Endpoint inserted access rules");
88
+ });
89
+ });
@@ -0,0 +1,150 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { CaptchaType } from "@prosopo/types";
16
+ import { describe, expect, it } from "vitest";
17
+ import { AccessPolicyType } from "#policy/rule.js";
18
+ import { sanitizeAccessPolicy } from "#policy/ruleInput/policyInput.js";
19
+
20
+ describe("sanitizeAccessPolicy", () => {
21
+ describe("block policies", () => {
22
+ it("should remove captchaType from block policies", () => {
23
+ const input = {
24
+ type: AccessPolicyType.Block,
25
+ captchaType: CaptchaType.image,
26
+ description: "test block policy",
27
+ };
28
+
29
+ const result = sanitizeAccessPolicy(input);
30
+
31
+ expect(result).toEqual({
32
+ type: AccessPolicyType.Block,
33
+ description: "test block policy",
34
+ });
35
+ expect(result.captchaType).toBeUndefined();
36
+ });
37
+
38
+ it("should remove solvedImagesCount from block policies", () => {
39
+ const input = {
40
+ type: AccessPolicyType.Block,
41
+ solvedImagesCount: 5,
42
+ description: "test block policy",
43
+ };
44
+
45
+ const result = sanitizeAccessPolicy(input);
46
+
47
+ expect(result).toEqual({
48
+ type: AccessPolicyType.Block,
49
+ description: "test block policy",
50
+ });
51
+ expect(result.solvedImagesCount).toBeUndefined();
52
+ });
53
+
54
+ it("should remove both captchaType and solvedImagesCount from block policies", () => {
55
+ const input = {
56
+ type: AccessPolicyType.Block,
57
+ captchaType: CaptchaType.image,
58
+ solvedImagesCount: 5,
59
+ description: "test block policy",
60
+ };
61
+
62
+ const result = sanitizeAccessPolicy(input);
63
+
64
+ expect(result).toEqual({
65
+ type: AccessPolicyType.Block,
66
+ description: "test block policy",
67
+ });
68
+ expect(result.captchaType).toBeUndefined();
69
+ expect(result.solvedImagesCount).toBeUndefined();
70
+ });
71
+
72
+ it("should keep other fields in block policies", () => {
73
+ const input = {
74
+ type: AccessPolicyType.Block,
75
+ captchaType: CaptchaType.image,
76
+ solvedImagesCount: 5,
77
+ description: "test block policy",
78
+ imageThreshold: 0.5,
79
+ powDifficulty: 10,
80
+ unsolvedImagesCount: 3,
81
+ frictionlessScore: 100,
82
+ };
83
+
84
+ const result = sanitizeAccessPolicy(input);
85
+
86
+ expect(result).toEqual({
87
+ type: AccessPolicyType.Block,
88
+ description: "test block policy",
89
+ imageThreshold: 0.5,
90
+ powDifficulty: 10,
91
+ unsolvedImagesCount: 3,
92
+ frictionlessScore: 100,
93
+ });
94
+ expect(result.captchaType).toBeUndefined();
95
+ expect(result.solvedImagesCount).toBeUndefined();
96
+ });
97
+ });
98
+
99
+ describe("restrict policies", () => {
100
+ it("should keep captchaType in restrict policies", () => {
101
+ const input = {
102
+ type: AccessPolicyType.Restrict,
103
+ captchaType: CaptchaType.image,
104
+ description: "test restrict policy",
105
+ };
106
+
107
+ const result = sanitizeAccessPolicy(input);
108
+
109
+ expect(result).toEqual({
110
+ type: AccessPolicyType.Restrict,
111
+ captchaType: CaptchaType.image,
112
+ description: "test restrict policy",
113
+ });
114
+ });
115
+
116
+ it("should keep solvedImagesCount in restrict policies", () => {
117
+ const input = {
118
+ type: AccessPolicyType.Restrict,
119
+ solvedImagesCount: 5,
120
+ description: "test restrict policy",
121
+ };
122
+
123
+ const result = sanitizeAccessPolicy(input);
124
+
125
+ expect(result).toEqual({
126
+ type: AccessPolicyType.Restrict,
127
+ solvedImagesCount: 5,
128
+ description: "test restrict policy",
129
+ });
130
+ });
131
+
132
+ it("should keep both captchaType and solvedImagesCount in restrict policies", () => {
133
+ const input = {
134
+ type: AccessPolicyType.Restrict,
135
+ captchaType: CaptchaType.image,
136
+ solvedImagesCount: 5,
137
+ description: "test restrict policy",
138
+ };
139
+
140
+ const result = sanitizeAccessPolicy(input);
141
+
142
+ expect(result).toEqual({
143
+ type: AccessPolicyType.Restrict,
144
+ captchaType: CaptchaType.image,
145
+ solvedImagesCount: 5,
146
+ description: "test restrict policy",
147
+ });
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,284 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { describe, expect, it } from "vitest";
16
+ import { getRulesRedisQuery } from "#policy/redis/reader/redisRulesQuery.js";
17
+ import {
18
+ type AccessRulesFilter,
19
+ FilterScopeMatch,
20
+ } from "#policy/rulesStorage.js";
21
+
22
+ describe("getRulesRedisQuery", () => {
23
+ it("puts ismissing(x) for field x passed in as `undefined` when user scope match is exact", () => {
24
+ const filter = {
25
+ userScope: {
26
+ numericIp: BigInt(100),
27
+ ja4Hash: "ja4Hash",
28
+ userAgentHash: undefined,
29
+ },
30
+ userScopeMatch: FilterScopeMatch.Exact,
31
+ } as AccessRulesFilter;
32
+
33
+ const query = getRulesRedisQuery(filter, false);
34
+
35
+ expect(query).toBe(
36
+ "( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) )",
37
+ );
38
+ });
39
+
40
+ it("puts ismissing(x) for field x passed in as `undefined` when user scope match is exact and for missing fields when matchingFieldsOnly is set", () => {
41
+ const filter = {
42
+ userScope: {
43
+ numericIp: BigInt(100),
44
+ ja4Hash: "ja4Hash",
45
+ userAgentHash: undefined,
46
+ },
47
+ userScopeMatch: FilterScopeMatch.Exact,
48
+ } as AccessRulesFilter;
49
+
50
+ const query = getRulesRedisQuery(filter, true);
51
+
52
+ expect(query).toBe(
53
+ "( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@userId) ismissing(@headersHash) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) )",
54
+ );
55
+ });
56
+
57
+ it("puts ismissing(x) for multiple fields passed in as `undefined` when user scope match is exact", () => {
58
+ const filter = {
59
+ userScope: {
60
+ numericIp: BigInt(100),
61
+ ja4Hash: "ja4Hash",
62
+ userAgentHash: undefined,
63
+ headersHash: undefined,
64
+ userId: undefined,
65
+ },
66
+ userScopeMatch: FilterScopeMatch.Exact,
67
+ } as AccessRulesFilter;
68
+
69
+ const query = getRulesRedisQuery(filter, false);
70
+
71
+ expect(query).toBe(
72
+ "( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) )",
73
+ );
74
+ });
75
+
76
+ it("does not put ismissing(x) for multiple fields passed in as `undefined` when user scope match is greedy", () => {
77
+ const filter = {
78
+ userScope: {
79
+ numericIp: BigInt(100),
80
+ ja4Hash: "ja4Hash",
81
+ userAgentHash: undefined,
82
+ headersHash: undefined,
83
+ userId: undefined,
84
+ },
85
+ userScopeMatch: FilterScopeMatch.Greedy,
86
+ } as AccessRulesFilter;
87
+
88
+ const query = getRulesRedisQuery(filter, false);
89
+
90
+ expect(query).toBe(
91
+ "( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) | @ja4Hash:{ja4Hash} )",
92
+ );
93
+ });
94
+
95
+ it("puts ismissing(x) for multiple fields passed in as `undefined` when user scope match is exact 2", () => {
96
+ const filter = {
97
+ userScope: {
98
+ numericIp: undefined,
99
+ ja4Hash: "ja4Hash",
100
+ userAgentHash: undefined,
101
+ headersHash: undefined,
102
+ userId: undefined,
103
+ },
104
+ userScopeMatch: FilterScopeMatch.Exact,
105
+ } as AccessRulesFilter;
106
+
107
+ const query = getRulesRedisQuery(filter, false);
108
+
109
+ expect(query).toBe(
110
+ "( ismissing(@numericIp) ismissing(@numericIpMaskMin) ismissing(@numericIpMaskMax) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) )",
111
+ );
112
+ });
113
+
114
+ it("does not put ismissing(numericIpMaskMin) and does not put ismissing(numericIpMaskMax) when numericIp is passed in", () => {
115
+ const filter = {
116
+ userScope: {
117
+ numericIp: BigInt(100),
118
+ ja4Hash: "ja4Hash",
119
+ userAgentHash: undefined,
120
+ headersHash: undefined,
121
+ userId: undefined,
122
+ },
123
+ userScopeMatch: FilterScopeMatch.Exact,
124
+ } as AccessRulesFilter;
125
+
126
+ const query = getRulesRedisQuery(filter, true);
127
+
128
+ expect(query).toBe(
129
+ "( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) )",
130
+ );
131
+ });
132
+
133
+ it("does not put ismissing(numericIp) when numericIpMaskMin and numericIpMaskMax are passed in", () => {
134
+ const filter = {
135
+ userScope: {
136
+ numericIpMaskMin: BigInt(100),
137
+ numericIpMaskMax: BigInt(200),
138
+ ja4Hash: "ja4Hash",
139
+ userAgentHash: undefined,
140
+ headersHash: undefined,
141
+ userId: undefined,
142
+ },
143
+ userScopeMatch: FilterScopeMatch.Exact,
144
+ } as AccessRulesFilter;
145
+
146
+ const query = getRulesRedisQuery(filter, true);
147
+
148
+ expect(query).toBe(
149
+ "( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[200 +inf] @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) )",
150
+ );
151
+ });
152
+
153
+ it("does not duplicate ismissing for numericIpMaskMin and numericIpMaskMax when matchingFieldsOnly is true and all IP fields are undefined", () => {
154
+ const filter = {
155
+ userScope: {
156
+ userId: "user123",
157
+ },
158
+ userScopeMatch: FilterScopeMatch.Exact,
159
+ } as AccessRulesFilter;
160
+
161
+ const query = getRulesRedisQuery(filter, true);
162
+
163
+ // numericIp handler emits all three ismissing clauses when all IP fields are undefined.
164
+ // The individual numericIpMaskMin and numericIpMaskMax handlers should not duplicate them.
165
+ const numericIpMaskMinMatches = query.match(
166
+ /ismissing\(@numericIpMaskMin\)/g,
167
+ );
168
+ const numericIpMaskMaxMatches = query.match(
169
+ /ismissing\(@numericIpMaskMax\)/g,
170
+ );
171
+
172
+ expect(numericIpMaskMinMatches).toHaveLength(1);
173
+ expect(numericIpMaskMaxMatches).toHaveLength(1);
174
+ expect(query).toContain("ismissing(@numericIp)");
175
+ expect(query).toContain("@userId:{user123}");
176
+ });
177
+
178
+ it("emits ismissing for numericIpMaskMin when numericIpMaskMax is defined but numericIpMaskMin is not", () => {
179
+ const filter = {
180
+ userScope: {
181
+ numericIpMaskMax: BigInt(200),
182
+ },
183
+ userScopeMatch: FilterScopeMatch.Exact,
184
+ } as AccessRulesFilter;
185
+
186
+ const query = getRulesRedisQuery(filter, true);
187
+
188
+ // numericIp handler returns "" because numericIpMaskMax is defined.
189
+ // numericIpMaskMin handler should still emit ismissing since the numericIp handler didn't cover it.
190
+ expect(query).toContain("ismissing(@numericIpMaskMin)");
191
+ expect(query).toContain("@numericIpMaskMax:[200 +inf]");
192
+ expect(query).not.toContain("ismissing(@numericIpMaskMax)");
193
+ });
194
+
195
+ it("emits ismissing for numericIpMaskMax when numericIpMaskMin is defined but numericIpMaskMax is not", () => {
196
+ const filter = {
197
+ userScope: {
198
+ numericIpMaskMin: BigInt(100),
199
+ },
200
+ userScopeMatch: FilterScopeMatch.Exact,
201
+ } as AccessRulesFilter;
202
+
203
+ const query = getRulesRedisQuery(filter, true);
204
+
205
+ expect(query).toContain("@numericIpMaskMin:[-inf 100]");
206
+ expect(query).toContain("ismissing(@numericIpMaskMax)");
207
+ expect(query).not.toContain("ismissing(@numericIpMaskMin)");
208
+ });
209
+
210
+ it("includes headHash in query when provided", () => {
211
+ const filter = {
212
+ userScope: {
213
+ headHash: "abc123def456",
214
+ },
215
+ userScopeMatch: FilterScopeMatch.Exact,
216
+ } as AccessRulesFilter;
217
+
218
+ const query = getRulesRedisQuery(filter, false);
219
+
220
+ expect(query).toBe("( @headHash:{abc123def456} )");
221
+ });
222
+
223
+ it("includes coords in query when provided with escaped special characters", () => {
224
+ const filter = {
225
+ userScope: {
226
+ coords: "[[[100,200]]]",
227
+ },
228
+ userScopeMatch: FilterScopeMatch.Exact,
229
+ } as AccessRulesFilter;
230
+
231
+ const query = getRulesRedisQuery(filter, false);
232
+
233
+ // Special characters like [, ], and , should be escaped
234
+ expect(query).toContain("@coords:{\\[\\[\\[100\\,200\\]\\]\\]}");
235
+ });
236
+
237
+ it("includes both headHash and coords when both provided", () => {
238
+ const filter = {
239
+ userScope: {
240
+ headHash: "abc123def456",
241
+ coords: "[[[100,200]]]",
242
+ },
243
+ userScopeMatch: FilterScopeMatch.Exact,
244
+ } as AccessRulesFilter;
245
+
246
+ const query = getRulesRedisQuery(filter, false);
247
+
248
+ expect(query).toContain("@headHash:{abc123def456}");
249
+ expect(query).toContain("@coords:{\\[\\[\\[100\\,200\\]\\]\\]}");
250
+ });
251
+
252
+ it("puts ismissing(headHash) and ismissing(coords) when not provided and matchingFieldsOnly is true", () => {
253
+ const filter = {
254
+ userScope: {
255
+ ja4Hash: "ja4Hash",
256
+ },
257
+ userScopeMatch: FilterScopeMatch.Exact,
258
+ } as AccessRulesFilter;
259
+
260
+ const query = getRulesRedisQuery(filter, true);
261
+
262
+ expect(query).toContain("ismissing(@headHash)");
263
+ expect(query).toContain("ismissing(@coords)");
264
+ });
265
+
266
+ it("combines headHash with other fields correctly in exact match", () => {
267
+ const filter = {
268
+ userScope: {
269
+ numericIp: BigInt(100),
270
+ ja4Hash: "ja4Hash",
271
+ headHash: "abc123def456",
272
+ userAgentHash: undefined,
273
+ },
274
+ userScopeMatch: FilterScopeMatch.Exact,
275
+ } as AccessRulesFilter;
276
+
277
+ const query = getRulesRedisQuery(filter, false);
278
+
279
+ expect(query).toContain("@numericIp:[100 100]");
280
+ expect(query).toContain("@ja4Hash:{ja4Hash}");
281
+ expect(query).toContain("@headHash:{abc123def456}");
282
+ expect(query).toContain("ismissing(@userAgentHash)");
283
+ });
284
+ });