@prosopo/provider 5.9.0 → 5.10.1
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.
- package/.turbo/turbo-build$colon$cjs.log +13 -12
- package/.turbo/turbo-build$colon$tsc.log +27 -27
- package/.turbo/turbo-build.log +14 -13
- package/CHANGELOG.md +82 -0
- package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.d.ts.map +1 -1
- package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js +5 -1
- package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js.map +1 -1
- package/dist/api/captcha/submitPoWCaptchaSolution.js +51 -11
- package/dist/api/captcha/submitPoWCaptchaSolution.js.map +1 -1
- package/dist/api/healthzGeo.d.ts +54 -0
- package/dist/api/healthzGeo.d.ts.map +1 -0
- package/dist/api/healthzGeo.js +205 -0
- package/dist/api/healthzGeo.js.map +1 -0
- package/dist/api/metrics.d.ts +3 -0
- package/dist/api/metrics.d.ts.map +1 -1
- package/dist/api/metrics.js +11 -1
- package/dist/api/metrics.js.map +1 -1
- package/dist/api/public.d.ts.map +1 -1
- package/dist/api/public.js +13 -3
- package/dist/api/public.js.map +1 -1
- package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/handler.cjs +5 -1
- package/dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs +51 -11
- package/dist/cjs/api/healthzGeo.cjs +209 -0
- package/dist/cjs/api/metrics.cjs +11 -0
- package/dist/cjs/api/public.cjs +13 -2
- package/dist/cjs/services/ipComparison.cjs +4 -2
- package/dist/cjs/tasks/detection/decodePayload.cjs +1126 -983
- package/dist/cjs/tasks/detection/decodePayload.js +1 -2
- package/dist/cjs/tasks/detection/getBotScore.cjs +28 -1
- package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +97 -80
- package/dist/cjs/util.cjs +4 -9
- package/dist/services/ipComparison.d.ts.map +1 -1
- package/dist/services/ipComparison.js +4 -2
- package/dist/services/ipComparison.js.map +1 -1
- package/dist/tasks/detection/decodePayload.d.ts +2 -2
- package/dist/tasks/detection/decodePayload.d.ts.map +1 -1
- package/dist/tasks/detection/decodePayload.js +1 -2
- package/dist/tasks/detection/decodePayload.js.map +1 -1
- package/dist/tasks/detection/getBotScore.d.ts +9 -0
- package/dist/tasks/detection/getBotScore.d.ts.map +1 -1
- package/dist/tasks/detection/getBotScore.js +30 -4
- package/dist/tasks/detection/getBotScore.js.map +1 -1
- package/dist/tasks/frictionless/frictionlessTasks.d.ts +8 -3
- package/dist/tasks/frictionless/frictionlessTasks.d.ts.map +1 -1
- package/dist/tasks/frictionless/frictionlessTasks.js +97 -80
- package/dist/tasks/frictionless/frictionlessTasks.js.map +1 -1
- package/dist/tests/integration/escalationPeekBeforeConsume.integration.test.js +26 -2
- package/dist/tests/integration/escalationPeekBeforeConsume.integration.test.js.map +1 -1
- package/dist/tests/integration/trafficFilterHierarchy.integration.test.js +1 -0
- package/dist/tests/integration/trafficFilterHierarchy.integration.test.js.map +1 -1
- package/dist/tests/integration/webBotAuthAuthenticated.integration.test.js +1 -0
- package/dist/tests/integration/webBotAuthAuthenticated.integration.test.js.map +1 -1
- package/dist/tests/unit/api/captcha/submitPoWCaptchaSolution.escalation.unit.test.js +49 -38
- package/dist/tests/unit/api/captcha/submitPoWCaptchaSolution.escalation.unit.test.js.map +1 -1
- package/dist/tests/unit/api/healthzGeo.unit.test.d.ts +2 -0
- package/dist/tests/unit/api/healthzGeo.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/api/healthzGeo.unit.test.js +272 -0
- package/dist/tests/unit/api/healthzGeo.unit.test.js.map +1 -0
- package/dist/tests/unit/api/public.unit.test.js +132 -2
- package/dist/tests/unit/api/public.unit.test.js.map +1 -1
- package/dist/tests/unit/services/ipComparison.unit.test.js +29 -0
- package/dist/tests/unit/services/ipComparison.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/detection/getBotScore.unit.test.js +44 -1
- package/dist/tests/unit/tasks/detection/getBotScore.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/frictionless/decryptPayload.unit.test.js +39 -0
- package/dist/tests/unit/tasks/frictionless/decryptPayload.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/frictionless/frictionlessTasks.unit.test.js +78 -2
- package/dist/tests/unit/tasks/frictionless/frictionlessTasks.unit.test.js.map +1 -1
- package/dist/tests/unit/testUtils/mockProviderEnv.d.ts.map +1 -1
- package/dist/tests/unit/testUtils/mockProviderEnv.js +1 -0
- package/dist/tests/unit/testUtils/mockProviderEnv.js.map +1 -1
- package/dist/tests/unit/util.deepValidateIpAddress.rules.unit.test.d.ts +2 -0
- package/dist/tests/unit/util.deepValidateIpAddress.rules.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/util.deepValidateIpAddress.rules.unit.test.js +115 -0
- package/dist/tests/unit/util.deepValidateIpAddress.rules.unit.test.js.map +1 -0
- package/dist/tests/unit/util.evaluateIpValidationRules.unit.test.js +70 -0
- package/dist/tests/unit/util.evaluateIpValidationRules.unit.test.js.map +1 -1
- package/dist/tests/unit/util.ipDistance.unit.test.js +1 -0
- package/dist/tests/unit/util.ipDistance.unit.test.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +4 -9
- package/dist/util.js.map +1 -1
- package/package.json +14 -14
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
let _prosopo_types = require("@prosopo/types");
|
|
2
|
+
let zod = require("zod");
|
|
3
|
+
//#region src/api/healthzGeo.ts
|
|
4
|
+
/**
|
|
5
|
+
* Geo steering for `/healthz`.
|
|
6
|
+
*
|
|
7
|
+
* `/healthz` tells a client which node to pin its captcha calls to. By default
|
|
8
|
+
* a node answers with its own name, so the pin is whatever the DNS layer
|
|
9
|
+
* picked. With steering enabled the node instead answers with the node the
|
|
10
|
+
* caller's country is mapped to, when that node is known to be up.
|
|
11
|
+
*
|
|
12
|
+
* Off by default. With the flag off nothing here runs: no lookup, no poller,
|
|
13
|
+
* no response header, no counter.
|
|
14
|
+
*
|
|
15
|
+
* Configuration (all read once, at first use):
|
|
16
|
+
*
|
|
17
|
+
* PROSOPO_HEALTHZ_GEO_STEERING
|
|
18
|
+
* "true" enables steering. Anything else, including unset, disables it.
|
|
19
|
+
*
|
|
20
|
+
* PROSOPO_HEALTHZ_GEO_ROUTES
|
|
21
|
+
* JSON object mapping ISO 3166-1 alpha-2 country code to hostname, e.g.
|
|
22
|
+
* `{"XX":"node1.example.com","YY":"node2.example.com"}`. Keys are
|
|
23
|
+
* case-insensitive. Countries absent from the map are not steered.
|
|
24
|
+
*
|
|
25
|
+
* The map is the candidate set: a host that must not receive traffic
|
|
26
|
+
* simply does not appear in it. Nothing here derives candidates from
|
|
27
|
+
* anywhere else, so the deployment that generates the map is the single
|
|
28
|
+
* place that decides which nodes are in rotation.
|
|
29
|
+
*
|
|
30
|
+
* PROSOPO_HEALTHZ_GEO_PROBE_INTERVAL_MS (default 30000)
|
|
31
|
+
* PROSOPO_HEALTHZ_GEO_PROBE_TIMEOUT_MS (default 2000)
|
|
32
|
+
* Background health-probe period and per-probe timeout.
|
|
33
|
+
*
|
|
34
|
+
* An invalid or unparseable route map disables steering rather than failing
|
|
35
|
+
* startup — the fallback is the node's own name, which is the default
|
|
36
|
+
* behaviour.
|
|
37
|
+
*/
|
|
38
|
+
var DEFAULT_PROBE_INTERVAL_MS = 3e4;
|
|
39
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 2e3;
|
|
40
|
+
var MIN_PROBE_INTERVAL_MS = 1e3;
|
|
41
|
+
/** Hostname, not a URL: no scheme, no path, no port. */
|
|
42
|
+
var hostnameSchema = zod.z.string().min(1).max(253).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i);
|
|
43
|
+
var routesSchema = zod.z.record(zod.z.string().regex(/^[a-z]{2}$/i), hostnameSchema);
|
|
44
|
+
var parsePositiveInt = (raw, fallback) => {
|
|
45
|
+
if (raw === void 0) return fallback;
|
|
46
|
+
const parsed = Number.parseInt(raw, 10);
|
|
47
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
48
|
+
};
|
|
49
|
+
var parseRoutes = (raw, logger) => {
|
|
50
|
+
if (!raw || raw.trim().length === 0) return /* @__PURE__ */ new Map();
|
|
51
|
+
try {
|
|
52
|
+
const parsed = routesSchema.parse(JSON.parse(raw));
|
|
53
|
+
return new Map(Object.entries(parsed).map(([country, host]) => [country.toUpperCase(), host]));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
logger?.warn(() => ({
|
|
56
|
+
msg: "PROSOPO_HEALTHZ_GEO_ROUTES is not a valid country->host map; healthz geo steering stays off",
|
|
57
|
+
err
|
|
58
|
+
}));
|
|
59
|
+
return /* @__PURE__ */ new Map();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var readHealthzGeoConfig = (envVars = process.env, logger) => {
|
|
63
|
+
const routes = parseRoutes(envVars.PROSOPO_HEALTHZ_GEO_ROUTES, logger);
|
|
64
|
+
return {
|
|
65
|
+
enabled: envVars.PROSOPO_HEALTHZ_GEO_STEERING === "true" && routes.size > 0,
|
|
66
|
+
routes,
|
|
67
|
+
probeIntervalMs: Math.max(MIN_PROBE_INTERVAL_MS, parsePositiveInt(envVars.PROSOPO_HEALTHZ_GEO_PROBE_INTERVAL_MS, DEFAULT_PROBE_INTERVAL_MS)),
|
|
68
|
+
probeTimeoutMs: parsePositiveInt(envVars.PROSOPO_HEALTHZ_GEO_PROBE_TIMEOUT_MS, DEFAULT_PROBE_TIMEOUT_MS)
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
var fetchHealthzProbe = async (host, timeoutMs) => {
|
|
72
|
+
try {
|
|
73
|
+
return (await fetch(`https://${host}${_prosopo_types.PublicApiPaths.Healthz}`, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
76
|
+
})).ok;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Tracks whether each candidate host is answering, in the background.
|
|
83
|
+
*
|
|
84
|
+
* `/healthz` is served before the request pipeline that would otherwise
|
|
85
|
+
* observe a node's health, so nothing else here knows whether a candidate is
|
|
86
|
+
* reachable. Every host starts `false` and only becomes `true` on a successful
|
|
87
|
+
* probe, so a poller that has not run, cannot run, or is failing leaves every
|
|
88
|
+
* candidate down and steering off — degrading to the default behaviour by
|
|
89
|
+
* construction rather than by a fallback branch.
|
|
90
|
+
*
|
|
91
|
+
* The probe never touches the request path: `isUp()` reads a boolean.
|
|
92
|
+
*/
|
|
93
|
+
var TargetHealthMonitor = class {
|
|
94
|
+
constructor(options) {
|
|
95
|
+
this.up = /* @__PURE__ */ new Map();
|
|
96
|
+
this.hosts = [...new Set(options.hosts)];
|
|
97
|
+
this.intervalMs = options.intervalMs;
|
|
98
|
+
this.timeoutMs = options.timeoutMs;
|
|
99
|
+
this.probe = options.probe ?? fetchHealthzProbe;
|
|
100
|
+
this.logger = options.logger;
|
|
101
|
+
}
|
|
102
|
+
isUp(host) {
|
|
103
|
+
return this.up.get(host) === true;
|
|
104
|
+
}
|
|
105
|
+
start() {
|
|
106
|
+
if (this.timer !== void 0 || this.hosts.length === 0) return;
|
|
107
|
+
this.pollAll();
|
|
108
|
+
this.timer = setInterval(() => {
|
|
109
|
+
this.pollAll();
|
|
110
|
+
}, this.intervalMs);
|
|
111
|
+
this.timer.unref?.();
|
|
112
|
+
}
|
|
113
|
+
stop() {
|
|
114
|
+
if (this.timer !== void 0) {
|
|
115
|
+
clearInterval(this.timer);
|
|
116
|
+
this.timer = void 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** One probe round over every candidate. Exposed so tests can await it. */
|
|
120
|
+
async pollAll() {
|
|
121
|
+
await Promise.all(this.hosts.map(async (host) => {
|
|
122
|
+
let healthy = false;
|
|
123
|
+
try {
|
|
124
|
+
healthy = await this.probe(host, this.timeoutMs);
|
|
125
|
+
} catch {
|
|
126
|
+
healthy = false;
|
|
127
|
+
}
|
|
128
|
+
const previous = this.up.get(host);
|
|
129
|
+
this.up.set(host, healthy);
|
|
130
|
+
if (previous !== healthy) this.logger?.info(() => ({
|
|
131
|
+
msg: "healthz geo target health changed",
|
|
132
|
+
data: {
|
|
133
|
+
host,
|
|
134
|
+
healthy
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Picks the host `/healthz` should answer with. Pure apart from reading the
|
|
142
|
+
* monitor's booleans and the memory-mapped country lookup — it never awaits
|
|
143
|
+
* anything, so it cannot delay or fail a health check.
|
|
144
|
+
*/
|
|
145
|
+
var HealthzGeoRouter = class {
|
|
146
|
+
constructor(config, env, monitor) {
|
|
147
|
+
this.enabled = config.enabled;
|
|
148
|
+
this.routes = config.routes;
|
|
149
|
+
this.ipInfoService = env.ipInfoService;
|
|
150
|
+
this.monitor = monitor ?? new TargetHealthMonitor({
|
|
151
|
+
hosts: [...new Set(config.routes.values())],
|
|
152
|
+
intervalMs: config.probeIntervalMs,
|
|
153
|
+
timeoutMs: config.probeTimeoutMs,
|
|
154
|
+
logger: env.logger
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
start() {
|
|
158
|
+
if (this.enabled) this.monitor.start();
|
|
159
|
+
}
|
|
160
|
+
stop() {
|
|
161
|
+
this.monitor.stop();
|
|
162
|
+
}
|
|
163
|
+
resolveHost(clientIp, ownHost) {
|
|
164
|
+
if (!this.enabled) return {
|
|
165
|
+
host: ownHost,
|
|
166
|
+
outcome: "not_steered"
|
|
167
|
+
};
|
|
168
|
+
if (!clientIp) return {
|
|
169
|
+
host: ownHost,
|
|
170
|
+
outcome: "geo_unavailable"
|
|
171
|
+
};
|
|
172
|
+
if (!this.ipInfoService.isAvailable()) return {
|
|
173
|
+
host: ownHost,
|
|
174
|
+
outcome: "geo_unavailable"
|
|
175
|
+
};
|
|
176
|
+
const country = this.ipInfoService.country(clientIp);
|
|
177
|
+
if (!country) return {
|
|
178
|
+
host: ownHost,
|
|
179
|
+
outcome: "geo_unavailable"
|
|
180
|
+
};
|
|
181
|
+
const target = this.routes.get(country.toUpperCase());
|
|
182
|
+
if (target === void 0 || target === ownHost) return {
|
|
183
|
+
host: ownHost,
|
|
184
|
+
outcome: "not_steered"
|
|
185
|
+
};
|
|
186
|
+
if (!this.monitor.isUp(target)) return {
|
|
187
|
+
host: ownHost,
|
|
188
|
+
outcome: "target_down"
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
host: target,
|
|
192
|
+
outcome: "steered"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var cachedRouter;
|
|
197
|
+
var getHealthzGeoRouter = (env) => {
|
|
198
|
+
if (!cachedRouter) {
|
|
199
|
+
cachedRouter = new HealthzGeoRouter(readHealthzGeoConfig(process.env, env.logger), env);
|
|
200
|
+
cachedRouter.start();
|
|
201
|
+
}
|
|
202
|
+
return cachedRouter;
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
exports.HealthzGeoRouter = HealthzGeoRouter;
|
|
206
|
+
exports.TargetHealthMonitor = TargetHealthMonitor;
|
|
207
|
+
exports.fetchHealthzProbe = fetchHealthzProbe;
|
|
208
|
+
exports.getHealthzGeoRouter = getHealthzGeoRouter;
|
|
209
|
+
exports.readHealthzGeoConfig = readHealthzGeoConfig;
|
package/dist/cjs/api/metrics.cjs
CHANGED
|
@@ -154,6 +154,12 @@ var buildMetrics = () => {
|
|
|
154
154
|
help: "1 when the given redis connection is ready, else 0",
|
|
155
155
|
labelNames: ["actor"],
|
|
156
156
|
registers: [registry]
|
|
157
|
+
}),
|
|
158
|
+
healthzGeoOutcomesTotal: new prom_client.Counter({
|
|
159
|
+
name: `${PREFIX}healthz_geo_outcomes_total`,
|
|
160
|
+
help: "Healthz geo steering outcomes (steered/not_steered/target_down/geo_unavailable)",
|
|
161
|
+
labelNames: ["outcome"],
|
|
162
|
+
registers: [registry]
|
|
157
163
|
})
|
|
158
164
|
};
|
|
159
165
|
};
|
|
@@ -202,6 +208,10 @@ var recordSpamEmail = (result) => {
|
|
|
202
208
|
if (!metricsEnabled()) return;
|
|
203
209
|
getMetrics().spamEmailTotal.inc({ result });
|
|
204
210
|
};
|
|
211
|
+
var recordHealthzGeoOutcome = (outcome) => {
|
|
212
|
+
if (!metricsEnabled()) return;
|
|
213
|
+
getMetrics().healthzGeoOutcomesTotal.inc({ outcome });
|
|
214
|
+
};
|
|
205
215
|
var setMaintenanceModeGauge = (on) => {
|
|
206
216
|
if (!metricsEnabled()) return;
|
|
207
217
|
getMetrics().maintenanceMode.set(on ? 1 : 0);
|
|
@@ -257,5 +267,6 @@ exports.recordCaptchaVerify = recordCaptchaVerify;
|
|
|
257
267
|
exports.recordDetectorTriggered = recordDetectorTriggered;
|
|
258
268
|
exports.recordDomainValidation = recordDomainValidation;
|
|
259
269
|
exports.recordFrictionlessDecision = recordFrictionlessDecision;
|
|
270
|
+
exports.recordHealthzGeoOutcome = recordHealthzGeoOutcome;
|
|
260
271
|
exports.recordSpamEmail = recordSpamEmail;
|
|
261
272
|
exports.setMaintenanceModeGauge = setMaintenanceModeGauge;
|
package/dist/cjs/api/public.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
2
|
const require_metrics = require("./metrics.cjs");
|
|
3
|
+
const require_healthzGeo = require("./healthzGeo.cjs");
|
|
3
4
|
let _prosopo_common = require("@prosopo/common");
|
|
4
5
|
let _prosopo_types = require("@prosopo/types");
|
|
5
6
|
let _prosopo_util = require("@prosopo/util");
|
|
@@ -14,11 +15,19 @@ express = require_runtime.__toESM(express, 1);
|
|
|
14
15
|
*/
|
|
15
16
|
function publicRouter(env) {
|
|
16
17
|
const router = express.default.Router();
|
|
18
|
+
const geo = require_healthzGeo.getHealthzGeoRouter(env);
|
|
17
19
|
router.get(_prosopo_types.PublicApiPaths.Healthz, (req, res) => {
|
|
18
|
-
const
|
|
20
|
+
const ownHost = env.config.host && env.config.host.length > 0 ? env.config.host : req.hostname;
|
|
21
|
+
if (!geo.enabled) return res.status(200).json({
|
|
22
|
+
ok: true,
|
|
23
|
+
host: ownHost
|
|
24
|
+
});
|
|
25
|
+
res.set("Cache-Control", "no-store, private");
|
|
26
|
+
const decision = geo.resolveHost(req.ip, ownHost);
|
|
27
|
+
require_metrics.recordHealthzGeoOutcome(decision.outcome);
|
|
19
28
|
return res.status(200).json({
|
|
20
29
|
ok: true,
|
|
21
|
-
host
|
|
30
|
+
host: decision.host
|
|
22
31
|
});
|
|
23
32
|
});
|
|
24
33
|
if (require_metrics.metricsEnabled()) router.get(_prosopo_types.PublicApiPaths.Metrics, require_metrics.metricsHandler(env));
|
|
@@ -30,9 +39,11 @@ function publicRouter(env) {
|
|
|
30
39
|
const db = env.getDb();
|
|
31
40
|
const redisConnection = db.getRedisConnection();
|
|
32
41
|
const redisAccessRulesConnection = db.getRedisAccessRulesConnection();
|
|
42
|
+
const host = env.config.host && env.config.host.length > 0 ? env.config.host : req.hostname;
|
|
33
43
|
const response = {
|
|
34
44
|
version: _prosopo_util.version,
|
|
35
45
|
message: "Provider online",
|
|
46
|
+
host,
|
|
36
47
|
redis: [{
|
|
37
48
|
actor: "General",
|
|
38
49
|
isReady: redisConnection.isReady(),
|
|
@@ -100,7 +100,8 @@ async function compareIPs(ip1, ip2, ipInfoService) {
|
|
|
100
100
|
country: ip1Info.country,
|
|
101
101
|
countryCode: ip1Info.countryCode,
|
|
102
102
|
city: ip1Info.city,
|
|
103
|
-
coordinates: ip1Coordinates
|
|
103
|
+
coordinates: ip1Coordinates,
|
|
104
|
+
abuserScore: ip1Info.abuserScore
|
|
104
105
|
},
|
|
105
106
|
ip2Details: {
|
|
106
107
|
provider: ip2Provider,
|
|
@@ -109,7 +110,8 @@ async function compareIPs(ip1, ip2, ipInfoService) {
|
|
|
109
110
|
country: ip2Info.country,
|
|
110
111
|
countryCode: ip2Info.countryCode,
|
|
111
112
|
city: ip2Info.city,
|
|
112
|
-
coordinates: ip2Coordinates
|
|
113
|
+
coordinates: ip2Coordinates,
|
|
114
|
+
abuserScore: ip2Info.abuserScore
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
};
|