@prosopo/provider 5.0.5 → 5.1.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 (59) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +12 -11
  2. package/.turbo/turbo-build$colon$tsc.log +24 -24
  3. package/.turbo/turbo-build.log +13 -12
  4. package/CHANGELOG.md +37 -0
  5. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.d.ts.map +1 -1
  6. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js +12 -0
  7. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js.map +1 -1
  8. package/dist/api/captcha/getImageCaptchaChallenge.d.ts.map +1 -1
  9. package/dist/api/captcha/getImageCaptchaChallenge.js +13 -1
  10. package/dist/api/captcha/getImageCaptchaChallenge.js.map +1 -1
  11. package/dist/api/captcha/getPoWCaptchaChallenge.d.ts.map +1 -1
  12. package/dist/api/captcha/getPoWCaptchaChallenge.js +12 -1
  13. package/dist/api/captcha/getPoWCaptchaChallenge.js.map +1 -1
  14. package/dist/api/captcha/getPuzzleCaptchaChallenge.d.ts.map +1 -1
  15. package/dist/api/captcha/getPuzzleCaptchaChallenge.js +12 -1
  16. package/dist/api/captcha/getPuzzleCaptchaChallenge.js.map +1 -1
  17. package/dist/api/captcha/trafficFilterRequestTime.d.ts +38 -0
  18. package/dist/api/captcha/trafficFilterRequestTime.d.ts.map +1 -0
  19. package/dist/api/captcha/trafficFilterRequestTime.js +109 -0
  20. package/dist/api/captcha/trafficFilterRequestTime.js.map +1 -0
  21. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/handler.cjs +13 -0
  22. package/dist/cjs/api/captcha/getImageCaptchaChallenge.cjs +13 -1
  23. package/dist/cjs/api/captcha/getPoWCaptchaChallenge.cjs +12 -1
  24. package/dist/cjs/api/captcha/getPuzzleCaptchaChallenge.cjs +12 -1
  25. package/dist/cjs/api/captcha/trafficFilterRequestTime.cjs +111 -0
  26. package/dist/cjs/tasks/captchaManager.cjs +13 -6
  27. package/dist/cjs/tasks/dnsEvent/enrichDnsEvent.cjs +10 -8
  28. package/dist/cjs/tasks/puzzleCaptcha/puzzleTasks.cjs +2 -3
  29. package/dist/cjs/tasks/spam/checkTrafficFilter.cjs +101 -75
  30. package/dist/tasks/captchaManager.d.ts.map +1 -1
  31. package/dist/tasks/captchaManager.js +14 -7
  32. package/dist/tasks/captchaManager.js.map +1 -1
  33. package/dist/tasks/dnsEvent/enrichDnsEvent.d.ts.map +1 -1
  34. package/dist/tasks/dnsEvent/enrichDnsEvent.js +10 -8
  35. package/dist/tasks/dnsEvent/enrichDnsEvent.js.map +1 -1
  36. package/dist/tasks/puzzleCaptcha/puzzleTasks.d.ts.map +1 -1
  37. package/dist/tasks/puzzleCaptcha/puzzleTasks.js +2 -3
  38. package/dist/tasks/puzzleCaptcha/puzzleTasks.js.map +1 -1
  39. package/dist/tasks/spam/checkTrafficFilter.d.ts +18 -1
  40. package/dist/tasks/spam/checkTrafficFilter.d.ts.map +1 -1
  41. package/dist/tasks/spam/checkTrafficFilter.js +101 -77
  42. package/dist/tasks/spam/checkTrafficFilter.js.map +1 -1
  43. package/dist/tests/integration/clientSettingsPersistence.integration.test.js +24 -17
  44. package/dist/tests/integration/clientSettingsPersistence.integration.test.js.map +1 -1
  45. package/dist/tests/integration/trafficFilterHierarchy.integration.test.d.ts +2 -0
  46. package/dist/tests/integration/trafficFilterHierarchy.integration.test.d.ts.map +1 -0
  47. package/dist/tests/integration/trafficFilterHierarchy.integration.test.js +215 -0
  48. package/dist/tests/integration/trafficFilterHierarchy.integration.test.js.map +1 -0
  49. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.d.ts +2 -0
  50. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.d.ts.map +1 -0
  51. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.js +255 -0
  52. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.js.map +1 -0
  53. package/dist/tests/unit/tasks/dnsEvent/enrichDnsEvent.unit.test.js +6 -9
  54. package/dist/tests/unit/tasks/dnsEvent/enrichDnsEvent.unit.test.js.map +1 -1
  55. package/dist/tests/unit/tasks/puzzleCaptcha/puzzleTasks.unit.test.js +95 -0
  56. package/dist/tests/unit/tasks/puzzleCaptcha/puzzleTasks.unit.test.js.map +1 -1
  57. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js +272 -98
  58. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js.map +1 -1
  59. package/package.json +13 -13
@@ -1,5 +1,6 @@
1
+ import { CaptchaType, TrafficFilterAction, } from "@prosopo/types";
1
2
  import { describe, expect, it } from "vitest";
2
- import { checkTrafficFilter } from "../../../../tasks/spam/checkTrafficFilter.js";
3
+ import { checkTrafficFilter, resolveChallengePolicy, } from "../../../../tasks/spam/checkTrafficFilter.js";
3
4
  const baseInfo = (overrides = {}) => ({
4
5
  ip: "1.2.3.4",
5
6
  isValid: true,
@@ -13,38 +14,54 @@ const baseInfo = (overrides = {}) => ({
13
14
  isCrawler: false,
14
15
  ...overrides,
15
16
  });
17
+ const block = { action: TrafficFilterAction.Block };
16
18
  const allBlocked = {
17
- blockVpn: true,
18
- blockProxy: true,
19
- blockTor: true,
20
- blockAbuser: true,
19
+ vpn: block,
20
+ proxy: block,
21
+ tor: block,
22
+ abuser: block,
21
23
  abuserScoreThreshold: 0,
22
- blockDatacenter: true,
24
+ datacenter: block,
23
25
  skipExtrasOnValidDnsPath: false,
24
- blockMobile: true,
25
- blockSatellite: true,
26
- blockCrawler: true,
26
+ mobile: block,
27
+ satellite: block,
28
+ crawler: block,
27
29
  };
28
30
  describe("checkTrafficFilter", () => {
29
31
  it("blocks VPN when blockVpn is true", () => {
30
32
  const result = checkTrafficFilter(baseInfo({ isVPN: true }), allBlocked);
31
- expect(result).toEqual({ isBlocked: true, reason: "API.VPN_BLOCKED" });
33
+ expect(result).toMatchObject({
34
+ isBlocked: true,
35
+ reason: "API.VPN_BLOCKED",
36
+ });
32
37
  });
33
38
  it("blocks proxy when blockProxy is true", () => {
34
39
  const result = checkTrafficFilter(baseInfo({ isProxy: true }), allBlocked);
35
- expect(result).toEqual({ isBlocked: true, reason: "API.PROXY_BLOCKED" });
40
+ expect(result).toMatchObject({
41
+ isBlocked: true,
42
+ reason: "API.PROXY_BLOCKED",
43
+ });
36
44
  });
37
45
  it("blocks Tor when blockTor is true", () => {
38
46
  const result = checkTrafficFilter(baseInfo({ isTor: true }), allBlocked);
39
- expect(result).toEqual({ isBlocked: true, reason: "API.TOR_BLOCKED" });
47
+ expect(result).toMatchObject({
48
+ isBlocked: true,
49
+ reason: "API.TOR_BLOCKED",
50
+ });
40
51
  });
41
52
  it("blocks abusive ASN when blockAbuser is true", () => {
42
53
  const result = checkTrafficFilter(baseInfo({ isAbuser: true }), allBlocked);
43
- expect(result).toEqual({ isBlocked: true, reason: "API.ABUSER_BLOCKED" });
54
+ expect(result).toMatchObject({
55
+ isBlocked: true,
56
+ reason: "API.ABUSER_BLOCKED",
57
+ });
44
58
  });
45
59
  it("blocks abuser when score meets threshold", () => {
46
60
  const result = checkTrafficFilter(baseInfo({ isAbuser: true, abuserScore: 0.06, companyAbuserScore: 0.03 }), { ...allBlocked, abuserScoreThreshold: 0.05 });
47
- expect(result).toEqual({ isBlocked: true, reason: "API.ABUSER_BLOCKED" });
61
+ expect(result).toMatchObject({
62
+ isBlocked: true,
63
+ reason: "API.ABUSER_BLOCKED",
64
+ });
48
65
  });
49
66
  it("allows abuser when score is below threshold", () => {
50
67
  const result = checkTrafficFilter(baseInfo({
@@ -52,7 +69,7 @@ describe("checkTrafficFilter", () => {
52
69
  abuserScore: 0.0052,
53
70
  companyAbuserScore: 0.0273,
54
71
  }), { ...allBlocked, abuserScoreThreshold: 0.05 });
55
- expect(result).toEqual({ isBlocked: false });
72
+ expect(result).toMatchObject({ isBlocked: false });
56
73
  });
57
74
  it("uses company abuser score when it is higher than ASN score", () => {
58
75
  const result = checkTrafficFilter(baseInfo({
@@ -60,81 +77,93 @@ describe("checkTrafficFilter", () => {
60
77
  abuserScore: 0.01,
61
78
  companyAbuserScore: 0.08,
62
79
  }), { ...allBlocked, abuserScoreThreshold: 0.05 });
63
- expect(result).toEqual({ isBlocked: true, reason: "API.ABUSER_BLOCKED" });
80
+ expect(result).toMatchObject({
81
+ isBlocked: true,
82
+ reason: "API.ABUSER_BLOCKED",
83
+ });
64
84
  });
65
85
  it("blocks datacenter when blockDatacenter is true", () => {
66
86
  const result = checkTrafficFilter(baseInfo({ isDatacenter: true }), allBlocked);
67
- expect(result).toEqual({
87
+ expect(result).toMatchObject({
68
88
  isBlocked: true,
69
89
  reason: "API.DATACENTER_BLOCKED",
70
90
  });
71
91
  });
72
92
  it("does not block VPN-on-datacenter IPs when blockDatacenter is on but blockVpn is off", () => {
73
- const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isVPN: true }), { ...allBlocked, blockVpn: false });
74
- expect(result).toEqual({ isBlocked: false });
93
+ const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isVPN: true }), { ...allBlocked, vpn: undefined });
94
+ expect(result).toMatchObject({ isBlocked: false });
75
95
  });
76
96
  it("does not block proxy-on-datacenter IPs when blockDatacenter is on but blockProxy is off", () => {
77
- const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isProxy: true }), { ...allBlocked, blockProxy: false });
78
- expect(result).toEqual({ isBlocked: false });
97
+ const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isProxy: true }), { ...allBlocked, proxy: undefined });
98
+ expect(result).toMatchObject({ isBlocked: false });
79
99
  });
80
100
  it("does not block Tor-on-datacenter IPs when blockDatacenter is on but blockTor is off", () => {
81
- const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isTor: true }), { ...allBlocked, blockTor: false });
82
- expect(result).toEqual({ isBlocked: false });
101
+ const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isTor: true }), { ...allBlocked, tor: undefined });
102
+ expect(result).toMatchObject({ isBlocked: false });
83
103
  });
84
104
  it("does not block crawler-on-datacenter IPs when blockDatacenter is on but blockCrawler is off", () => {
85
- const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isCrawler: true }), { ...allBlocked, blockCrawler: false });
86
- expect(result).toEqual({ isBlocked: false });
105
+ const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isCrawler: true }), { ...allBlocked, crawler: undefined });
106
+ expect(result).toMatchObject({ isBlocked: false });
87
107
  });
88
108
  it("still blocks raw datacenter (non-VPN) IPs when blockDatacenter is on and blockVpn is off", () => {
89
- const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isVPN: false }), { ...allBlocked, blockVpn: false });
90
- expect(result).toEqual({
109
+ const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isVPN: false }), { ...allBlocked, vpn: undefined });
110
+ expect(result).toMatchObject({
91
111
  isBlocked: true,
92
112
  reason: "API.DATACENTER_BLOCKED",
93
113
  });
94
114
  });
95
115
  it("blocks VPN-on-datacenter IPs as VPN when both filters are enabled", () => {
96
116
  const result = checkTrafficFilter(baseInfo({ isDatacenter: true, isVPN: true }), allBlocked);
97
- expect(result).toEqual({ isBlocked: true, reason: "API.VPN_BLOCKED" });
117
+ expect(result).toMatchObject({
118
+ isBlocked: true,
119
+ reason: "API.VPN_BLOCKED",
120
+ });
98
121
  });
99
122
  it("blocks mobile when blockMobile is true", () => {
100
123
  const result = checkTrafficFilter(baseInfo({ isMobile: true }), allBlocked);
101
- expect(result).toEqual({ isBlocked: true, reason: "API.MOBILE_BLOCKED" });
124
+ expect(result).toMatchObject({
125
+ isBlocked: true,
126
+ reason: "API.MOBILE_BLOCKED",
127
+ });
102
128
  });
103
129
  it("blocks satellite when blockSatellite is true", () => {
104
130
  const result = checkTrafficFilter(baseInfo({ isSatellite: true }), allBlocked);
105
- expect(result).toEqual({
131
+ expect(result).toMatchObject({
106
132
  isBlocked: true,
107
133
  reason: "API.SATELLITE_BLOCKED",
108
134
  });
109
135
  });
110
136
  it("blocks crawler when blockCrawler is true", () => {
111
137
  const result = checkTrafficFilter(baseInfo({ isCrawler: true }), allBlocked);
112
- expect(result).toEqual({ isBlocked: true, reason: "API.CRAWLER_BLOCKED" });
138
+ expect(result).toMatchObject({
139
+ isBlocked: true,
140
+ reason: "API.CRAWLER_BLOCKED",
141
+ });
113
142
  });
114
143
  it("allows traffic when all filters are disabled", () => {
115
144
  const result = checkTrafficFilter(baseInfo({ isVPN: true }), {
116
- blockVpn: false,
117
- blockProxy: false,
118
- blockTor: false,
119
- blockAbuser: false,
120
- blockDatacenter: false,
121
- blockMobile: false,
122
- blockSatellite: false,
123
- blockCrawler: false,
124
- });
125
- expect(result).toEqual({ isBlocked: false });
145
+ vpn: undefined,
146
+ proxy: undefined,
147
+ tor: undefined,
148
+ abuser: undefined,
149
+ datacenter: undefined,
150
+ mobile: undefined,
151
+ satellite: undefined,
152
+ crawler: undefined,
153
+ });
154
+ expect(result).toMatchObject({ isBlocked: false });
126
155
  });
127
156
  it("allows residential IPs through", () => {
128
157
  const result = checkTrafficFilter(baseInfo(), allBlocked);
129
- expect(result).toEqual({ isBlocked: false });
158
+ expect(result).toMatchObject({ isBlocked: false });
130
159
  });
131
160
  it("allows through if IP info is missing entirely", () => {
132
161
  const result = checkTrafficFilter(undefined, allBlocked);
133
- expect(result).toEqual({ isBlocked: false });
162
+ expect(result).toMatchObject({ isBlocked: false });
134
163
  });
135
164
  it("allows through if IP info is invalid", () => {
136
165
  const result = checkTrafficFilter({ isValid: false, error: "lookup failed", ip: "1.2.3.4" }, allBlocked);
137
- expect(result).toEqual({ isBlocked: false });
166
+ expect(result).toMatchObject({ isBlocked: false });
138
167
  });
139
168
  describe("extraIpInfos", () => {
140
169
  const cleanPrimary = baseInfo({ ip: "192.0.2.1" });
@@ -142,7 +171,7 @@ describe("checkTrafficFilter", () => {
142
171
  const result = checkTrafficFilter(cleanPrimary, allBlocked, [
143
172
  baseInfo({ ip: "198.51.100.10", isDatacenter: true }),
144
173
  ]);
145
- expect(result).toEqual({
174
+ expect(result).toMatchObject({
146
175
  isBlocked: true,
147
176
  reason: "API.DATACENTER_BLOCKED",
148
177
  });
@@ -155,13 +184,13 @@ describe("checkTrafficFilter", () => {
155
184
  abuserScore: 95,
156
185
  }),
157
186
  ]);
158
- expect(result).toEqual({
187
+ expect(result).toMatchObject({
159
188
  isBlocked: true,
160
189
  reason: "API.ABUSER_BLOCKED",
161
190
  });
162
191
  });
163
192
  it("applies VPN-datacenter suppression to extra IPs when blockVpn is off", () => {
164
- const noVpnBlock = { ...allBlocked, blockVpn: false };
193
+ const noVpnBlock = { ...allBlocked, vpn: undefined };
165
194
  const result = checkTrafficFilter(cleanPrimary, noVpnBlock, [
166
195
  baseInfo({
167
196
  ip: "198.51.100.10",
@@ -169,10 +198,10 @@ describe("checkTrafficFilter", () => {
169
198
  isDatacenter: true,
170
199
  }),
171
200
  ]);
172
- expect(result).toEqual({ isBlocked: false });
201
+ expect(result).toMatchObject({ isBlocked: false });
173
202
  });
174
203
  it("still blocks a datacenter extra that is not a VPN when blockVpn is off", () => {
175
- const noVpnBlock = { ...allBlocked, blockVpn: false };
204
+ const noVpnBlock = { ...allBlocked, vpn: undefined };
176
205
  const result = checkTrafficFilter(cleanPrimary, noVpnBlock, [
177
206
  baseInfo({
178
207
  ip: "198.51.100.10",
@@ -180,22 +209,22 @@ describe("checkTrafficFilter", () => {
180
209
  isDatacenter: true,
181
210
  }),
182
211
  ]);
183
- expect(result).toEqual({
212
+ expect(result).toMatchObject({
184
213
  isBlocked: true,
185
214
  reason: "API.DATACENTER_BLOCKED",
186
215
  });
187
216
  });
188
217
  it("does apply VPN-datacenter suppression to the primary IP", () => {
189
- const noVpnBlock = { ...allBlocked, blockVpn: false };
218
+ const noVpnBlock = { ...allBlocked, vpn: undefined };
190
219
  const result = checkTrafficFilter(baseInfo({ isVPN: true, isDatacenter: true }), noVpnBlock);
191
- expect(result).toEqual({ isBlocked: false });
220
+ expect(result).toMatchObject({ isBlocked: false });
192
221
  });
193
222
  it("allows through when extra IPs are clean", () => {
194
223
  const result = checkTrafficFilter(cleanPrimary, allBlocked, [
195
224
  baseInfo({ ip: "8.8.8.8" }),
196
225
  baseInfo({ ip: "1.1.1.1" }),
197
226
  ]);
198
- expect(result).toEqual({ isBlocked: false });
227
+ expect(result).toMatchObject({ isBlocked: false });
199
228
  });
200
229
  it("ignores undefined / invalid entries in the extras list", () => {
201
230
  const result = checkTrafficFilter(cleanPrimary, allBlocked, [
@@ -203,7 +232,7 @@ describe("checkTrafficFilter", () => {
203
232
  { isValid: false, error: "lookup failed", ip: "1.2.3.4" },
204
233
  baseInfo({ ip: "198.51.100.10", isDatacenter: true }),
205
234
  ]);
206
- expect(result).toEqual({
235
+ expect(result).toMatchObject({
207
236
  isBlocked: true,
208
237
  reason: "API.DATACENTER_BLOCKED",
209
238
  });
@@ -212,23 +241,26 @@ describe("checkTrafficFilter", () => {
212
241
  const result = checkTrafficFilter(baseInfo({ isTor: true }), allBlocked, [
213
242
  baseInfo({ ip: "198.51.100.10", isDatacenter: true }),
214
243
  ]);
215
- expect(result).toEqual({ isBlocked: true, reason: "API.TOR_BLOCKED" });
244
+ expect(result).toMatchObject({
245
+ isBlocked: true,
246
+ reason: "API.TOR_BLOCKED",
247
+ });
216
248
  });
217
249
  it("does not check the crawler flag on extra IPs even when blockCrawler is on", () => {
218
250
  const result = checkTrafficFilter(cleanPrimary, allBlocked, [
219
251
  baseInfo({ ip: "8.8.8.8", isCrawler: true }),
220
252
  ]);
221
- expect(result).toEqual({ isBlocked: false });
253
+ expect(result).toMatchObject({ isBlocked: false });
222
254
  });
223
255
  it("still blocks the crawler flag on the primary IP when an extra is present", () => {
224
256
  const result = checkTrafficFilter(baseInfo({ isCrawler: true }), allBlocked, [baseInfo({ ip: "8.8.8.8" })]);
225
- expect(result).toEqual({
257
+ expect(result).toMatchObject({
226
258
  isBlocked: true,
227
259
  reason: "API.CRAWLER_BLOCKED",
228
260
  });
229
261
  });
230
262
  it("applies proxy-datacenter suppression to extra IPs when blockProxy is off", () => {
231
- const noProxyBlock = { ...allBlocked, blockProxy: false };
263
+ const noProxyBlock = { ...allBlocked, proxy: undefined };
232
264
  const result = checkTrafficFilter(cleanPrimary, noProxyBlock, [
233
265
  baseInfo({
234
266
  ip: "198.51.100.10",
@@ -236,10 +268,10 @@ describe("checkTrafficFilter", () => {
236
268
  isDatacenter: true,
237
269
  }),
238
270
  ]);
239
- expect(result).toEqual({ isBlocked: false });
271
+ expect(result).toMatchObject({ isBlocked: false });
240
272
  });
241
273
  it("applies Tor-datacenter suppression to extra IPs when blockTor is off", () => {
242
- const noTorBlock = { ...allBlocked, blockTor: false };
274
+ const noTorBlock = { ...allBlocked, tor: undefined };
243
275
  const result = checkTrafficFilter(cleanPrimary, noTorBlock, [
244
276
  baseInfo({
245
277
  ip: "198.51.100.10",
@@ -247,12 +279,12 @@ describe("checkTrafficFilter", () => {
247
279
  isDatacenter: true,
248
280
  }),
249
281
  ]);
250
- expect(result).toEqual({ isBlocked: false });
282
+ expect(result).toMatchObject({ isBlocked: false });
251
283
  });
252
284
  it("applies crawler-datacenter suppression to extra IPs when blockCrawler is off", () => {
253
285
  const noCrawlerBlock = {
254
286
  ...allBlocked,
255
- blockCrawler: false,
287
+ crawler: undefined,
256
288
  };
257
289
  const result = checkTrafficFilter(cleanPrimary, noCrawlerBlock, [
258
290
  baseInfo({
@@ -261,7 +293,7 @@ describe("checkTrafficFilter", () => {
261
293
  isDatacenter: true,
262
294
  }),
263
295
  ]);
264
- expect(result).toEqual({ isBlocked: false });
296
+ expect(result).toMatchObject({ isBlocked: false });
265
297
  });
266
298
  });
267
299
  describe("datacenterNameAllowlist", () => {
@@ -270,7 +302,7 @@ describe("checkTrafficFilter", () => {
270
302
  isDatacenter: true,
271
303
  datacenterName: "iCloud Private Relay",
272
304
  }), { ...allBlocked, datacenterNameAllowlist: ["iCloud Private Relay"] });
273
- expect(result).toEqual({ isBlocked: false });
305
+ expect(result).toMatchObject({ isBlocked: false });
274
306
  });
275
307
  it("matches the allowlist case-insensitively and ignores whitespace", () => {
276
308
  const result = checkTrafficFilter(baseInfo({
@@ -280,11 +312,11 @@ describe("checkTrafficFilter", () => {
280
312
  ...allBlocked,
281
313
  datacenterNameAllowlist: ["icloud private relay"],
282
314
  });
283
- expect(result).toEqual({ isBlocked: false });
315
+ expect(result).toMatchObject({ isBlocked: false });
284
316
  });
285
317
  it("still blocks datacenter IPs whose name does not match", () => {
286
318
  const result = checkTrafficFilter(baseInfo({ isDatacenter: true, datacenterName: "Amazon AWS" }), { ...allBlocked, datacenterNameAllowlist: ["iCloud Private Relay"] });
287
- expect(result).toEqual({
319
+ expect(result).toMatchObject({
288
320
  isBlocked: true,
289
321
  reason: "API.DATACENTER_BLOCKED",
290
322
  });
@@ -294,7 +326,7 @@ describe("checkTrafficFilter", () => {
294
326
  ...allBlocked,
295
327
  datacenterNameAllowlist: ["iCloud Private Relay"],
296
328
  });
297
- expect(result).toEqual({
329
+ expect(result).toMatchObject({
298
330
  isBlocked: true,
299
331
  reason: "API.DATACENTER_BLOCKED",
300
332
  });
@@ -304,7 +336,7 @@ describe("checkTrafficFilter", () => {
304
336
  isDatacenter: true,
305
337
  datacenterName: "iCloud Private Relay",
306
338
  }), allBlocked);
307
- expect(missing).toEqual({
339
+ expect(missing).toMatchObject({
308
340
  isBlocked: true,
309
341
  reason: "API.DATACENTER_BLOCKED",
310
342
  });
@@ -312,7 +344,7 @@ describe("checkTrafficFilter", () => {
312
344
  isDatacenter: true,
313
345
  datacenterName: "iCloud Private Relay",
314
346
  }), { ...allBlocked, datacenterNameAllowlist: [] });
315
- expect(empty).toEqual({
347
+ expect(empty).toMatchObject({
316
348
  isBlocked: true,
317
349
  reason: "API.DATACENTER_BLOCKED",
318
350
  });
@@ -323,7 +355,7 @@ describe("checkTrafficFilter", () => {
323
355
  isDatacenter: true,
324
356
  datacenterName: "iCloud Private Relay",
325
357
  }), { ...allBlocked, datacenterNameAllowlist: ["iCloud Private Relay"] });
326
- expect(torResult).toEqual({
358
+ expect(torResult).toMatchObject({
327
359
  isBlocked: true,
328
360
  reason: "API.TOR_BLOCKED",
329
361
  });
@@ -332,7 +364,7 @@ describe("checkTrafficFilter", () => {
332
364
  isDatacenter: true,
333
365
  datacenterName: "iCloud Private Relay",
334
366
  }), { ...allBlocked, datacenterNameAllowlist: ["iCloud Private Relay"] });
335
- expect(vpnResult).toEqual({
367
+ expect(vpnResult).toMatchObject({
336
368
  isBlocked: true,
337
369
  reason: "API.VPN_BLOCKED",
338
370
  });
@@ -345,28 +377,28 @@ describe("checkTrafficFilter", () => {
345
377
  datacenterName: "iCloud Private Relay",
346
378
  }),
347
379
  ]);
348
- expect(result).toEqual({ isBlocked: false });
380
+ expect(result).toMatchObject({ isBlocked: false });
349
381
  });
350
382
  it("matches the allowlist against providerName when datacenterName is absent", () => {
351
383
  const result = checkTrafficFilter(baseInfo({
352
384
  isDatacenter: true,
353
385
  providerName: "iCloud Private Relay",
354
386
  }), { ...allBlocked, datacenterNameAllowlist: ["iCloud Private Relay"] });
355
- expect(result).toEqual({ isBlocked: false });
387
+ expect(result).toMatchObject({ isBlocked: false });
356
388
  });
357
389
  it("matches the allowlist against asnOrganization when neither datacenterName nor providerName carries the operator", () => {
358
390
  const result = checkTrafficFilter(baseInfo({
359
391
  isDatacenter: true,
360
392
  asnOrganization: "Cloudflare, Inc.",
361
393
  }), { ...allBlocked, datacenterNameAllowlist: ["Cloudflare, Inc."] });
362
- expect(result).toEqual({ isBlocked: false });
394
+ expect(result).toMatchObject({ isBlocked: false });
363
395
  });
364
396
  it("still blocks datacenter IPs that carry none of the three name fields", () => {
365
397
  const result = checkTrafficFilter(baseInfo({ isDatacenter: true }), {
366
398
  ...allBlocked,
367
399
  datacenterNameAllowlist: ["iCloud Private Relay"],
368
400
  });
369
- expect(result).toEqual({
401
+ expect(result).toMatchObject({
370
402
  isBlocked: true,
371
403
  reason: "API.DATACENTER_BLOCKED",
372
404
  });
@@ -380,18 +412,18 @@ describe("checkTrafficFilter", () => {
380
412
  providerName: "afrihost.co.za",
381
413
  asnOrganization: "AFRIHOST SP (PTY) LTD",
382
414
  }), allBlocked);
383
- expect(result).toEqual({ isBlocked: false });
415
+ expect(result).toMatchObject({ isBlocked: false });
384
416
  });
385
417
  it("still blocks datacenter IPs when providerType is 'hosting'", () => {
386
418
  const result = checkTrafficFilter(baseInfo({ isDatacenter: true, providerType: "hosting" }), allBlocked);
387
- expect(result).toEqual({
419
+ expect(result).toMatchObject({
388
420
  isBlocked: true,
389
421
  reason: "API.DATACENTER_BLOCKED",
390
422
  });
391
423
  });
392
424
  it("still blocks datacenter IPs when providerType is missing", () => {
393
425
  const result = checkTrafficFilter(baseInfo({ isDatacenter: true }), allBlocked);
394
- expect(result).toEqual({
426
+ expect(result).toMatchObject({
395
427
  isBlocked: true,
396
428
  reason: "API.DATACENTER_BLOCKED",
397
429
  });
@@ -402,7 +434,7 @@ describe("checkTrafficFilter", () => {
402
434
  isDatacenter: true,
403
435
  providerType: "isp",
404
436
  }), allBlocked);
405
- expect(torResult).toEqual({
437
+ expect(torResult).toMatchObject({
406
438
  isBlocked: true,
407
439
  reason: "API.TOR_BLOCKED",
408
440
  });
@@ -411,7 +443,7 @@ describe("checkTrafficFilter", () => {
411
443
  isDatacenter: true,
412
444
  providerType: "isp",
413
445
  }), allBlocked);
414
- expect(vpnResult).toEqual({
446
+ expect(vpnResult).toMatchObject({
415
447
  isBlocked: true,
416
448
  reason: "API.VPN_BLOCKED",
417
449
  });
@@ -424,7 +456,7 @@ describe("checkTrafficFilter", () => {
424
456
  providerType: "isp",
425
457
  }),
426
458
  ]);
427
- expect(result).toEqual({ isBlocked: false });
459
+ expect(result).toMatchObject({ isBlocked: false });
428
460
  });
429
461
  });
430
462
  describe("datacenterNameDenylist", () => {
@@ -437,7 +469,7 @@ describe("checkTrafficFilter", () => {
437
469
  ...allBlocked,
438
470
  datacenterNameDenylist: ["proxy-lease-provider"],
439
471
  });
440
- expect(result).toEqual({
472
+ expect(result).toMatchObject({
441
473
  isBlocked: true,
442
474
  reason: "API.DATACENTER_BLOCKED",
443
475
  });
@@ -451,7 +483,7 @@ describe("checkTrafficFilter", () => {
451
483
  ...allBlocked,
452
484
  datacenterNameDenylist: ["proxy-lease-provider"],
453
485
  });
454
- expect(result).toEqual({
486
+ expect(result).toMatchObject({
455
487
  isBlocked: true,
456
488
  reason: "API.DATACENTER_BLOCKED",
457
489
  });
@@ -465,7 +497,7 @@ describe("checkTrafficFilter", () => {
465
497
  ...allBlocked,
466
498
  datacenterNameDenylist: ["proxy-lease-provider"],
467
499
  });
468
- expect(result).toEqual({
500
+ expect(result).toMatchObject({
469
501
  isBlocked: true,
470
502
  reason: "API.DATACENTER_BLOCKED",
471
503
  });
@@ -479,7 +511,7 @@ describe("checkTrafficFilter", () => {
479
511
  ...allBlocked,
480
512
  datacenterNameDenylist: ["Proxy Lease Ltd"],
481
513
  });
482
- expect(result).toEqual({
514
+ expect(result).toMatchObject({
483
515
  isBlocked: true,
484
516
  reason: "API.DATACENTER_BLOCKED",
485
517
  });
@@ -493,7 +525,7 @@ describe("checkTrafficFilter", () => {
493
525
  datacenterNameAllowlist: ["shared-name"],
494
526
  datacenterNameDenylist: ["shared-name"],
495
527
  });
496
- expect(result).toEqual({
528
+ expect(result).toMatchObject({
497
529
  isBlocked: true,
498
530
  reason: "API.DATACENTER_BLOCKED",
499
531
  });
@@ -506,7 +538,7 @@ describe("checkTrafficFilter", () => {
506
538
  ...allBlocked,
507
539
  datacenterNameDenylist: ["proxy-lease-provider"],
508
540
  });
509
- expect(result).toEqual({ isBlocked: false });
541
+ expect(result).toMatchObject({ isBlocked: false });
510
542
  });
511
543
  it("does not fire when blockDatacenter is off", () => {
512
544
  const result = checkTrafficFilter(baseInfo({
@@ -515,10 +547,10 @@ describe("checkTrafficFilter", () => {
515
547
  datacenterName: "proxy-lease-provider",
516
548
  }), {
517
549
  ...allBlocked,
518
- blockDatacenter: false,
550
+ datacenter: undefined,
519
551
  datacenterNameDenylist: ["proxy-lease-provider"],
520
552
  });
521
- expect(result).toEqual({ isBlocked: false });
553
+ expect(result).toMatchObject({ isBlocked: false });
522
554
  });
523
555
  it("preserves legacy behaviour when denylist is missing or empty", () => {
524
556
  const missing = checkTrafficFilter(baseInfo({
@@ -526,13 +558,13 @@ describe("checkTrafficFilter", () => {
526
558
  providerType: "isp",
527
559
  datacenterName: "some-isp",
528
560
  }), allBlocked);
529
- expect(missing).toEqual({ isBlocked: false });
561
+ expect(missing).toMatchObject({ isBlocked: false });
530
562
  const empty = checkTrafficFilter(baseInfo({
531
563
  isDatacenter: true,
532
564
  providerType: "isp",
533
565
  datacenterName: "some-isp",
534
566
  }), { ...allBlocked, datacenterNameDenylist: [] });
535
- expect(empty).toEqual({ isBlocked: false });
567
+ expect(empty).toMatchObject({ isBlocked: false });
536
568
  });
537
569
  it("still lets earlier category rules fire before the datacenter check", () => {
538
570
  const result = checkTrafficFilter(baseInfo({
@@ -544,7 +576,7 @@ describe("checkTrafficFilter", () => {
544
576
  ...allBlocked,
545
577
  datacenterNameDenylist: ["proxy-lease-provider"],
546
578
  });
547
- expect(result).toEqual({
579
+ expect(result).toMatchObject({
548
580
  isBlocked: true,
549
581
  reason: "API.TOR_BLOCKED",
550
582
  });
@@ -561,7 +593,7 @@ describe("checkTrafficFilter", () => {
561
593
  datacenterName: "proxy-lease-provider",
562
594
  }),
563
595
  ]);
564
- expect(result).toEqual({
596
+ expect(result).toMatchObject({
565
597
  isBlocked: true,
566
598
  reason: "API.DATACENTER_BLOCKED",
567
599
  });
@@ -576,7 +608,7 @@ describe("checkTrafficFilter", () => {
576
608
  asnOrganization: "Cloudflare, Inc.",
577
609
  }),
578
610
  ], true);
579
- expect(result).toEqual({ isBlocked: false });
611
+ expect(result).toMatchObject({ isBlocked: false });
580
612
  });
581
613
  it("still evaluates extras when the setting is off, even if pathValid is true", () => {
582
614
  const result = checkTrafficFilter(baseInfo({ ip: "192.0.2.1" }), { ...allBlocked, skipExtrasOnValidDnsPath: false }, [
@@ -586,7 +618,7 @@ describe("checkTrafficFilter", () => {
586
618
  asnOrganization: "Cloudflare, Inc.",
587
619
  }),
588
620
  ], true);
589
- expect(result).toEqual({
621
+ expect(result).toMatchObject({
590
622
  isBlocked: true,
591
623
  reason: "API.DATACENTER_BLOCKED",
592
624
  });
@@ -599,7 +631,7 @@ describe("checkTrafficFilter", () => {
599
631
  asnOrganization: "Cloudflare, Inc.",
600
632
  }),
601
633
  ], false);
602
- expect(result).toEqual({
634
+ expect(result).toMatchObject({
603
635
  isBlocked: true,
604
636
  reason: "API.DATACENTER_BLOCKED",
605
637
  });
@@ -612,7 +644,7 @@ describe("checkTrafficFilter", () => {
612
644
  asnOrganization: "Cloudflare, Inc.",
613
645
  }),
614
646
  ], undefined);
615
- expect(result).toEqual({
647
+ expect(result).toMatchObject({
616
648
  isBlocked: true,
617
649
  reason: "API.DATACENTER_BLOCKED",
618
650
  });
@@ -622,11 +654,153 @@ describe("checkTrafficFilter", () => {
622
654
  isDatacenter: true,
623
655
  datacenterName: "Amazon AWS",
624
656
  }), { ...allBlocked, skipExtrasOnValidDnsPath: true }, [], true);
625
- expect(result).toEqual({
657
+ expect(result).toMatchObject({
626
658
  isBlocked: true,
627
659
  reason: "API.DATACENTER_BLOCKED",
628
660
  });
629
661
  });
630
662
  });
631
663
  });
664
+ describe("resolveChallengePolicy", () => {
665
+ const challenge = (category, policy) => ({ category, policy });
666
+ it("returns undefined when there are no challenge matches", () => {
667
+ expect(resolveChallengePolicy([])).toBeUndefined();
668
+ expect(resolveChallengePolicy([
669
+ challenge("vpn", { action: TrafficFilterAction.Block }),
670
+ ])).toBeUndefined();
671
+ });
672
+ it("returns the sole challenge policy verbatim when only one matches", () => {
673
+ const resolved = resolveChallengePolicy([
674
+ challenge("vpn", {
675
+ action: TrafficFilterAction.Challenge,
676
+ captchaType: CaptchaType.pow,
677
+ powDifficulty: 7,
678
+ }),
679
+ ]);
680
+ expect(resolved).toEqual({
681
+ captchaType: CaptchaType.pow,
682
+ powDifficulty: 7,
683
+ solvedImagesCount: undefined,
684
+ puzzleTolerance: undefined,
685
+ sourceCategories: ["vpn"],
686
+ });
687
+ });
688
+ it("picks image over puzzle over pow when multiple categories name a captchaType", () => {
689
+ const resolved = resolveChallengePolicy([
690
+ challenge("vpn", {
691
+ action: TrafficFilterAction.Challenge,
692
+ captchaType: CaptchaType.pow,
693
+ }),
694
+ challenge("proxy", {
695
+ action: TrafficFilterAction.Challenge,
696
+ captchaType: CaptchaType.puzzle,
697
+ }),
698
+ challenge("datacenter", {
699
+ action: TrafficFilterAction.Challenge,
700
+ captchaType: CaptchaType.image,
701
+ }),
702
+ ]);
703
+ expect(resolved?.captchaType).toBe(CaptchaType.image);
704
+ });
705
+ it("picks puzzle over pow when no image challenge is present", () => {
706
+ const resolved = resolveChallengePolicy([
707
+ challenge("vpn", {
708
+ action: TrafficFilterAction.Challenge,
709
+ captchaType: CaptchaType.pow,
710
+ }),
711
+ challenge("proxy", {
712
+ action: TrafficFilterAction.Challenge,
713
+ captchaType: CaptchaType.puzzle,
714
+ }),
715
+ ]);
716
+ expect(resolved?.captchaType).toBe(CaptchaType.puzzle);
717
+ });
718
+ it("takes the highest pow difficulty across challenge matches", () => {
719
+ const resolved = resolveChallengePolicy([
720
+ challenge("vpn", {
721
+ action: TrafficFilterAction.Challenge,
722
+ captchaType: CaptchaType.pow,
723
+ powDifficulty: 4,
724
+ }),
725
+ challenge("proxy", {
726
+ action: TrafficFilterAction.Challenge,
727
+ captchaType: CaptchaType.pow,
728
+ powDifficulty: 9,
729
+ }),
730
+ challenge("datacenter", {
731
+ action: TrafficFilterAction.Challenge,
732
+ captchaType: CaptchaType.pow,
733
+ powDifficulty: 6,
734
+ }),
735
+ ]);
736
+ expect(resolved?.powDifficulty).toBe(9);
737
+ });
738
+ it("takes the highest image count across challenge matches", () => {
739
+ const resolved = resolveChallengePolicy([
740
+ challenge("vpn", {
741
+ action: TrafficFilterAction.Challenge,
742
+ captchaType: CaptchaType.image,
743
+ solvedImagesCount: 3,
744
+ }),
745
+ challenge("datacenter", {
746
+ action: TrafficFilterAction.Challenge,
747
+ captchaType: CaptchaType.image,
748
+ solvedImagesCount: 8,
749
+ }),
750
+ ]);
751
+ expect(resolved?.solvedImagesCount).toBe(8);
752
+ });
753
+ it("takes the lowest puzzle tolerance across challenge matches (stricter accuracy)", () => {
754
+ const resolved = resolveChallengePolicy([
755
+ challenge("vpn", {
756
+ action: TrafficFilterAction.Challenge,
757
+ captchaType: CaptchaType.puzzle,
758
+ puzzleTolerance: 40,
759
+ }),
760
+ challenge("datacenter", {
761
+ action: TrafficFilterAction.Challenge,
762
+ captchaType: CaptchaType.puzzle,
763
+ puzzleTolerance: 15,
764
+ }),
765
+ ]);
766
+ expect(resolved?.puzzleTolerance).toBe(15);
767
+ });
768
+ it("ignores block matches when combining challenge params", () => {
769
+ const resolved = resolveChallengePolicy([
770
+ challenge("vpn", { action: TrafficFilterAction.Block }),
771
+ challenge("proxy", {
772
+ action: TrafficFilterAction.Challenge,
773
+ captchaType: CaptchaType.pow,
774
+ powDifficulty: 5,
775
+ }),
776
+ ]);
777
+ expect(resolved?.sourceCategories).toEqual(["proxy"]);
778
+ expect(resolved?.powDifficulty).toBe(5);
779
+ });
780
+ it("leaves fields undefined when no matched policy sets them", () => {
781
+ const resolved = resolveChallengePolicy([
782
+ challenge("vpn", {
783
+ action: TrafficFilterAction.Challenge,
784
+ captchaType: CaptchaType.image,
785
+ }),
786
+ ]);
787
+ expect(resolved).toEqual({
788
+ captchaType: CaptchaType.image,
789
+ powDifficulty: undefined,
790
+ solvedImagesCount: undefined,
791
+ puzzleTolerance: undefined,
792
+ sourceCategories: ["vpn"],
793
+ });
794
+ });
795
+ it("keeps captchaType undefined when no matched policy specifies one (falls through to site default)", () => {
796
+ const resolved = resolveChallengePolicy([
797
+ challenge("vpn", {
798
+ action: TrafficFilterAction.Challenge,
799
+ powDifficulty: 7,
800
+ }),
801
+ ]);
802
+ expect(resolved?.captchaType).toBeUndefined();
803
+ expect(resolved?.powDifficulty).toBe(7);
804
+ });
805
+ });
632
806
  //# sourceMappingURL=checkTrafficFilter.unit.test.js.map