@prosopo/provider 3.2.5 → 3.12.3

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 (52) hide show
  1. package/CHANGELOG.md +578 -0
  2. package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
  3. package/dist/api/blacklistRequestInspector.js +26 -20
  4. package/dist/api/captcha.js +121 -33
  5. package/dist/api/domainMiddleware.js +8 -8
  6. package/dist/api/headerCheckMiddleware.js +4 -0
  7. package/dist/api/ignoreMiddleware.js +4 -1
  8. package/dist/api/ja4Middleware.js +5 -23
  9. package/dist/api/public.js +26 -3
  10. package/dist/api/verify.js +4 -2
  11. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +6 -3
  12. package/dist/cjs/api/blacklistRequestInspector.cjs +25 -19
  13. package/dist/cjs/api/captcha.cjs +121 -33
  14. package/dist/cjs/api/domainMiddleware.cjs +8 -8
  15. package/dist/cjs/api/headerCheckMiddleware.cjs +4 -0
  16. package/dist/cjs/api/ignoreMiddleware.cjs +3 -0
  17. package/dist/cjs/api/ja4Middleware.cjs +4 -22
  18. package/dist/cjs/api/public.cjs +26 -3
  19. package/dist/cjs/api/verify.cjs +4 -2
  20. package/dist/cjs/compositeIpAddress.cjs +53 -0
  21. package/dist/cjs/index.cjs +7 -0
  22. package/dist/cjs/pairs.cjs +27 -0
  23. package/dist/cjs/services/ipComparison.cjs +123 -0
  24. package/dist/cjs/services/ipInfo.cjs +87 -0
  25. package/dist/cjs/tasks/captchaManager.cjs +49 -2
  26. package/dist/cjs/tasks/client/clientTasks.cjs +33 -12
  27. package/dist/cjs/tasks/detection/decodePayload.cjs +712 -278
  28. package/dist/cjs/tasks/detection/getBotScore.cjs +14 -3
  29. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +128 -24
  30. package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +17 -0
  31. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +62 -20
  32. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +43 -15
  33. package/dist/cjs/util.cjs +248 -16
  34. package/dist/cjs/utils/hashUserAgent.cjs +10 -0
  35. package/dist/compositeIpAddress.js +53 -0
  36. package/dist/index.js +8 -1
  37. package/dist/pairs.js +27 -0
  38. package/dist/services/ipComparison.js +123 -0
  39. package/dist/services/ipInfo.js +87 -0
  40. package/dist/tasks/captchaManager.js +49 -2
  41. package/dist/tasks/client/clientTasks.js +33 -12
  42. package/dist/tasks/detection/decodePayload.js +712 -278
  43. package/dist/tasks/detection/getBotScore.js +15 -4
  44. package/dist/tasks/frictionless/frictionlessTasks.js +128 -24
  45. package/dist/tasks/frictionless/frictionlessTasksUtils.js +18 -1
  46. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +64 -22
  47. package/dist/tasks/powCaptcha/powTasks.js +44 -16
  48. package/dist/util.js +249 -17
  49. package/dist/utils/hashUserAgent.js +10 -0
  50. package/package.json +28 -25
  51. package/vite.test.config.ts +3 -2
  52. package/vite.threads.test.config.ts +33 -0
package/dist/util.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { hexToU8a } from "@polkadot/util/hex";
2
2
  import { isHex } from "@polkadot/util/is";
3
3
  import { ProsopoContractError, ProsopoEnvError } from "@prosopo/common";
4
- import { ScheduledTaskStatus } from "@prosopo/types";
4
+ import { ScheduledTaskStatus, IPValidationAction } from "@prosopo/types";
5
5
  import { at } from "@prosopo/util";
6
6
  import { encodeAddress, decodeAddress } from "@prosopo/util-crypto";
7
7
  import { Address4, Address6 } from "ip-address";
8
+ import { compareIPs } from "./services/ipComparison.js";
8
9
  function encodeStringAddress(address) {
9
10
  try {
10
11
  return encodeAddress(
@@ -61,7 +62,7 @@ const getIPAddressFromBigInt = (ipAddressBigInt) => {
61
62
  throw new ProsopoEnvError("API.INVALID_IP");
62
63
  }
63
64
  };
64
- const validateIpAddress = (ip, challengeRecordIpAddress, logger) => {
65
+ const validateIpAddress = (ip, challengeIpAddress, logger) => {
65
66
  if (!ip) {
66
67
  return { isValid: true };
67
68
  }
@@ -74,27 +75,19 @@ const validateIpAddress = (ip, challengeRecordIpAddress, logger) => {
74
75
  logger.info(() => ({ msg: errorMessage }));
75
76
  return { isValid: false, errorMessage };
76
77
  }
77
- let challengeIpV4orV6Address = getIPAddressFromBigInt(
78
- challengeRecordIpAddress
79
- );
80
78
  ipV4orV6Address = "address4" in ipV4orV6Address && ipV4orV6Address.address4 ? ipV4orV6Address.address4 : ipV4orV6Address;
81
- challengeIpV4orV6Address = "address4" in challengeIpV4orV6Address && challengeIpV4orV6Address.address4 ? challengeIpV4orV6Address.address4 : challengeIpV4orV6Address;
82
- if (ipV4orV6Address.v4 && !challengeIpV4orV6Address.v4) {
83
- challengeIpV4orV6Address = new Address4(
84
- challengeIpV4orV6Address.to4().correctForm()
85
- );
86
- }
87
- if (!ipV4orV6Address.v4 && challengeIpV4orV6Address.v4) {
88
- ipV4orV6Address = new Address6(
89
- ipV4orV6Address.to4().correctForm()
79
+ challengeIpAddress = "address4" in challengeIpAddress && challengeIpAddress.address4 ? challengeIpAddress.address4 : challengeIpAddress;
80
+ if (ipV4orV6Address.v4 && !challengeIpAddress.v4) {
81
+ challengeIpAddress = new Address4(
82
+ challengeIpAddress.to4().correctForm()
90
83
  );
91
84
  }
92
- if (challengeIpV4orV6Address.bigInt() - ipV4orV6Address.bigInt() !== 0n) {
93
- const errorMessage = `IP address mismatch: ${challengeIpV4orV6Address.address} !== ${ipV4orV6Address.address}`;
85
+ if (challengeIpAddress.bigInt() - ipV4orV6Address.bigInt() !== 0n) {
86
+ const errorMessage = `IP address mismatch: ${challengeIpAddress.address} !== ${ipV4orV6Address.address}`;
94
87
  logger.info(() => ({
95
88
  msg: errorMessage,
96
89
  data: {
97
- challengeIp: challengeIpV4orV6Address.address,
90
+ challengeIp: challengeIpAddress.address,
98
91
  providedIp: ipV4orV6Address.address
99
92
  }
100
93
  }));
@@ -102,9 +95,248 @@ const validateIpAddress = (ip, challengeRecordIpAddress, logger) => {
102
95
  }
103
96
  return { isValid: true };
104
97
  };
98
+ const evaluateIpValidationRules = (comparison, rules, logger) => {
99
+ if (!comparison.comparison) {
100
+ return { action: IPValidationAction.Allow };
101
+ }
102
+ const conditions = [];
103
+ const rejectActions = Object.values(rules.actions).filter(
104
+ (action) => action === IPValidationAction.Reject
105
+ );
106
+ const ip1Country = comparison.comparison.ip1Details?.country;
107
+ const ip2Country = comparison.comparison.ip2Details?.country;
108
+ const ip1CountryCode = comparison.comparison.ip1Details?.countryCode;
109
+ const ip2CountryCode = comparison.comparison.ip2Details?.countryCode;
110
+ let effectiveRules = rules;
111
+ let countryOverride = void 0;
112
+ if (ip1CountryCode && rules.countryOverrides?.[ip1CountryCode]) {
113
+ countryOverride = rules.countryOverrides[ip1CountryCode];
114
+ }
115
+ if (ip2CountryCode && rules.countryOverrides?.[ip2CountryCode]) {
116
+ countryOverride = rules.countryOverrides[ip2CountryCode];
117
+ }
118
+ if (countryOverride) {
119
+ effectiveRules = {
120
+ ...rules,
121
+ actions: {
122
+ ...rules.actions,
123
+ ...countryOverride.actions
124
+ },
125
+ distanceThresholdKm: countryOverride.distanceThresholdKm ?? rules.distanceThresholdKm,
126
+ abuseScoreThreshold: countryOverride.abuseScoreThreshold ?? rules.abuseScoreThreshold,
127
+ requireAllConditions: countryOverride.requireAllConditions ?? rules.requireAllConditions
128
+ };
129
+ }
130
+ if (ip1Country !== ip2Country) {
131
+ conditions.push({
132
+ met: true,
133
+ action: effectiveRules.actions.countryChangeAction,
134
+ message: `Country changed from ${ip1Country} to ${ip2Country}`
135
+ });
136
+ }
137
+ const ip1City = comparison.comparison.ip1Details?.city;
138
+ const ip2City = comparison.comparison.ip2Details?.city;
139
+ if (ip1City !== ip2City) {
140
+ conditions.push({
141
+ met: true,
142
+ action: effectiveRules.actions.cityChangeAction,
143
+ message: `City changed from ${ip1City} to ${ip2City}`
144
+ });
145
+ }
146
+ if (comparison.comparison.differentProviders) {
147
+ const ip1Provider = comparison.comparison.ip1Details?.provider;
148
+ const ip2Provider = comparison.comparison.ip2Details?.provider;
149
+ conditions.push({
150
+ met: true,
151
+ action: effectiveRules.actions.ispChangeAction,
152
+ message: `ISP changed from ${ip1Provider} to ${ip2Provider}`
153
+ });
154
+ }
155
+ const distanceKm = comparison.comparison.distanceKm;
156
+ if (distanceKm !== void 0 && distanceKm > effectiveRules.distanceThresholdKm) {
157
+ conditions.push({
158
+ met: true,
159
+ action: effectiveRules.actions.distanceExceedAction,
160
+ message: `IP addresses are ${distanceKm.toFixed(2)}km apart (>${effectiveRules.distanceThresholdKm}km limit)`
161
+ });
162
+ }
163
+ console.log(JSON.stringify(effectiveRules, null, 2));
164
+ const ip2AbuseScore = comparison.comparison.ip2Details?.abuserScore;
165
+ if (ip2AbuseScore !== void 0 && ip2AbuseScore > effectiveRules.abuseScoreThreshold) {
166
+ conditions.push({
167
+ met: true,
168
+ action: effectiveRules.actions.abuseScoreExceedAction,
169
+ message: `Abuse score ${ip2AbuseScore.toFixed(4)} exceeds threshold ${effectiveRules.abuseScoreThreshold}`
170
+ });
171
+ }
172
+ const ip1AbuseScore = comparison.comparison.ip1Details?.abuserScore;
173
+ if (ip1AbuseScore !== void 0 && ip1AbuseScore > effectiveRules.abuseScoreThreshold) {
174
+ conditions.push({
175
+ met: true,
176
+ action: effectiveRules.actions.abuseScoreExceedAction,
177
+ message: `Abuse score ${ip1AbuseScore.toFixed(4)} exceeds threshold ${effectiveRules.abuseScoreThreshold}`
178
+ });
179
+ }
180
+ if (conditions.length === 0) {
181
+ return { action: IPValidationAction.Allow };
182
+ }
183
+ const errorMessages = [];
184
+ let finalAction = IPValidationAction.Allow;
185
+ let shouldFlag = false;
186
+ if (effectiveRules.requireAllConditions) {
187
+ const rejectConditions = conditions.filter(
188
+ (c) => c.action === IPValidationAction.Reject
189
+ );
190
+ if (rejectConditions.length > 0 && rejectConditions.length >= rejectActions.length) {
191
+ finalAction = IPValidationAction.Reject;
192
+ }
193
+ for (const condition of conditions) {
194
+ if (condition.action === IPValidationAction.Reject || condition.action === IPValidationAction.Flag) {
195
+ errorMessages.push(condition.message);
196
+ }
197
+ }
198
+ } else {
199
+ for (const condition of conditions) {
200
+ if (condition.action === IPValidationAction.Reject) {
201
+ finalAction = IPValidationAction.Reject;
202
+ errorMessages.push(condition.message);
203
+ break;
204
+ }
205
+ if (condition.action === IPValidationAction.Flag) {
206
+ finalAction = IPValidationAction.Flag;
207
+ shouldFlag = true;
208
+ }
209
+ errorMessages.push(condition.message);
210
+ }
211
+ }
212
+ logger.info(() => ({
213
+ msg: `IP validation rules evaluated: ${finalAction}`,
214
+ data: {
215
+ conditions,
216
+ finalAction,
217
+ requireAllConditions: effectiveRules.requireAllConditions
218
+ }
219
+ }));
220
+ return {
221
+ action: finalAction,
222
+ errorMessage: errorMessages.length > 0 ? errorMessages.join("; ") : void 0,
223
+ shouldFlag
224
+ };
225
+ };
226
+ const deepValidateIpAddress = async (ip, challengeIpAddress, logger, apiKey, apiUrl, ipValidationRules) => {
227
+ const standardValidation = validateIpAddress(ip, challengeIpAddress, logger);
228
+ if (!standardValidation.isValid) {
229
+ if (standardValidation.errorMessage?.includes("Invalid IP address")) {
230
+ return standardValidation;
231
+ }
232
+ } else {
233
+ return { isValid: true };
234
+ }
235
+ try {
236
+ const challengeIpString = challengeIpAddress.address;
237
+ const comparison = await compareIPs(challengeIpString, ip, apiKey, apiUrl);
238
+ if ("error" in comparison) {
239
+ logger.error(() => ({
240
+ msg: "Failed to get IP distance comparison",
241
+ data: {
242
+ error: comparison.error,
243
+ challengeIp: challengeIpString,
244
+ providedIp: ip
245
+ }
246
+ }));
247
+ return {
248
+ isValid: false,
249
+ errorMessage: "Could not determine IP distance"
250
+ };
251
+ }
252
+ if (comparison.ipsMatch) {
253
+ return { isValid: true };
254
+ }
255
+ const distanceKm = comparison.comparison?.distanceKm;
256
+ if (!ipValidationRules) {
257
+ logger.info(() => ({
258
+ msg: "No IP validation rules provided, using legacy logic",
259
+ data: {
260
+ challengeIp: challengeIpString,
261
+ providedIp: ip,
262
+ distanceKm
263
+ }
264
+ }));
265
+ if (distanceKm !== void 0 && distanceKm > 1e3) {
266
+ const errorMessage = `IP addresses are too far apart: ${distanceKm.toFixed(2)}km (>1000km limit)`;
267
+ logger.info(() => ({
268
+ msg: "IP validation failed - distance too great",
269
+ data: {
270
+ challengeIp: challengeIpString,
271
+ providedIp: ip,
272
+ distanceKm,
273
+ comparison: comparison.comparison
274
+ }
275
+ }));
276
+ return {
277
+ isValid: false,
278
+ errorMessage,
279
+ distanceKm
280
+ };
281
+ }
282
+ logger.info(() => ({
283
+ msg: "IP addresses differ but within acceptable distance",
284
+ data: {
285
+ challengeIp: challengeIpString,
286
+ providedIp: ip,
287
+ distanceKm,
288
+ comparison: comparison.comparison
289
+ }
290
+ }));
291
+ return {
292
+ isValid: true,
293
+ distanceKm,
294
+ shouldFlag: true
295
+ };
296
+ }
297
+ const ruleEvaluation = evaluateIpValidationRules(
298
+ comparison,
299
+ ipValidationRules,
300
+ logger
301
+ );
302
+ switch (ruleEvaluation.action) {
303
+ case IPValidationAction.Reject:
304
+ return {
305
+ isValid: false,
306
+ errorMessage: ruleEvaluation.errorMessage,
307
+ distanceKm
308
+ };
309
+ case IPValidationAction.Flag:
310
+ return {
311
+ isValid: true,
312
+ distanceKm,
313
+ shouldFlag: true,
314
+ errorMessage: ruleEvaluation.errorMessage
315
+ };
316
+ default:
317
+ return {
318
+ isValid: true,
319
+ distanceKm
320
+ };
321
+ }
322
+ } catch (error) {
323
+ logger.error(() => ({
324
+ msg: "Error during IP distance validation",
325
+ err: error,
326
+ data: { challengeIp: challengeIpAddress.address, providedIp: ip }
327
+ }));
328
+ return {
329
+ isValid: true,
330
+ shouldFlag: true,
331
+ errorMessage: "IP distance validation error"
332
+ };
333
+ }
334
+ };
105
335
  export {
106
336
  checkIfTaskIsRunning,
337
+ deepValidateIpAddress,
107
338
  encodeStringAddress,
339
+ evaluateIpValidationRules,
108
340
  getIPAddress,
109
341
  getIPAddressFromBigInt,
110
342
  shuffleArray,
@@ -0,0 +1,10 @@
1
+ import { createHash } from "node:crypto";
2
+ function hashUserAgent(userAgent) {
3
+ const hash = createHash("sha256");
4
+ hash.update(userAgent, "utf8");
5
+ const hashHex = hash.digest("hex");
6
+ return hashHex.substring(0, 32);
7
+ }
8
+ export {
9
+ hashUserAgent
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/provider",
3
- "version": "3.2.5",
3
+ "version": "3.12.3",
4
4
  "author": "PROSOPO LIMITED <info@prosopo.io>",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,41 +10,44 @@
10
10
  },
11
11
  "scripts": {
12
12
  "clean": "del-cli --verbose dist tsconfig.tsbuildinfo",
13
- "build": "NODE_ENV=${NODE_ENV:-production}; vite build --config vite.esm.config.ts --mode $NODE_ENV",
13
+ "build": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.esm.config.ts --mode $NODE_ENV",
14
14
  "build:tsc": "tsc --build --verbose",
15
- "build:cjs": "NODE_ENV=${NODE_ENV:-production}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
16
- "typecheck": "tsc --build --declaration --emitDeclarationOnly --force",
17
- "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts",
15
+ "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
16
+ "typecheck": "tsc --project tsconfig.types.json",
17
+ "test:unit": "NODE_ENV=${NODE_ENV:-test}; TEST_TYPE=unit npx vitest run --config ./vite.test.config.ts",
18
+ "test:integration": "NODE_ENV=${NODE_ENV:-test}; NX_PARALLEL=1 TEST_TYPE=integration npx vitest run --config ./vite.threads.test.config.ts",
19
+ "test": "npm run test:unit && npm run test:integration",
18
20
  "mnemonic": "tsx ./scripts/generateMnemonic.ts"
19
21
  },
20
22
  "dependencies": {
21
23
  "@noble/hashes": "1.8.0",
22
24
  "@polkadot/util": "12.6.2",
23
- "@prosopo/api-express-router": "3.0.5",
24
- "@prosopo/api-route": "2.6.9",
25
- "@prosopo/common": "3.1.1",
26
- "@prosopo/config": "3.1.2",
27
- "@prosopo/database": "3.0.12",
28
- "@prosopo/datasets": "3.0.12",
29
- "@prosopo/env": "3.1.3",
30
- "@prosopo/keyring": "2.8.8",
31
- "@prosopo/types": "3.0.5",
32
- "@prosopo/types-database": "3.0.12",
33
- "@prosopo/types-env": "2.7.16",
34
- "@prosopo/user-access-policy": "3.3.2",
35
- "@prosopo/util": "3.0.4",
36
- "@prosopo/util-crypto": "13.5.3",
37
- "@typegoose/auto-increment": "4.13.0",
38
- "axios": "1.10.0",
25
+ "@prosopo/api": "3.1.24",
26
+ "@prosopo/api-express-router": "3.0.25",
27
+ "@prosopo/api-route": "2.6.28",
28
+ "@prosopo/common": "3.1.20",
29
+ "@prosopo/config": "3.1.20",
30
+ "@prosopo/database": "3.4.5",
31
+ "@prosopo/datasets": "3.0.34",
32
+ "@prosopo/env": "3.2.13",
33
+ "@prosopo/keyring": "2.8.27",
34
+ "@prosopo/load-balancer": "2.8.0",
35
+ "@prosopo/locale": "3.1.20",
36
+ "@prosopo/types": "3.5.3",
37
+ "@prosopo/types-database": "3.3.5",
38
+ "@prosopo/types-env": "2.7.38",
39
+ "@prosopo/user-access-policy": "3.5.19",
40
+ "@prosopo/util": "3.1.5",
41
+ "@prosopo/util-crypto": "13.5.22",
39
42
  "cron": "3.1.7",
40
- "esbuild": "0.25.6",
41
43
  "express": "4.21.2",
44
+ "geolib": "3.3.4",
45
+ "i18next": "24.1.0",
42
46
  "ip-address": "10.0.1",
43
- "node-fetch": "3.3.2",
44
- "openpgp": "5.11.3",
47
+ "mongodb": "6.15.0",
48
+ "mongoose": "8.13.0",
45
49
  "read-tls-client-hello": "1.1.0",
46
50
  "uuid": "11.1.0",
47
- "webpack-dev-server": "5.2.2",
48
51
  "zod": "3.23.8"
49
52
  },
50
53
  "devDependencies": {
@@ -1,5 +1,3 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  // Copyright 2021-2025 Prosopo (UK) Ltd.
4
2
  //
5
3
  // Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,6 +11,9 @@ import path from "node:path";
13
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
12
  // See the License for the specific language governing permissions and
15
13
  // limitations under the License.
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
16
17
  import { ViteTestConfig } from "@prosopo/config";
17
18
  import dotenv from "dotenv";
18
19
  process.env.NODE_ENV = "test";
@@ -0,0 +1,33 @@
1
+ // Copyright 2021-2025 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 fs from "node:fs";
16
+ import path from "node:path";
17
+ import { ViteThreadsTestConfig } from "@prosopo/config";
18
+ import dotenv from "dotenv";
19
+ process.env.NODE_ENV = "test";
20
+ // if .env.test exists at this level, use it, otherwise use the one at the root
21
+ const envFile = `.env.${process.env.NODE_ENV || "development"}`;
22
+ let envPath = envFile;
23
+ if (fs.existsSync(envFile)) {
24
+ envPath = path.resolve(envFile);
25
+ } else if (fs.existsSync(`../../${envFile}`)) {
26
+ envPath = path.resolve(`../../${envFile}`);
27
+ } else {
28
+ throw new Error(`No ${envFile} file found`);
29
+ }
30
+
31
+ dotenv.config({ path: envPath });
32
+
33
+ export default ViteThreadsTestConfig();