@fraudnova/sdk 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FraudNova
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,317 @@
1
- # Temporary Holding Version
1
+ # @fraudnova/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [FraudNova](https://fraudnova.com) is a fraud-detection API. It returns a fraud score and risk flags for any IP address, email address or phone number, so your application can decide whether to accept, review or block a signup, lead or order. This is the official Node.js client.
4
+
5
+ [![CI](https://github.com/dahleedam/fraudnova-node/actions/workflows/ci.yml/badge.svg)](https://github.com/dahleedam/fraudnova-node/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/%40fraudnova%2Fsdk)](https://www.npmjs.com/package/@fraudnova/sdk)
7
+ [![Node versions](https://img.shields.io/node/v/%40fraudnova%2Fsdk)](https://www.npmjs.com/package/@fraudnova/sdk)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
9
+
10
+ ## Requirements
11
+
12
+ - Node.js 18 or later
13
+ - No runtime dependencies: the client uses the built-in `fetch`
14
+ - ESM and CommonJS are both supported, and TypeScript types are included
15
+ - Server-side only: the client sends your API key, so never use it in browser code. Browsers are not supported.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @fraudnova/sdk
21
+ ```
22
+
23
+ You need an API key. [Register](https://fraudnova.com/register) to get free trial credits, then create a key at [fraudnova.com/dashboard/api-keys](https://fraudnova.com/dashboard/api-keys). The key starts with `fn_` and is shown once, so copy it right away.
24
+
25
+ ## Quickstart
26
+
27
+ ```js
28
+ import { FraudNova } from "@fraudnova/sdk";
29
+
30
+ const client = new FraudNova("fn_your_api_key");
31
+ const result = await client.checkIp("8.8.8.8");
32
+ if (result.flagged()) {
33
+ // review or block
34
+ }
35
+ console.log(result.fraudScore);
36
+ ```
37
+
38
+ With CommonJS, load the client with `require`:
39
+
40
+ ```js
41
+ const { FraudNova } = require("@fraudnova/sdk");
42
+ ```
43
+
44
+ ## Checking IP addresses, emails and phone numbers
45
+
46
+ ```js
47
+ const client = new FraudNova("fn_your_api_key");
48
+
49
+ const ip = await client.checkIp("8.8.8.8");
50
+ const email = await client.checkEmail("someone@example.com");
51
+ const phone = await client.checkPhone("+1 817 679 1808");
52
+ const hinted = await client.checkPhone("8176791808", { country: "US" }); // optional two-letter country hint
53
+ ```
54
+
55
+ `checkIp()` takes an optional options object. Leave an option out to use the API default.
56
+
57
+ ```js
58
+ const result = await client.checkIp(visitorIp, {
59
+ strictness: 1, // 0 = lenient, 3 = strict
60
+ mobile: false, // the request comes from a mobile device
61
+ userAgent: req.headers["user-agent"], // enables the device and browser fields
62
+ userLanguage: req.headers["accept-language"]?.split(",")[0]?.trim() || undefined,
63
+ });
64
+ ```
65
+
66
+ `userLanguage` takes a short language tag such as `en-US`, not the whole `Accept-Language` header, so the example sends only the first tag.
67
+
68
+ An empty value, or a `strictness` that is not an integer from 0 to 3, throws `InvalidArgumentError` before any request is sent.
69
+
70
+ ## Batch checks
71
+
72
+ `checkIps()`, `checkEmails()` and `checkPhones()` score an array of values. The array can be any length: the client splits it into requests of 100 (`BATCH_MAX`) and joins the answers in input order.
73
+
74
+ ```js
75
+ const batch = await client.checkIps(ips);
76
+
77
+ for (const item of batch.successful()) {
78
+ console.log(item.subject, item.result.fraudScore);
79
+ }
80
+
81
+ for (const item of batch.failed()) {
82
+ console.log(item.subject, "failed:", item.error);
83
+ }
84
+
85
+ console.log("Credits left:", batch.creditsRemaining);
86
+ ```
87
+
88
+ `checkIps()` takes the same options as `checkIp()`, and `checkPhones()` takes one `country` hint for the whole batch. A `BatchResult` is iterable with `for...of`, has a `length`, and keeps every item in `items`; each `BatchItem` has `subject`, `success`, `result` (an `IpResult`, `EmailResult` or `PhoneResult`, or `null` on failure) and `error`. `hasFailures()` tells you whether any item failed.
89
+
90
+ Batches are split into requests of 100. When a request reports zero credits remaining, the client stops and marks the remaining items `insufficient_credits` instead of sending a request that would fail. Any other error on a later request (a 429 with no retry policy or with the retries used up, a 503, or a `TransportError`) is thrown, and the results of the earlier requests are discarded even though they were billed. A large array is sent as sequential requests of 100, so 10,000 items means 100 requests; use a `RetryPolicy` for large batches, because a 429 partway through without retries discards the earlier, already-billed results. If you need control over each request, slice the array to 100 yourself:
91
+
92
+ ```js
93
+ import { BATCH_MAX } from "@fraudnova/sdk";
94
+
95
+ for (let start = 0; start < ips.length; start += BATCH_MAX) {
96
+ const batch = await client.checkIps(ips.slice(start, start + BATCH_MAX));
97
+ // handle this request's result before sending the next
98
+ }
99
+ ```
100
+
101
+ Items must be strings or `null`; any other type throws `InvalidArgumentError` before the request is sent. One bad value never fails the batch, so an empty string or `null` comes back as a failed item. A failed item carries one of these error codes:
102
+
103
+ | `error` | Helper | Meaning |
104
+ | ---------------------------------------------- | ------------------------- | ------------------------------------------------------------------- |
105
+ | `invalid_ip`, `invalid_email`, `invalid_phone` | `isInvalid()` | The value is not a valid IP address, email address or phone number. |
106
+ | `insufficient_credits` | `isInsufficientCredits()` | Your balance ran out partway through the batch. |
107
+ | `upstream_failed` | `isUpstreamFailed()` | The lookup failed. The item was not charged. |
108
+
109
+ ## Results
110
+
111
+ `checkIp()`, `checkEmail()` and `checkPhone()` resolve to an `IpResult`, `EmailResult` or `PhoneResult`. Each has the API's fields as camelCase properties, for example `fraudScore`, `recentAbuse` and `countryCode`. `fraudScore` is always a number (0 when the API does not return one); every other field can be `null`, because the API does not return every field for every check.
112
+
113
+ ```js
114
+ const ip = await client.checkIp("8.8.8.8");
115
+ ip.proxy; // boolean | null
116
+ ip.vpn; // boolean | null
117
+ ip.countryCode; // string | null, for example "US"
118
+ ip.isp; // string | null (API field ISP)
119
+ ip.asn; // number | null (API field ASN)
120
+
121
+ const email = await client.checkEmail("someone@example.com");
122
+ email.disposable; // boolean | null
123
+ email.deliverability; // string | null
124
+ email.firstSeen; // DateInfo | null, also domainAge
125
+
126
+ const phone = await client.checkPhone("+18176791808");
127
+ phone.voip; // boolean | null (API field VOIP)
128
+ phone.lineType; // string | null
129
+ phone.carrier; // string | null
130
+ ```
131
+
132
+ The email dates `firstSeen` and `domainAge` are `DateInfo` objects with `human`, `timestamp` and `iso`.
133
+
134
+ `raw` holds the full API response for that check, with the original field names, including any field the client does not map to a property. `toJSON()` returns the same object, so `JSON.stringify(result)` gives you the API's own fields:
135
+
136
+ ```js
137
+ const result = await client.checkIp("8.8.8.8");
138
+ result.raw; // { fraud_score: 20, proxy: false, ISP: "Google", ... }
139
+ JSON.stringify(result); // '{"fraud_score":20,"proxy":false,"ISP":"Google",...}'
140
+ ```
141
+
142
+ `creditsRemaining` holds your balance after the check. It is `null` on results inside a batch; read `BatchResult.creditsRemaining` there instead.
143
+
144
+ ## The `flagged()` helper
145
+
146
+ `flagged()` returns `true` when the `fraud_score` is at or above a risk threshold (default 75), or when any of a set of fields is `true`. `flagReasons()` lists which of those fired, for example `["fraud_score", "proxy"]`. Both take the same optional options object, `{ riskThreshold, flagOn }`.
147
+
148
+ | Check | Fields checked by default |
149
+ | ------------- | ------------------------- |
150
+ | IP address | `proxy`, `vpn`, `tor` |
151
+ | Email address | `disposable` |
152
+ | Phone number | `VOIP`, `risky` |
153
+
154
+ Pass your own threshold and fields with the raw API field names (`VOIP`, not `voip`):
155
+
156
+ ```js
157
+ const result = await client.checkPhone("+18176791808");
158
+ const rules = { riskThreshold: 85, flagOn: ["VOIP", "risky", "recent_abuse"] };
159
+
160
+ if (result.flagged(rules)) {
161
+ const reasons = result.flagReasons(rules);
162
+ // ...
163
+ }
164
+ ```
165
+
166
+ The defaults are available as `IpResult.DEFAULT_FLAG_ON`, `EmailResult.DEFAULT_FLAG_ON` and `PhoneResult.DEFAULT_FLAG_ON`.
167
+
168
+ ## Balance and your IP
169
+
170
+ ```js
171
+ const credits = await client.balance(); // number, costs no credits
172
+ const myAddress = await client.myIp(); // string, the public IP your server calls from; no API key is sent
173
+ ```
174
+
175
+ ## Errors
176
+
177
+ Every error the client throws is a `FraudNovaError`, so one `instanceof` check handles them all. Each error class also sets `name`, for example `"RateLimitError"`.
178
+
179
+ ```js
180
+ import { FraudNovaError, InsufficientCreditsError } from "@fraudnova/sdk";
181
+
182
+ try {
183
+ const result = await client.checkIp(visitorIp);
184
+ } catch (error) {
185
+ if (error instanceof InsufficientCreditsError) {
186
+ // buy a credit pack in the FraudNova dashboard
187
+ } else if (error instanceof FraudNovaError) {
188
+ // log it and let the request through, or hold it for review
189
+ } else {
190
+ throw error;
191
+ }
192
+ }
193
+ ```
194
+
195
+ | Error | Status | When |
196
+ | -------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
197
+ | `AuthenticationError` | 401 | The API key is missing, invalid or revoked. |
198
+ | `InsufficientCreditsError` | 402 | Your balance is too low for the check. |
199
+ | `AccountSuspendedError` | 403 | The account is suspended. |
200
+ | `ValidationError` | 422 | The API rejected the request parameters. |
201
+ | `RateLimitError` | 429 | Too many requests, or too many checks in flight at once. `retryAfter` (seconds, or `null`) and `isConcurrencyLimit` tell you which. |
202
+ | `UpstreamError` | 502, 503, 504 | The lookup failed. On a 503 the credit is refunded automatically and `creditRefunded` is `true`. |
203
+ | `UnexpectedResponseError` | 2xx | The response could not be read as a FraudNova response. |
204
+ | `ApiError` | any other error status | Base class of every error with a status. `statusCode`, `apiMessage` and `body` are on all of them. A 401, 402, 403 or 422 whose body is not a FraudNova JSON response (for example a proxy error page) is a plain `ApiError`. |
205
+ | `TransportError` | none | The request failed before a response arrived, for example a timeout or DNS error. The original error is `cause`. |
206
+ | `InvalidArgumentError` | none | A value was empty or out of range; no request was sent. |
207
+
208
+ `TransportError.cause` is the original error from `fetch` and is not masked, so avoid logging it verbatim if your fetch wrapper could echo request headers.
209
+
210
+ ## Retries
211
+
212
+ The client does not retry by default. To retry HTTP 429 responses, pass a `RetryPolicy`:
213
+
214
+ ```js
215
+ import { FraudNova, RetryPolicy } from "@fraudnova/sdk";
216
+
217
+ const client = new FraudNova("fn_your_api_key", {
218
+ retry: new RetryPolicy({ maxAttempts: 3 }),
219
+ });
220
+ ```
221
+
222
+ `maxAttempts` counts the first attempt, so `3` means up to two retries. Only 429 is retried; every other error is thrown right away. The wait is the `Retry-After` header when the response has one, otherwise a short schedule for the concurrency limit (`concurrencyDelays`, default `[1, 2, 4]`) and a longer one for the rate limit (`rateLimitDelays`, default `[5, 15, 40]`). All `RetryPolicy` values are in seconds. When the attempts run out, the `RateLimitError` is thrown. A `Retry-After` longer than `maxRetryAfter` (60 seconds by default) is not waited for; the `RateLimitError` is thrown right away. Set `honourRetryAfter: false` to always use the schedules. In your own tests, pass `sleep: async () => {}` so retries do not wait. Batch requests are safe to retry, because a 429 is returned before any item is checked.
223
+
224
+ Retries can make one call slow. With `new RetryPolicy({ maxAttempts: 3 })` and the default `maxRetryAfter` of 60, one call can sleep for up to about two minutes, plus three timeouts. Inside a web request, use a small `maxRetryAfter` (for example `2`) and a short `timeout`, and keep the full policy for background jobs.
225
+
226
+ ## Bring your own fetch
227
+
228
+ The client calls the global `fetch` with a 30 second timeout per request. Change the timeout with the `timeout` option, in milliseconds:
229
+
230
+ ```js
231
+ const client = new FraudNova("fn_your_api_key", { timeout: 5_000 });
232
+ ```
233
+
234
+ Proxies, custom TLS and connection pooling belong to the HTTP layer, so pass your own `fetch` function. For example, to send requests through a proxy with [undici](https://undici.nodejs.org/) (install it yourself with `npm install undici`; it is not a dependency of this package):
235
+
236
+ ```js
237
+ import { FraudNova } from "@fraudnova/sdk";
238
+ import { ProxyAgent, fetch as undiciFetch } from "undici";
239
+
240
+ const dispatcher = new ProxyAgent("http://proxy.internal:3128");
241
+
242
+ const client = new FraudNova("fn_your_api_key", {
243
+ fetch: (url, init) => undiciFetch(url, { ...init, dispatcher }),
244
+ timeout: 10_000,
245
+ });
246
+ ```
247
+
248
+ The `fetch` option takes any function with the signature `(url: string, init: RequestInit) => Promise<Response>` (the `FetchLike` type). The client still applies its own `timeout` to every call. The SDK sets its own `signal` for the timeout (a caller-supplied `AbortSignal` is not supported in 1.0), so a custom `fetch` must honour `init.signal` for the timeout to work. Use `baseUrl` to point the client somewhere other than `https://fraudnova.com/api/v1` (`DEFAULT_BASE_URL`).
249
+
250
+ ## Testing
251
+
252
+ Inject a fake `fetch`, so your tests send no real request:
253
+
254
+ ```js
255
+ import assert from "node:assert/strict";
256
+ import { FraudNova } from "@fraudnova/sdk";
257
+
258
+ const fakeFetch = async () =>
259
+ new Response(
260
+ JSON.stringify({
261
+ success: true,
262
+ data: { fraud_score: 12, proxy: false },
263
+ credits_remaining: 99,
264
+ }),
265
+ { status: 200 },
266
+ );
267
+
268
+ const client = new FraudNova("fn_test", { fetch: fakeFetch });
269
+ const result = await client.checkIp("8.8.8.8");
270
+
271
+ assert.equal(result.fraudScore, 12);
272
+ assert.equal(result.creditsRemaining, 99);
273
+ ```
274
+
275
+ ## TypeScript
276
+
277
+ The package ships its own type definitions for both ESM and CommonJS; no `@types` package is needed. Besides the classes (`FraudNova`, `RetryPolicy`, `IpResult`, `EmailResult`, `PhoneResult`, `BatchResult`, `BatchItem` and every error class), it exports these types:
278
+
279
+ | Type | Describes |
280
+ | -------------------- | ----------------------------------------------------------- |
281
+ | `FraudNovaOptions` | The client options: `baseUrl`, `timeout`, `retry`, `fetch`. |
282
+ | `IpCheckOptions` | `checkIp()` and `checkIps()` options. |
283
+ | `PhoneCheckOptions` | `checkPhone()` and `checkPhones()` options (`country`). |
284
+ | `FetchLike` | The `fetch` function the client accepts. |
285
+ | `RetryPolicyOptions` | The `RetryPolicy` constructor options. |
286
+ | `FlagOptions` | The `flagged()` and `flagReasons()` options. |
287
+ | `DateInfo` | The `firstSeen` and `domainAge` email dates. |
288
+ | `Result` | `IpResult \| EmailResult \| PhoneResult`. |
289
+ | `CheckKind` | `"ip" \| "email" \| "phone"`. |
290
+
291
+ ```ts
292
+ import { FraudNova, type IpCheckOptions, type IpResult } from "@fraudnova/sdk";
293
+
294
+ const client = new FraudNova(process.env.FRAUDNOVA_API_KEY ?? "");
295
+ const options: IpCheckOptions = { strictness: 1 };
296
+ const result: IpResult = await client.checkIp("8.8.8.8", options);
297
+ ```
298
+
299
+ The package also exports `VERSION` (the SDK version, sent in the `User-Agent` header), `BATCH_MAX`, `DEFAULT_BASE_URL`, and three small helpers: `maskApiKey()` (a shortened key such as `fn_…abcd` that is safe to log), `parseRetryAfter()` and `parseDateInfo()`.
300
+
301
+ ## Credits and limits
302
+
303
+ One check costs one credit, whichever type it is. Failed checks are refunded. You get free trial credits when you verify your email address, and you buy more as credit packs, with no subscription. A batch costs one credit per successful item.
304
+
305
+ The API limits requests per minute for each key and checks in flight for each account. The current limits are in the [API documentation](https://fraudnova.com/docs).
306
+
307
+ ## Resources
308
+
309
+ - [FraudNova API documentation](https://fraudnova.com/docs)
310
+ - [Examples repository](https://github.com/dahleedam/fraudnova-examples)
311
+ - [Status](https://fraudnova.com/status)
312
+ - [Changelog](https://fraudnova.com/changelog)
313
+ - Support: [support@fraudnova.com](mailto:support@fraudnova.com)
314
+
315
+ ## Licence
316
+
317
+ Released under the MIT license. See [LICENSE](LICENSE).