@fraudnova/sdk 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +316 -2
- package/dist/index.cjs +841 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +260 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.js +791 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AccountSuspendedError: () => AccountSuspendedError,
|
|
24
|
+
ApiError: () => ApiError,
|
|
25
|
+
AuthenticationError: () => AuthenticationError,
|
|
26
|
+
BATCH_MAX: () => BATCH_MAX,
|
|
27
|
+
BatchItem: () => BatchItem,
|
|
28
|
+
BatchResult: () => BatchResult,
|
|
29
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
30
|
+
EmailResult: () => EmailResult,
|
|
31
|
+
FraudNova: () => FraudNova,
|
|
32
|
+
FraudNovaError: () => FraudNovaError,
|
|
33
|
+
InsufficientCreditsError: () => InsufficientCreditsError,
|
|
34
|
+
InvalidArgumentError: () => InvalidArgumentError,
|
|
35
|
+
IpResult: () => IpResult,
|
|
36
|
+
PhoneResult: () => PhoneResult,
|
|
37
|
+
RateLimitError: () => RateLimitError,
|
|
38
|
+
RetryPolicy: () => RetryPolicy,
|
|
39
|
+
TransportError: () => TransportError,
|
|
40
|
+
UnexpectedResponseError: () => UnexpectedResponseError,
|
|
41
|
+
UpstreamError: () => UpstreamError,
|
|
42
|
+
VERSION: () => VERSION,
|
|
43
|
+
ValidationError: () => ValidationError,
|
|
44
|
+
maskApiKey: () => maskApiKey,
|
|
45
|
+
parseDateInfo: () => parseDateInfo,
|
|
46
|
+
parseRetryAfter: () => parseRetryAfter
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(index_exports);
|
|
49
|
+
|
|
50
|
+
// src/version.ts
|
|
51
|
+
var VERSION = "1.0.0";
|
|
52
|
+
|
|
53
|
+
// src/errors.ts
|
|
54
|
+
function maskApiKey(apiKey) {
|
|
55
|
+
return apiKey.length > 12 ? `fn_\u2026${apiKey.slice(-4)}` : "fn_\u2026";
|
|
56
|
+
}
|
|
57
|
+
var FraudNovaError = class extends Error {
|
|
58
|
+
constructor(message, options) {
|
|
59
|
+
super(message, options);
|
|
60
|
+
this.name = "FraudNovaError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var InvalidArgumentError = class extends FraudNovaError {
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "InvalidArgumentError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var TransportError = class extends FraudNovaError {
|
|
70
|
+
constructor(message, options) {
|
|
71
|
+
super(message, options);
|
|
72
|
+
this.name = "TransportError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var ApiError = class _ApiError extends FraudNovaError {
|
|
76
|
+
statusCode;
|
|
77
|
+
apiMessage;
|
|
78
|
+
body;
|
|
79
|
+
constructor(statusCode, apiMessage, body = {}, message) {
|
|
80
|
+
super(message ?? `FraudNova API error (HTTP ${statusCode}): ${apiMessage}`);
|
|
81
|
+
this.name = "ApiError";
|
|
82
|
+
this.statusCode = statusCode;
|
|
83
|
+
this.apiMessage = apiMessage;
|
|
84
|
+
this.body = body;
|
|
85
|
+
}
|
|
86
|
+
static fromResponse(statusCode, text, retryAfter = null) {
|
|
87
|
+
let decoded = null;
|
|
88
|
+
try {
|
|
89
|
+
decoded = JSON.parse(text);
|
|
90
|
+
} catch {
|
|
91
|
+
decoded = null;
|
|
92
|
+
}
|
|
93
|
+
const isJson = typeof decoded === "object" && decoded !== null && !Array.isArray(decoded);
|
|
94
|
+
const body = isJson ? decoded : {};
|
|
95
|
+
const envelope = isJson && ("success" in body || "message" in body);
|
|
96
|
+
let apiMessage = "";
|
|
97
|
+
for (const key of ["message", "error"]) {
|
|
98
|
+
const value = body[key];
|
|
99
|
+
if (typeof value === "string" && value !== "") {
|
|
100
|
+
apiMessage = value;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (apiMessage === "") {
|
|
105
|
+
const stripped = text.trim();
|
|
106
|
+
apiMessage = stripped !== "" ? stripped.slice(0, 200) : `HTTP ${statusCode}`;
|
|
107
|
+
}
|
|
108
|
+
const full = isJson ? void 0 : `FraudNova API error (HTTP ${statusCode}): non-JSON response`;
|
|
109
|
+
if (envelope && statusCode === 401)
|
|
110
|
+
return new AuthenticationError(statusCode, apiMessage, body, full);
|
|
111
|
+
if (envelope && statusCode === 402) {
|
|
112
|
+
return new InsufficientCreditsError(statusCode, apiMessage, body, full);
|
|
113
|
+
}
|
|
114
|
+
if (envelope && statusCode === 403)
|
|
115
|
+
return new AccountSuspendedError(statusCode, apiMessage, body, full);
|
|
116
|
+
if (envelope && statusCode === 422)
|
|
117
|
+
return new ValidationError(statusCode, apiMessage, body, full);
|
|
118
|
+
if (statusCode === 429) {
|
|
119
|
+
return new RateLimitError(statusCode, apiMessage, body, retryAfter, full);
|
|
120
|
+
}
|
|
121
|
+
if (statusCode === 502 || statusCode === 503 || statusCode === 504) {
|
|
122
|
+
return new UpstreamError(statusCode, apiMessage, body, statusCode === 503 && envelope, full);
|
|
123
|
+
}
|
|
124
|
+
return new _ApiError(statusCode, apiMessage, body, full);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var AuthenticationError = class extends ApiError {
|
|
128
|
+
constructor(statusCode, apiMessage, body = {}, message) {
|
|
129
|
+
super(statusCode, apiMessage, body, message);
|
|
130
|
+
this.name = "AuthenticationError";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var InsufficientCreditsError = class extends ApiError {
|
|
134
|
+
constructor(statusCode, apiMessage, body = {}, message) {
|
|
135
|
+
super(statusCode, apiMessage, body, message);
|
|
136
|
+
this.name = "InsufficientCreditsError";
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var AccountSuspendedError = class extends ApiError {
|
|
140
|
+
constructor(statusCode, apiMessage, body = {}, message) {
|
|
141
|
+
super(statusCode, apiMessage, body, message);
|
|
142
|
+
this.name = "AccountSuspendedError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var ValidationError = class extends ApiError {
|
|
146
|
+
constructor(statusCode, apiMessage, body = {}, message) {
|
|
147
|
+
super(statusCode, apiMessage, body, message);
|
|
148
|
+
this.name = "ValidationError";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var RateLimitError = class extends ApiError {
|
|
152
|
+
retryAfter;
|
|
153
|
+
constructor(statusCode, apiMessage, body = {}, retryAfter = null, message) {
|
|
154
|
+
super(statusCode, apiMessage, body, message);
|
|
155
|
+
this.name = "RateLimitError";
|
|
156
|
+
this.retryAfter = retryAfter;
|
|
157
|
+
}
|
|
158
|
+
get isConcurrencyLimit() {
|
|
159
|
+
return this.apiMessage.includes("Too many concurrent requests");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var UpstreamError = class extends ApiError {
|
|
163
|
+
creditRefunded;
|
|
164
|
+
constructor(statusCode, apiMessage, body = {}, creditRefunded = false, message) {
|
|
165
|
+
let text = message ?? `FraudNova API error (HTTP ${statusCode}): ${apiMessage}`;
|
|
166
|
+
if (creditRefunded) text += " The credit for this check was refunded.";
|
|
167
|
+
super(statusCode, apiMessage, body, text);
|
|
168
|
+
this.name = "UpstreamError";
|
|
169
|
+
this.creditRefunded = creditRefunded;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var UnexpectedResponseError = class extends ApiError {
|
|
173
|
+
constructor(statusCode, apiMessage, body = {}, message) {
|
|
174
|
+
super(statusCode, apiMessage, body, message);
|
|
175
|
+
this.name = "UnexpectedResponseError";
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/results.ts
|
|
180
|
+
function coerceBool(v) {
|
|
181
|
+
return typeof v === "boolean" ? v : null;
|
|
182
|
+
}
|
|
183
|
+
function coerceString(v) {
|
|
184
|
+
return typeof v === "string" ? v : null;
|
|
185
|
+
}
|
|
186
|
+
function roundHalfAway(n) {
|
|
187
|
+
const r = Math.sign(n) * Math.floor(Math.abs(n) + 0.5);
|
|
188
|
+
return r === 0 ? 0 : r;
|
|
189
|
+
}
|
|
190
|
+
function coerceInt(v) {
|
|
191
|
+
let n;
|
|
192
|
+
if (typeof v === "number") {
|
|
193
|
+
n = v;
|
|
194
|
+
} else if (typeof v === "string" && (/^-?\d+$/.test(v) || /^-?\d+\.\d+$/.test(v))) {
|
|
195
|
+
n = Number(v);
|
|
196
|
+
} else {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return Number.isFinite(n) ? roundHalfAway(n) : null;
|
|
200
|
+
}
|
|
201
|
+
function isPlainObject(v) {
|
|
202
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
203
|
+
}
|
|
204
|
+
function parseDateInfo(value) {
|
|
205
|
+
if (typeof value === "string") {
|
|
206
|
+
return { human: value, timestamp: null, iso: null };
|
|
207
|
+
}
|
|
208
|
+
if (isPlainObject(value)) {
|
|
209
|
+
return {
|
|
210
|
+
human: coerceString(value.human),
|
|
211
|
+
timestamp: coerceInt(value.timestamp),
|
|
212
|
+
iso: coerceString(value.iso)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
var FlaggableResult = class {
|
|
218
|
+
creditsRemaining;
|
|
219
|
+
raw;
|
|
220
|
+
constructor(raw, creditsRemaining) {
|
|
221
|
+
this.raw = raw;
|
|
222
|
+
this.creditsRemaining = creditsRemaining;
|
|
223
|
+
}
|
|
224
|
+
toJSON() {
|
|
225
|
+
return this.raw;
|
|
226
|
+
}
|
|
227
|
+
flagReasons(options = {}) {
|
|
228
|
+
const threshold = options.riskThreshold ?? 75;
|
|
229
|
+
const reasons = [];
|
|
230
|
+
if ((coerceInt(this.raw.fraud_score) ?? 0) >= threshold) {
|
|
231
|
+
reasons.push("fraud_score");
|
|
232
|
+
}
|
|
233
|
+
for (const name of options.flagOn ?? this.defaultFlagOn()) {
|
|
234
|
+
if (this.raw[name] === true) {
|
|
235
|
+
reasons.push(name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return reasons;
|
|
239
|
+
}
|
|
240
|
+
flagged(options = {}) {
|
|
241
|
+
return this.flagReasons(options).length > 0;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var IpResult = class _IpResult extends FlaggableResult {
|
|
245
|
+
static DEFAULT_FLAG_ON = ["proxy", "vpn", "tor"];
|
|
246
|
+
fraudScore;
|
|
247
|
+
proxy;
|
|
248
|
+
vpn;
|
|
249
|
+
tor;
|
|
250
|
+
activeVpn;
|
|
251
|
+
activeTor;
|
|
252
|
+
recentAbuse;
|
|
253
|
+
botStatus;
|
|
254
|
+
countryCode;
|
|
255
|
+
region;
|
|
256
|
+
city;
|
|
257
|
+
isp;
|
|
258
|
+
organization;
|
|
259
|
+
asn;
|
|
260
|
+
operatingSystem;
|
|
261
|
+
browser;
|
|
262
|
+
deviceBrand;
|
|
263
|
+
deviceModel;
|
|
264
|
+
constructor(raw, creditsRemaining) {
|
|
265
|
+
super(raw, creditsRemaining);
|
|
266
|
+
this.fraudScore = coerceInt(raw.fraud_score) ?? 0;
|
|
267
|
+
this.proxy = coerceBool(raw.proxy);
|
|
268
|
+
this.vpn = coerceBool(raw.vpn);
|
|
269
|
+
this.tor = coerceBool(raw.tor);
|
|
270
|
+
this.activeVpn = coerceBool(raw.active_vpn);
|
|
271
|
+
this.activeTor = coerceBool(raw.active_tor);
|
|
272
|
+
this.recentAbuse = coerceBool(raw.recent_abuse);
|
|
273
|
+
this.botStatus = coerceBool(raw.bot_status);
|
|
274
|
+
this.countryCode = coerceString(raw.country_code);
|
|
275
|
+
this.region = coerceString(raw.region);
|
|
276
|
+
this.city = coerceString(raw.city);
|
|
277
|
+
this.isp = coerceString(raw.ISP);
|
|
278
|
+
this.organization = coerceString(raw.organization);
|
|
279
|
+
this.asn = coerceInt(raw.ASN);
|
|
280
|
+
this.operatingSystem = coerceString(raw.operating_system);
|
|
281
|
+
this.browser = coerceString(raw.browser);
|
|
282
|
+
this.deviceBrand = coerceString(raw.device_brand);
|
|
283
|
+
this.deviceModel = coerceString(raw.device_model);
|
|
284
|
+
}
|
|
285
|
+
static fromData(data2, creditsRemaining = null) {
|
|
286
|
+
return new _IpResult(data2, creditsRemaining);
|
|
287
|
+
}
|
|
288
|
+
defaultFlagOn() {
|
|
289
|
+
return _IpResult.DEFAULT_FLAG_ON;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
var EmailResult = class _EmailResult extends FlaggableResult {
|
|
293
|
+
static DEFAULT_FLAG_ON = ["disposable"];
|
|
294
|
+
fraudScore;
|
|
295
|
+
valid;
|
|
296
|
+
disposable;
|
|
297
|
+
catchAll;
|
|
298
|
+
generic;
|
|
299
|
+
common;
|
|
300
|
+
dnsValid;
|
|
301
|
+
honeypot;
|
|
302
|
+
frequentComplainer;
|
|
303
|
+
suspect;
|
|
304
|
+
recentAbuse;
|
|
305
|
+
leaked;
|
|
306
|
+
riskyTld;
|
|
307
|
+
spfRecord;
|
|
308
|
+
dmarcRecord;
|
|
309
|
+
smtpScore;
|
|
310
|
+
overallScore;
|
|
311
|
+
deliverability;
|
|
312
|
+
spamTrapScore;
|
|
313
|
+
resultType;
|
|
314
|
+
sanitizedEmail;
|
|
315
|
+
firstSeen;
|
|
316
|
+
domainAge;
|
|
317
|
+
constructor(raw, creditsRemaining) {
|
|
318
|
+
super(raw, creditsRemaining);
|
|
319
|
+
this.fraudScore = coerceInt(raw.fraud_score) ?? 0;
|
|
320
|
+
this.valid = coerceBool(raw.valid);
|
|
321
|
+
this.disposable = coerceBool(raw.disposable);
|
|
322
|
+
this.catchAll = coerceBool(raw.catch_all);
|
|
323
|
+
this.generic = coerceBool(raw.generic);
|
|
324
|
+
this.common = coerceBool(raw.common);
|
|
325
|
+
this.dnsValid = coerceBool(raw.dns_valid);
|
|
326
|
+
this.honeypot = coerceBool(raw.honeypot);
|
|
327
|
+
this.frequentComplainer = coerceBool(raw.frequent_complainer);
|
|
328
|
+
this.suspect = coerceBool(raw.suspect);
|
|
329
|
+
this.recentAbuse = coerceBool(raw.recent_abuse);
|
|
330
|
+
this.leaked = coerceBool(raw.leaked);
|
|
331
|
+
this.riskyTld = coerceBool(raw.risky_tld);
|
|
332
|
+
this.spfRecord = coerceBool(raw.spf_record);
|
|
333
|
+
this.dmarcRecord = coerceBool(raw.dmarc_record);
|
|
334
|
+
this.smtpScore = coerceInt(raw.smtp_score);
|
|
335
|
+
this.overallScore = coerceInt(raw.overall_score);
|
|
336
|
+
this.deliverability = coerceString(raw.deliverability);
|
|
337
|
+
this.spamTrapScore = coerceString(raw.spam_trap_score);
|
|
338
|
+
this.resultType = coerceString(raw.result_type);
|
|
339
|
+
this.sanitizedEmail = coerceString(raw.sanitized_email);
|
|
340
|
+
this.firstSeen = parseDateInfo(raw.first_seen);
|
|
341
|
+
this.domainAge = parseDateInfo(raw.domain_age);
|
|
342
|
+
}
|
|
343
|
+
static fromData(data2, creditsRemaining = null) {
|
|
344
|
+
return new _EmailResult(data2, creditsRemaining);
|
|
345
|
+
}
|
|
346
|
+
defaultFlagOn() {
|
|
347
|
+
return _EmailResult.DEFAULT_FLAG_ON;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
var PhoneResult = class _PhoneResult extends FlaggableResult {
|
|
351
|
+
static DEFAULT_FLAG_ON = ["VOIP", "risky"];
|
|
352
|
+
fraudScore;
|
|
353
|
+
valid;
|
|
354
|
+
recentAbuse;
|
|
355
|
+
voip;
|
|
356
|
+
prepaid;
|
|
357
|
+
risky;
|
|
358
|
+
active;
|
|
359
|
+
doNotCall;
|
|
360
|
+
leaked;
|
|
361
|
+
spammer;
|
|
362
|
+
dialingCode;
|
|
363
|
+
carrier;
|
|
364
|
+
lineType;
|
|
365
|
+
country;
|
|
366
|
+
region;
|
|
367
|
+
city;
|
|
368
|
+
timezone;
|
|
369
|
+
zipCode;
|
|
370
|
+
activeStatus;
|
|
371
|
+
formatted;
|
|
372
|
+
localFormat;
|
|
373
|
+
constructor(raw, creditsRemaining) {
|
|
374
|
+
super(raw, creditsRemaining);
|
|
375
|
+
this.fraudScore = coerceInt(raw.fraud_score) ?? 0;
|
|
376
|
+
this.valid = coerceBool(raw.valid);
|
|
377
|
+
this.recentAbuse = coerceBool(raw.recent_abuse);
|
|
378
|
+
this.voip = coerceBool(raw.VOIP);
|
|
379
|
+
this.prepaid = coerceBool(raw.prepaid);
|
|
380
|
+
this.risky = coerceBool(raw.risky);
|
|
381
|
+
this.active = coerceBool(raw.active);
|
|
382
|
+
this.doNotCall = coerceBool(raw.do_not_call);
|
|
383
|
+
this.leaked = coerceBool(raw.leaked);
|
|
384
|
+
this.spammer = coerceBool(raw.spammer);
|
|
385
|
+
this.dialingCode = coerceInt(raw.dialing_code);
|
|
386
|
+
this.carrier = coerceString(raw.carrier);
|
|
387
|
+
this.lineType = coerceString(raw.line_type);
|
|
388
|
+
this.country = coerceString(raw.country);
|
|
389
|
+
this.region = coerceString(raw.region);
|
|
390
|
+
this.city = coerceString(raw.city);
|
|
391
|
+
this.timezone = coerceString(raw.timezone);
|
|
392
|
+
this.zipCode = coerceString(raw.zip_code);
|
|
393
|
+
this.activeStatus = coerceString(raw.active_status);
|
|
394
|
+
this.formatted = coerceString(raw.formatted);
|
|
395
|
+
this.localFormat = coerceString(raw.local_format);
|
|
396
|
+
}
|
|
397
|
+
static fromData(data2, creditsRemaining = null) {
|
|
398
|
+
return new _PhoneResult(data2, creditsRemaining);
|
|
399
|
+
}
|
|
400
|
+
defaultFlagOn() {
|
|
401
|
+
return _PhoneResult.DEFAULT_FLAG_ON;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
var BatchItem = class {
|
|
405
|
+
constructor(subject2, success, result, error) {
|
|
406
|
+
this.subject = subject2;
|
|
407
|
+
this.success = success;
|
|
408
|
+
this.result = result;
|
|
409
|
+
this.error = error;
|
|
410
|
+
}
|
|
411
|
+
subject;
|
|
412
|
+
success;
|
|
413
|
+
result;
|
|
414
|
+
error;
|
|
415
|
+
isInvalid() {
|
|
416
|
+
return this.error === "invalid_ip" || this.error === "invalid_email" || this.error === "invalid_phone";
|
|
417
|
+
}
|
|
418
|
+
isInsufficientCredits() {
|
|
419
|
+
return this.error === "insufficient_credits";
|
|
420
|
+
}
|
|
421
|
+
isUpstreamFailed() {
|
|
422
|
+
return this.error === "upstream_failed";
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var BatchResult = class _BatchResult {
|
|
426
|
+
constructor(items, creditsRemaining) {
|
|
427
|
+
this.items = items;
|
|
428
|
+
this.creditsRemaining = creditsRemaining;
|
|
429
|
+
}
|
|
430
|
+
items;
|
|
431
|
+
creditsRemaining;
|
|
432
|
+
static fromEnvelope(envelope, kind) {
|
|
433
|
+
const items = [];
|
|
434
|
+
const rawItems = envelope.results;
|
|
435
|
+
if (Array.isArray(rawItems)) {
|
|
436
|
+
for (const item of rawItems) {
|
|
437
|
+
if (!isPlainObject(item)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const success = coerceBool(item.success) ?? false;
|
|
441
|
+
const data2 = item.data;
|
|
442
|
+
let result = null;
|
|
443
|
+
if (success && isPlainObject(data2)) {
|
|
444
|
+
result = kind === "ip" ? IpResult.fromData(data2) : kind === "email" ? EmailResult.fromData(data2) : PhoneResult.fromData(data2);
|
|
445
|
+
}
|
|
446
|
+
items.push(
|
|
447
|
+
new BatchItem(coerceString(item[kind]) ?? "", success, result, coerceString(item.error))
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return new _BatchResult(items, coerceInt(envelope.credits_remaining) ?? 0);
|
|
452
|
+
}
|
|
453
|
+
successful() {
|
|
454
|
+
return this.items.filter((i) => i.success);
|
|
455
|
+
}
|
|
456
|
+
failed() {
|
|
457
|
+
return this.items.filter((i) => !i.success);
|
|
458
|
+
}
|
|
459
|
+
hasFailures() {
|
|
460
|
+
return this.items.some((i) => !i.success);
|
|
461
|
+
}
|
|
462
|
+
get length() {
|
|
463
|
+
return this.items.length;
|
|
464
|
+
}
|
|
465
|
+
*[Symbol.iterator]() {
|
|
466
|
+
yield* this.items;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/retry.ts
|
|
471
|
+
function parseRetryAfter(value, now = /* @__PURE__ */ new Date()) {
|
|
472
|
+
if (value === null) return null;
|
|
473
|
+
const text = value.trim();
|
|
474
|
+
if (text === "") return null;
|
|
475
|
+
if (/^[0-9]+$/.test(text)) return Number.parseInt(text, 10);
|
|
476
|
+
const when = Date.parse(text);
|
|
477
|
+
if (Number.isNaN(when)) return null;
|
|
478
|
+
return Math.max(0, Math.ceil((when - now.getTime()) / 1e3));
|
|
479
|
+
}
|
|
480
|
+
var defaultSleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
481
|
+
var RetryPolicy = class {
|
|
482
|
+
maxAttempts;
|
|
483
|
+
concurrencyDelays;
|
|
484
|
+
rateLimitDelays;
|
|
485
|
+
honourRetryAfter;
|
|
486
|
+
maxRetryAfter;
|
|
487
|
+
#sleeper;
|
|
488
|
+
constructor(options = {}) {
|
|
489
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
490
|
+
this.concurrencyDelays = [...options.concurrencyDelays ?? [1, 2, 4]];
|
|
491
|
+
this.rateLimitDelays = [...options.rateLimitDelays ?? [5, 15, 40]];
|
|
492
|
+
this.honourRetryAfter = options.honourRetryAfter ?? true;
|
|
493
|
+
this.maxRetryAfter = options.maxRetryAfter ?? 60;
|
|
494
|
+
this.#sleeper = options.sleep ?? defaultSleep;
|
|
495
|
+
if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) {
|
|
496
|
+
throw new InvalidArgumentError("maxAttempts must be an integer of at least 1.");
|
|
497
|
+
}
|
|
498
|
+
if (!Number.isFinite(this.maxRetryAfter) || this.maxRetryAfter < 0) {
|
|
499
|
+
throw new InvalidArgumentError("maxRetryAfter must be a finite number that is not negative.");
|
|
500
|
+
}
|
|
501
|
+
for (const [name, delays] of [
|
|
502
|
+
["concurrencyDelays", this.concurrencyDelays],
|
|
503
|
+
["rateLimitDelays", this.rateLimitDelays]
|
|
504
|
+
]) {
|
|
505
|
+
if (!delays.every((delay) => typeof delay === "number" && Number.isFinite(delay) && delay >= 0)) {
|
|
506
|
+
throw new InvalidArgumentError(
|
|
507
|
+
`${name} must contain only finite numbers that are not negative.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** Seconds to wait before retrying after `attempt` (1-based) failed, or null to stop. */
|
|
513
|
+
delayFor(error, attempt) {
|
|
514
|
+
if (attempt >= this.maxAttempts) return null;
|
|
515
|
+
if (this.honourRetryAfter && error.retryAfter !== null) {
|
|
516
|
+
return error.retryAfter > this.maxRetryAfter ? null : error.retryAfter;
|
|
517
|
+
}
|
|
518
|
+
const schedule = error.isConcurrencyLimit ? this.concurrencyDelays : this.rateLimitDelays;
|
|
519
|
+
const delay = schedule[attempt - 1];
|
|
520
|
+
return delay === void 0 ? null : delay;
|
|
521
|
+
}
|
|
522
|
+
sleep(seconds) {
|
|
523
|
+
return this.#sleeper(seconds);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/transport.ts
|
|
528
|
+
function userAgent() {
|
|
529
|
+
let agent = `fraudnova-node/${VERSION}`;
|
|
530
|
+
if (typeof process !== "undefined" && typeof process.versions?.node === "string") {
|
|
531
|
+
agent += ` node/${process.versions.node}`;
|
|
532
|
+
}
|
|
533
|
+
return agent;
|
|
534
|
+
}
|
|
535
|
+
function isPlainObject2(value) {
|
|
536
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
537
|
+
}
|
|
538
|
+
var Transport = class _Transport {
|
|
539
|
+
baseUrl;
|
|
540
|
+
#apiKey;
|
|
541
|
+
#timeout;
|
|
542
|
+
#retry;
|
|
543
|
+
#fetch;
|
|
544
|
+
#userAgent;
|
|
545
|
+
constructor(options) {
|
|
546
|
+
if (!/^[\x21-\x7e]+$/.test(options.apiKey)) {
|
|
547
|
+
throw new InvalidArgumentError(
|
|
548
|
+
"apiKey must contain only printable ASCII characters, without whitespace."
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
let fetchImpl = options.fetch ?? null;
|
|
552
|
+
if (fetchImpl === null) {
|
|
553
|
+
const globalFetch = globalThis.fetch;
|
|
554
|
+
if (typeof globalFetch === "function") {
|
|
555
|
+
fetchImpl = globalFetch.bind(globalThis);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (fetchImpl === null) {
|
|
559
|
+
throw new InvalidArgumentError(
|
|
560
|
+
"No fetch implementation available; pass one in the fetch option"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
this.#apiKey = options.apiKey;
|
|
564
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
565
|
+
this.#timeout = options.timeout;
|
|
566
|
+
this.#retry = options.retry;
|
|
567
|
+
this.#fetch = fetchImpl;
|
|
568
|
+
this.#userAgent = userAgent();
|
|
569
|
+
}
|
|
570
|
+
get(path, params = null, authenticated = true) {
|
|
571
|
+
const query = new URLSearchParams();
|
|
572
|
+
for (const [name, value] of Object.entries(params ?? {})) {
|
|
573
|
+
if (value === void 0 || value === null) continue;
|
|
574
|
+
query.append(name, String(value));
|
|
575
|
+
}
|
|
576
|
+
const search = query.toString();
|
|
577
|
+
const url = this.baseUrl + path + (search !== "" ? `?${search}` : "");
|
|
578
|
+
return this.#send("GET", url, authenticated);
|
|
579
|
+
}
|
|
580
|
+
post(path, json) {
|
|
581
|
+
return this.#send("POST", this.baseUrl + path, true, JSON.stringify(json));
|
|
582
|
+
}
|
|
583
|
+
#headers(authenticated, hasBody) {
|
|
584
|
+
const headers = {
|
|
585
|
+
Accept: "application/json",
|
|
586
|
+
"User-Agent": this.#userAgent
|
|
587
|
+
};
|
|
588
|
+
if (authenticated) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
589
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
590
|
+
return headers;
|
|
591
|
+
}
|
|
592
|
+
async #send(method, url, authenticated, body) {
|
|
593
|
+
let attempt = 1;
|
|
594
|
+
for (; ; ) {
|
|
595
|
+
const init = {
|
|
596
|
+
method,
|
|
597
|
+
headers: this.#headers(authenticated, body !== void 0)
|
|
598
|
+
};
|
|
599
|
+
if (body !== void 0) init.body = body;
|
|
600
|
+
const controller = new AbortController();
|
|
601
|
+
const timer = setTimeout(() => controller.abort(), this.#timeout);
|
|
602
|
+
let response;
|
|
603
|
+
let text;
|
|
604
|
+
try {
|
|
605
|
+
response = await this.#fetch(url, { ...init, signal: controller.signal });
|
|
606
|
+
text = await response.text();
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (controller.signal.aborted) {
|
|
609
|
+
throw new TransportError(`FraudNova request timed out after ${this.#timeout} ms`, {
|
|
610
|
+
cause: error
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
614
|
+
const masked = original.split(this.#apiKey).join(maskApiKey(this.#apiKey));
|
|
615
|
+
throw new TransportError(`FraudNova request failed: ${masked}`, { cause: error });
|
|
616
|
+
} finally {
|
|
617
|
+
clearTimeout(timer);
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
return _Transport.#decode(response, text);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (!(error instanceof RateLimitError)) throw error;
|
|
623
|
+
const delay = this.#retry?.delayFor(error, attempt) ?? null;
|
|
624
|
+
if (this.#retry === null || delay === null) throw error;
|
|
625
|
+
await this.#retry.sleep(delay);
|
|
626
|
+
attempt++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
static #decode(response, text) {
|
|
631
|
+
const status = response.status;
|
|
632
|
+
if (status < 200 || status >= 300) {
|
|
633
|
+
throw ApiError.fromResponse(
|
|
634
|
+
status,
|
|
635
|
+
text,
|
|
636
|
+
parseRetryAfter(response.headers.get("retry-after"))
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
let decoded = null;
|
|
640
|
+
try {
|
|
641
|
+
decoded = JSON.parse(text);
|
|
642
|
+
} catch {
|
|
643
|
+
decoded = null;
|
|
644
|
+
}
|
|
645
|
+
if (!isPlainObject2(decoded) || decoded.success !== true) {
|
|
646
|
+
throw new UnexpectedResponseError(
|
|
647
|
+
status,
|
|
648
|
+
"Unexpected response body",
|
|
649
|
+
isPlainObject2(decoded) ? decoded : {}
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return decoded;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// src/client.ts
|
|
657
|
+
var DEFAULT_BASE_URL = "https://fraudnova.com/api/v1";
|
|
658
|
+
var BATCH_MAX = 100;
|
|
659
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
660
|
+
var inspectSymbol = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
|
|
661
|
+
function subject(value, name) {
|
|
662
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
663
|
+
throw new InvalidArgumentError(`${name} must be a non-empty string`);
|
|
664
|
+
}
|
|
665
|
+
return value.trim();
|
|
666
|
+
}
|
|
667
|
+
function strictness(value) {
|
|
668
|
+
if (value === void 0) return void 0;
|
|
669
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 3) {
|
|
670
|
+
throw new InvalidArgumentError("strictness must be an integer from 0 to 3");
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
function country(value) {
|
|
675
|
+
if (typeof value !== "string") return void 0;
|
|
676
|
+
const cleaned = value.trim().toUpperCase();
|
|
677
|
+
return cleaned !== "" ? cleaned : void 0;
|
|
678
|
+
}
|
|
679
|
+
function data(envelope) {
|
|
680
|
+
const value = envelope.data;
|
|
681
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
682
|
+
throw new UnexpectedResponseError(200, "Missing data", envelope);
|
|
683
|
+
}
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
function ipOptions(options) {
|
|
687
|
+
const out = {};
|
|
688
|
+
const level = strictness(options.strictness);
|
|
689
|
+
if (level !== void 0) out.strictness = level;
|
|
690
|
+
if (options.mobile === true || options.mobile === false) out.mobile = options.mobile;
|
|
691
|
+
if (options.userAgent !== void 0) out.user_agent = options.userAgent;
|
|
692
|
+
if (options.userLanguage !== void 0) out.user_language = options.userLanguage;
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
var FraudNova = class {
|
|
696
|
+
baseUrl;
|
|
697
|
+
#apiKey;
|
|
698
|
+
#transport;
|
|
699
|
+
constructor(apiKey, options = {}) {
|
|
700
|
+
if (typeof apiKey !== "string" || apiKey.trim() === "") {
|
|
701
|
+
throw new InvalidArgumentError("apiKey must be a non-empty string");
|
|
702
|
+
}
|
|
703
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
704
|
+
if (typeof baseUrl !== "string" || !/^https?:\/\//i.test(baseUrl)) {
|
|
705
|
+
throw new InvalidArgumentError("baseUrl must start with http:// or https://");
|
|
706
|
+
}
|
|
707
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
708
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
709
|
+
throw new InvalidArgumentError(
|
|
710
|
+
"timeout must be a positive number of milliseconds (at most 2147483647)"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
this.#apiKey = apiKey.trim();
|
|
714
|
+
this.#transport = new Transport({
|
|
715
|
+
apiKey: this.#apiKey,
|
|
716
|
+
baseUrl,
|
|
717
|
+
timeout,
|
|
718
|
+
retry: options.retry ?? null,
|
|
719
|
+
fetch: options.fetch ?? null
|
|
720
|
+
});
|
|
721
|
+
this.baseUrl = this.#transport.baseUrl;
|
|
722
|
+
}
|
|
723
|
+
/** Score one IP address. */
|
|
724
|
+
async checkIp(ip, options = {}) {
|
|
725
|
+
const params = { ip: subject(ip, "ip"), ...ipOptions(options) };
|
|
726
|
+
const envelope = await this.#transport.get("/check", params);
|
|
727
|
+
return IpResult.fromData(data(envelope), coerceInt(envelope.credits_remaining));
|
|
728
|
+
}
|
|
729
|
+
/** Score one email address. */
|
|
730
|
+
async checkEmail(email) {
|
|
731
|
+
const params = { email: subject(email, "email") };
|
|
732
|
+
const envelope = await this.#transport.get("/check/email", params);
|
|
733
|
+
return EmailResult.fromData(data(envelope), coerceInt(envelope.credits_remaining));
|
|
734
|
+
}
|
|
735
|
+
/** Score one phone number. */
|
|
736
|
+
async checkPhone(phone, options = {}) {
|
|
737
|
+
const params = { phone: subject(phone, "phone"), country: country(options.country) };
|
|
738
|
+
const envelope = await this.#transport.get("/check/phone", params);
|
|
739
|
+
return PhoneResult.fromData(data(envelope), coerceInt(envelope.credits_remaining));
|
|
740
|
+
}
|
|
741
|
+
/** Score many IP addresses, chunked by {@link BATCH_MAX}. */
|
|
742
|
+
async checkIps(ips, options = {}) {
|
|
743
|
+
return this.#batch("/check/batch", "ips", "ip", ips, ipOptions(options));
|
|
744
|
+
}
|
|
745
|
+
/** Score many email addresses, chunked by {@link BATCH_MAX}. */
|
|
746
|
+
async checkEmails(emails) {
|
|
747
|
+
return this.#batch("/check/email/batch", "emails", "email", emails, {});
|
|
748
|
+
}
|
|
749
|
+
/** Score many phone numbers, chunked by {@link BATCH_MAX}. */
|
|
750
|
+
async checkPhones(phones, options = {}) {
|
|
751
|
+
const code = country(options.country);
|
|
752
|
+
const extra = code !== void 0 ? { country: code } : {};
|
|
753
|
+
return this.#batch("/check/phone/batch", "phones", "phone", phones, extra);
|
|
754
|
+
}
|
|
755
|
+
/** The account's remaining credits. */
|
|
756
|
+
async balance() {
|
|
757
|
+
const envelope = await this.#transport.get("/balance");
|
|
758
|
+
const credits = coerceInt(envelope.credits_remaining);
|
|
759
|
+
if (credits === null) {
|
|
760
|
+
throw new UnexpectedResponseError(200, "Missing credits_remaining", envelope);
|
|
761
|
+
}
|
|
762
|
+
return credits;
|
|
763
|
+
}
|
|
764
|
+
/** The caller's public IP address as the API sees it (free, unauthenticated). */
|
|
765
|
+
async myIp() {
|
|
766
|
+
const envelope = await this.#transport.get("/my-ip", null, false);
|
|
767
|
+
const ip = envelope.ip;
|
|
768
|
+
if (typeof ip !== "string" || ip === "") {
|
|
769
|
+
throw new UnexpectedResponseError(200, "Missing ip", envelope);
|
|
770
|
+
}
|
|
771
|
+
return ip;
|
|
772
|
+
}
|
|
773
|
+
async #batch(path, field, kind, subjects, extra) {
|
|
774
|
+
if (!Array.isArray(subjects)) {
|
|
775
|
+
throw new InvalidArgumentError(`${field} must be an array of values`);
|
|
776
|
+
}
|
|
777
|
+
if (subjects.length === 0) {
|
|
778
|
+
throw new InvalidArgumentError(`${field} must not be empty`);
|
|
779
|
+
}
|
|
780
|
+
const cleaned = subjects.map((item, index) => {
|
|
781
|
+
if (item !== null && typeof item !== "string") {
|
|
782
|
+
throw new InvalidArgumentError(`${field}[${index}] must be a string or null`);
|
|
783
|
+
}
|
|
784
|
+
return item === null ? null : item.trim();
|
|
785
|
+
});
|
|
786
|
+
const items = [];
|
|
787
|
+
let credits = 0;
|
|
788
|
+
for (let start = 0; start < cleaned.length; start += BATCH_MAX) {
|
|
789
|
+
const chunk = cleaned.slice(start, start + BATCH_MAX);
|
|
790
|
+
const envelope = await this.#transport.post(path, { [field]: chunk, ...extra });
|
|
791
|
+
if (!Array.isArray(envelope.results)) {
|
|
792
|
+
throw new UnexpectedResponseError(200, "Missing results", envelope);
|
|
793
|
+
}
|
|
794
|
+
const part = BatchResult.fromEnvelope(envelope, kind);
|
|
795
|
+
items.push(...part.items);
|
|
796
|
+
credits = part.creditsRemaining;
|
|
797
|
+
const rest = cleaned.slice(start + BATCH_MAX);
|
|
798
|
+
if (rest.length > 0 && coerceInt(envelope.credits_remaining) === 0) {
|
|
799
|
+
for (const item of rest) {
|
|
800
|
+
items.push(new BatchItem(item ?? "", false, null, "insufficient_credits"));
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return new BatchResult(items, credits);
|
|
806
|
+
}
|
|
807
|
+
toString() {
|
|
808
|
+
return `FraudNova { apiKey: "${maskApiKey(this.#apiKey)}", baseUrl: "${this.baseUrl}" }`;
|
|
809
|
+
}
|
|
810
|
+
[inspectSymbol]() {
|
|
811
|
+
return this.toString();
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
815
|
+
0 && (module.exports = {
|
|
816
|
+
AccountSuspendedError,
|
|
817
|
+
ApiError,
|
|
818
|
+
AuthenticationError,
|
|
819
|
+
BATCH_MAX,
|
|
820
|
+
BatchItem,
|
|
821
|
+
BatchResult,
|
|
822
|
+
DEFAULT_BASE_URL,
|
|
823
|
+
EmailResult,
|
|
824
|
+
FraudNova,
|
|
825
|
+
FraudNovaError,
|
|
826
|
+
InsufficientCreditsError,
|
|
827
|
+
InvalidArgumentError,
|
|
828
|
+
IpResult,
|
|
829
|
+
PhoneResult,
|
|
830
|
+
RateLimitError,
|
|
831
|
+
RetryPolicy,
|
|
832
|
+
TransportError,
|
|
833
|
+
UnexpectedResponseError,
|
|
834
|
+
UpstreamError,
|
|
835
|
+
VERSION,
|
|
836
|
+
ValidationError,
|
|
837
|
+
maskApiKey,
|
|
838
|
+
parseDateInfo,
|
|
839
|
+
parseRetryAfter
|
|
840
|
+
});
|
|
841
|
+
//# sourceMappingURL=index.cjs.map
|