@sentinelsup/sdk 0.1.2 → 0.2.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 +21 -21
- package/README.md +248 -180
- package/index.d.ts +180 -101
- package/index.js +198 -148
- package/package.json +42 -42
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Sentinel Edge Networks LTD
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sentinel Edge Networks LTD
|
|
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,180 +1,248 @@
|
|
|
1
|
-
# @sentinelsup/sdk
|
|
2
|
-
|
|
3
|
-
Official Node.js SDK for [Sentinel](https://sntlhq.com) — real-time fraud detection that flags VPNs, residential proxies, antidetect browsers, and AI bots in under 40 ms.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@sentinelsup/sdk)
|
|
6
|
-
[ — real-time fraud detection that flags VPNs, residential proxies, antidetect browsers, and AI bots in under 40 ms.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@sentinelsup/sdk)
|
|
6
|
+
[](https://www.npmjs.com/package/@sentinelsup/sdk)
|
|
7
|
+
[](./index.d.ts)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
## Set up with AI (fastest)
|
|
11
|
+
|
|
12
|
+
Using Claude Code, Cursor, Copilot, or any AI coding assistant? Paste this
|
|
13
|
+
one prompt and it wires the whole integration — frontend script, backend
|
|
14
|
+
check, env var, and a test:
|
|
15
|
+
|
|
16
|
+
> Fetch https://sntlhq.com/integrate.md and follow it to add Sentinel fraud
|
|
17
|
+
> protection to this app — protect signup, login, and checkout. My API key
|
|
18
|
+
> is sk_live_YOUR_KEY; put it in a SENTINEL_KEY env var, never in
|
|
19
|
+
> client-side code. Then show me how to test it.
|
|
20
|
+
|
|
21
|
+
[`integrate.md`](https://sntlhq.com/integrate.md) is the canonical
|
|
22
|
+
machine-readable integration guide, kept in sync with the live API.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @sentinelsup/sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Zero dependencies. Works on Node 14+, Bun, Deno, Cloudflare Workers, and Vercel Edge (wherever `fetch` exists).
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const Sentinel = require('@sentinelsup/sdk');
|
|
36
|
+
|
|
37
|
+
const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
|
|
38
|
+
|
|
39
|
+
const result = await sentinel.evaluate({
|
|
40
|
+
token: req.body.sentinelToken // from the frontend SDK
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result.decision === 'block') {
|
|
44
|
+
return res.status(403).json({ error: 'blocked' });
|
|
45
|
+
}
|
|
46
|
+
// 'review' → let through but flag; 'allow' → clean
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Get a free API key (no credit card) at [sntlhq.com/signup](https://sntlhq.com/signup).
|
|
50
|
+
|
|
51
|
+
## What you get back
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
{
|
|
55
|
+
decision: 'review', // 'allow' | 'review' | 'block' — route on this
|
|
56
|
+
risk_score: 65, // 0–100
|
|
57
|
+
isSuspicious: true, // simple boolean verdict
|
|
58
|
+
ip: '198.51.100.18',
|
|
59
|
+
country: 'NL',
|
|
60
|
+
network: {
|
|
61
|
+
vpn: true, proxy: false, datacenter: true, anonymous: true,
|
|
62
|
+
tor: false, residential: false, service: 'PROTON_VPN'
|
|
63
|
+
},
|
|
64
|
+
device: { // present only when you pass fingerprintEventId
|
|
65
|
+
antidetect: false, // antidetect browser detected
|
|
66
|
+
automation: false, // bot / browser automation
|
|
67
|
+
emulator: false, virtual_machine: false, incognito: false,
|
|
68
|
+
ip_blocklisted: false, visitor_id: 'abc123', tampering_score: 0
|
|
69
|
+
},
|
|
70
|
+
reasons: ['vpn_detected', 'datacenter_asn'], // machine-readable codes
|
|
71
|
+
evaluated_in_ms: 28
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Try the live sample (same shape, no key needed):
|
|
76
|
+
`curl "https://sntlhq.com/v1/evaluate/sample?scenario=vpn"`
|
|
77
|
+
|
|
78
|
+
Legacy `details` / `deviceIntel` fields are still returned for backwards
|
|
79
|
+
compatibility with 0.1.0 integrations.
|
|
80
|
+
|
|
81
|
+
## Frontend setup
|
|
82
|
+
|
|
83
|
+
Add the Sentinel SDK to your frontend. One script loads **both** layers —
|
|
84
|
+
network (VPN/proxy/datacenter) and device (antidetect/bot/tampering):
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<script async src="https://sntlhq.com/assets/sentinel.js"></script>
|
|
88
|
+
|
|
89
|
+
<!-- Add class="monocle-enriched" to any form you want evaluated -->
|
|
90
|
+
<form class="monocle-enriched" id="checkout-form">
|
|
91
|
+
<!-- The SDK injects both:
|
|
92
|
+
<input type="hidden" name="monocle" value="eyJ..."> (network)
|
|
93
|
+
<input type="hidden" name="sentinel_fp" value="a1b2..."> (device) -->
|
|
94
|
+
</form>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Collect both and send them to your backend:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const { token, fingerprintEventId } = await window.Sentinel.collect();
|
|
101
|
+
fetch('/checkout', { method: 'POST', body: JSON.stringify({ token, fingerprintEventId }) });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Examples
|
|
105
|
+
|
|
106
|
+
### Stripe Checkout — block card testing
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const Sentinel = require('@sentinelsup/sdk');
|
|
110
|
+
const stripe = require('stripe')(process.env.STRIPE_KEY);
|
|
111
|
+
const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
|
|
112
|
+
|
|
113
|
+
app.post('/checkout', async (req, res) => {
|
|
114
|
+
const { isSuspicious } = await sentinel.evaluate({ token: req.body.token, fingerprintEventId: req.body.fingerprintEventId });
|
|
115
|
+
if (isSuspicious) return res.status(403).json({ error: 'declined' });
|
|
116
|
+
|
|
117
|
+
const intent = await stripe.paymentIntents.create({ /* ... */ });
|
|
118
|
+
res.json({ clientSecret: intent.client_secret });
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Signup — block fake Google sign-ins
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
app.post('/auth/google', async (req, res) => {
|
|
126
|
+
const { credential, sentinelToken } = req.body;
|
|
127
|
+
const ticket = await googleClient.verifyIdToken({ idToken: credential });
|
|
128
|
+
|
|
129
|
+
const result = await sentinel.evaluate({ token: sentinelToken });
|
|
130
|
+
if (result.isSuspicious) return res.status(403).json({ error: 'signup_blocked' });
|
|
131
|
+
|
|
132
|
+
await createUser(ticket.getPayload().email, result.deviceIntel?.visitorId);
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Custom policy with `shouldBlock`
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
// Only block when we see both residential proxy AND an antidetect browser
|
|
140
|
+
const blocked = await sentinel.shouldBlock(
|
|
141
|
+
{ token },
|
|
142
|
+
r => r.network.proxy && r.device?.antidetect
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Burner-email check at signup
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// Pass the signup email and Sentinel checks it against a continuously
|
|
150
|
+
// refreshed disposable-domain feed. A hit adds the disposable_email
|
|
151
|
+
// reason, raises risk_score, and escalates allow → review. The address
|
|
152
|
+
// is checked transiently — never stored or logged.
|
|
153
|
+
const result = await sentinel.evaluate({ token, email: req.body.email });
|
|
154
|
+
if (result.email?.disposable) {
|
|
155
|
+
// e.g. require a real address before granting the trial
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Look up an arbitrary IP — no browser token needed
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
// Batch scoring, log enrichment, server-side screening. Same key,
|
|
163
|
+
// same hourly quota as evaluate().
|
|
164
|
+
const info = await sentinel.lookup('185.220.101.34');
|
|
165
|
+
// info.verdict → 'allow' | 'review' | 'block'
|
|
166
|
+
// info.risk_score → 0–100
|
|
167
|
+
// info.signals → { vpn, proxied, tor, dch, anon } (null when known:false)
|
|
168
|
+
// info.network → { asn, org, country, city }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## API
|
|
172
|
+
|
|
173
|
+
### `new Sentinel({ apiKey, endpoint?, timeoutMs? })`
|
|
174
|
+
|
|
175
|
+
| Option | Type | Default | Description |
|
|
176
|
+
|--------|------|---------|-------------|
|
|
177
|
+
| `apiKey` | string | required | Your key starting with `sk_live_` |
|
|
178
|
+
| `endpoint` | string | `https://sntlhq.com` | Override base URL |
|
|
179
|
+
| `timeoutMs` | number | `5000` | Per-request timeout |
|
|
180
|
+
|
|
181
|
+
### `sentinel.evaluate({ token, fingerprintEventId?, accountId?, email? })`
|
|
182
|
+
|
|
183
|
+
Returns `EvaluateResult`. Throws `SentinelError` on network/API failure — the error carries `.status` and `.body`.
|
|
184
|
+
|
|
185
|
+
- `fingerprintEventId` — adds the `device` signal block (antidetect, automation, emulator, …), including device history (`device.times_seen`, `device.first_seen` — ISO timestamp of the first sighting, `device.returning`).
|
|
186
|
+
- `accountId` — your own user id for this session; enables multi-accounting detection (`device.linked_accounts` / `device.multi_account`).
|
|
187
|
+
- `email` — adds `email.disposable` to the response; burner domains escalate `allow` to `review`.
|
|
188
|
+
|
|
189
|
+
### `sentinel.lookup(ip)`
|
|
190
|
+
|
|
191
|
+
Returns `LookupResponse` for any public IPv4/IPv6 address (wraps `GET /v1/lookup/{ip}`): allow/review/block verdict, 0–100 risk score, VPN/proxy/Tor/datacenter signals, and network attribution. Shares the per-key hourly quota with `evaluate()`. `known: false` means our feeds hold no data for the IP — it is **not** a clean guarantee.
|
|
192
|
+
|
|
193
|
+
### `sentinel.shouldBlock({ token, fingerprintEventId? }, predicate?)`
|
|
194
|
+
|
|
195
|
+
Convenience: runs `evaluate()` and returns a boolean. Default predicate is `r => r.isSuspicious`. Pass your own to build custom policies.
|
|
196
|
+
|
|
197
|
+
> `accountId`, `email`, and `lookup()` ship in the **next npm release (0.2.1)** — the published 0.1.2 silently ignores `accountId`/`email` — on npm today, use the raw HTTP endpoints documented at [sntlhq.com/api](https://sntlhq.com/api) until it lands.
|
|
198
|
+
|
|
199
|
+
## Testing
|
|
200
|
+
|
|
201
|
+
Deterministic test tokens exercise every decision path from a terminal — authenticated and rate-limited like real calls, but never billed, stored, or webhooked (responses carry `"test": true`):
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
await sentinel.evaluate({ token: 'test_vpn' }); // → decision: 'review'/'block' path
|
|
205
|
+
await sentinel.evaluate({ token: 'test_clean' }); // → decision: 'allow' path
|
|
206
|
+
// also: test_proxy, test_datacenter, test_tor
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- **No account yet?** The public sandbox key `sk_test_sandbox` answers the same `test_*` tokens with the same shapes — no signup, nothing stored.
|
|
210
|
+
- **CI / staging with real traffic:** every account also has a personal `sk_test_…` key (Settings → API Key) that runs the complete live pipeline — device intelligence, your rules and exception pins — but events are flagged as test, excluded from usage, and never fire webhooks. It is exempt from the account's IP allowlist.
|
|
211
|
+
|
|
212
|
+
## Rate limits
|
|
213
|
+
|
|
214
|
+
Free tier: **1,000 requests/hour** per API key (`evaluate()` and `lookup()` share the bucket). No monthly cap, no credit card. Upgrade at [sntlhq.com](https://sntlhq.com) when you need more.
|
|
215
|
+
|
|
216
|
+
On `429`, the thrown `SentinelError` has `.status === 429` — honor the `Retry-After` header and fail open (let the request through and log it) rather than blocking real users while you are throttled. Responses also carry `X-RateLimit-Limit` / `-Remaining` / `-Reset` for proactive backoff.
|
|
217
|
+
|
|
218
|
+
## TypeScript
|
|
219
|
+
|
|
220
|
+
Full types ship with the package. Importing `Sentinel` gives you the class plus `EvaluateResult`, `DeviceIntel`, `EvaluateDetails`, and `SentinelError` types.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import Sentinel, { EvaluateResult } from '@sentinelsup/sdk';
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## What Sentinel detects
|
|
227
|
+
|
|
228
|
+
VPNs (commercial + self-hosted) · residential proxies (Bright Data, IPRoyal,
|
|
229
|
+
and similar networks) · datacenter IPs · Tor exit nodes · antidetect browsers
|
|
230
|
+
(Kameleo, GoLogin, Multilogin, Dolphin{anty}, AdsPower) · headless browsers
|
|
231
|
+
and automation (Puppeteer, Playwright, Selenium) · AI agents · emulators and
|
|
232
|
+
virtual machines · browser tampering.
|
|
233
|
+
|
|
234
|
+
## Related
|
|
235
|
+
|
|
236
|
+
- **Python SDK** — [`sentinelsup`](https://github.com/sentinelsup/sentinel-python) on PyPI
|
|
237
|
+
- **Free IP lookup tool** — [sntlhq.com/ip-lookup](https://sntlhq.com/ip-lookup)
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT © Sentinel Edge Networks LTD
|
|
242
|
+
|
|
243
|
+
## Links
|
|
244
|
+
|
|
245
|
+
- Website — [sntlhq.com](https://sntlhq.com)
|
|
246
|
+
- API docs — [sntlhq.com/api](https://sntlhq.com/api)
|
|
247
|
+
- Blog — [sntlhq.com/blog](https://sntlhq.com/blog)
|
|
248
|
+
- X / Twitter — [@SentinelSup](https://x.com/SentinelSup)
|
package/index.d.ts
CHANGED
|
@@ -1,101 +1,180 @@
|
|
|
1
|
-
export interface EvaluateDetails {
|
|
2
|
-
ip: string;
|
|
3
|
-
cc: string;
|
|
4
|
-
vpn: boolean;
|
|
5
|
-
proxied: boolean;
|
|
6
|
-
dch: boolean;
|
|
7
|
-
anon: boolean;
|
|
8
|
-
crawler?: boolean;
|
|
9
|
-
service?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface DeviceIntel {
|
|
13
|
-
visitorId: string | null;
|
|
14
|
-
browserTampering: boolean;
|
|
15
|
-
botDetected: boolean;
|
|
16
|
-
vpnDetected: boolean;
|
|
17
|
-
proxyDetected: boolean;
|
|
18
|
-
torDetected: boolean;
|
|
19
|
-
ipBlocklisted: boolean;
|
|
20
|
-
incognito: boolean;
|
|
21
|
-
virtualMachine: boolean;
|
|
22
|
-
emulator: boolean;
|
|
23
|
-
tamperingScore?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface NetworkSignals {
|
|
27
|
-
vpn: boolean;
|
|
28
|
-
proxy: boolean;
|
|
29
|
-
datacenter: boolean;
|
|
30
|
-
anonymous: boolean;
|
|
31
|
-
tor: boolean;
|
|
32
|
-
residential: boolean;
|
|
33
|
-
service: string | null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface DeviceSignals {
|
|
37
|
-
antidetect: boolean;
|
|
38
|
-
automation: boolean;
|
|
39
|
-
emulator: boolean;
|
|
40
|
-
virtual_machine: boolean;
|
|
41
|
-
incognito: boolean;
|
|
42
|
-
privacy_mode: boolean;
|
|
43
|
-
ip_blocklisted: boolean;
|
|
44
|
-
visitor_id: string | null;
|
|
45
|
-
tampering_score: number;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1
|
+
export interface EvaluateDetails {
|
|
2
|
+
ip: string;
|
|
3
|
+
cc: string;
|
|
4
|
+
vpn: boolean;
|
|
5
|
+
proxied: boolean;
|
|
6
|
+
dch: boolean;
|
|
7
|
+
anon: boolean;
|
|
8
|
+
crawler?: boolean;
|
|
9
|
+
service?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DeviceIntel {
|
|
13
|
+
visitorId: string | null;
|
|
14
|
+
browserTampering: boolean;
|
|
15
|
+
botDetected: boolean;
|
|
16
|
+
vpnDetected: boolean;
|
|
17
|
+
proxyDetected: boolean;
|
|
18
|
+
torDetected: boolean;
|
|
19
|
+
ipBlocklisted: boolean;
|
|
20
|
+
incognito: boolean;
|
|
21
|
+
virtualMachine: boolean;
|
|
22
|
+
emulator: boolean;
|
|
23
|
+
tamperingScore?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NetworkSignals {
|
|
27
|
+
vpn: boolean;
|
|
28
|
+
proxy: boolean;
|
|
29
|
+
datacenter: boolean;
|
|
30
|
+
anonymous: boolean;
|
|
31
|
+
tor: boolean;
|
|
32
|
+
residential: boolean;
|
|
33
|
+
service: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DeviceSignals {
|
|
37
|
+
antidetect: boolean;
|
|
38
|
+
automation: boolean;
|
|
39
|
+
emulator: boolean;
|
|
40
|
+
virtual_machine: boolean;
|
|
41
|
+
incognito: boolean;
|
|
42
|
+
privacy_mode: boolean;
|
|
43
|
+
ip_blocklisted: boolean;
|
|
44
|
+
visitor_id: string | null;
|
|
45
|
+
tampering_score: number;
|
|
46
|
+
high_activity: boolean;
|
|
47
|
+
/** Sightings of this device for your account (present when the device is identified) */
|
|
48
|
+
times_seen?: number;
|
|
49
|
+
/** ISO 8601 timestamp of when Sentinel first saw this device (present when the device is identified) */
|
|
50
|
+
first_seen?: string;
|
|
51
|
+
returning?: boolean;
|
|
52
|
+
/** Distinct accountIds seen on this device (present when accountId was supplied) */
|
|
53
|
+
linked_accounts?: number;
|
|
54
|
+
multi_account?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ReasonCode =
|
|
58
|
+
| 'vpn_detected' | 'proxy_detected' | 'datacenter_asn' | 'tor_exit_node'
|
|
59
|
+
| 'anonymous_network' | 'antidetect_browser' | 'automation_detected'
|
|
60
|
+
| 'emulator_detected' | 'virtual_machine' | 'ip_blocklisted' | 'private_browsing'
|
|
61
|
+
| 'high_activity_device' | 'multi_account_device' | 'disposable_email';
|
|
62
|
+
|
|
63
|
+
export interface EmailSignals {
|
|
64
|
+
/** True when the address uses a disposable/burner domain */
|
|
65
|
+
disposable: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EvaluateResult {
|
|
69
|
+
/** "allow" | "review" | "block" — route on this */
|
|
70
|
+
decision: 'allow' | 'review' | 'block';
|
|
71
|
+
/** 0–100 weighted risk score */
|
|
72
|
+
risk_score: number;
|
|
73
|
+
/** Legacy convenience flag: VPN/proxy/antidetect/automation/emulator.
|
|
74
|
+
* Does NOT cover Tor or datacenter — route on `decision` instead. */
|
|
75
|
+
isSuspicious: boolean;
|
|
76
|
+
ip: string | null;
|
|
77
|
+
country: string | null;
|
|
78
|
+
network: NetworkSignals;
|
|
79
|
+
/** Present only when fingerprintEventId was supplied */
|
|
80
|
+
device?: DeviceSignals;
|
|
81
|
+
/** Machine-readable reason codes for the verdict */
|
|
82
|
+
reasons: ReasonCode[];
|
|
83
|
+
evaluated_in_ms: number;
|
|
84
|
+
/** Present when the optional `email` input was supplied */
|
|
85
|
+
email?: EmailSignals;
|
|
86
|
+
/** Present only when your own rules/exceptions changed `decision`:
|
|
87
|
+
* the engine's own risk-score verdict */
|
|
88
|
+
engine_decision?: 'allow' | 'review' | 'block';
|
|
89
|
+
/** Who authored the final decision when it was not the engine */
|
|
90
|
+
decision_source?: 'rules' | 'exception';
|
|
91
|
+
/** Signals that triggered a custom rule (decision_source === 'rules') */
|
|
92
|
+
rule_matched?: string[];
|
|
93
|
+
/** Matched per-IP/visitor pins (decision_source === 'exception') */
|
|
94
|
+
exception_matched?: string[];
|
|
95
|
+
/** Present on test-token / sk_test_ key responses — never billed */
|
|
96
|
+
test?: boolean;
|
|
97
|
+
/** Legacy fields (kept for backwards compatibility) */
|
|
98
|
+
status: string;
|
|
99
|
+
details: EvaluateDetails;
|
|
100
|
+
deviceIntel?: DeviceIntel | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface LookupSignals {
|
|
104
|
+
vpn: boolean;
|
|
105
|
+
/** Proxy detected (legacy field spelling) */
|
|
106
|
+
proxied: boolean;
|
|
107
|
+
tor: boolean;
|
|
108
|
+
/** Datacenter hosting (legacy field spelling) */
|
|
109
|
+
dch: boolean;
|
|
110
|
+
/** Anonymizing behaviors observed */
|
|
111
|
+
anon: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface LookupNetwork {
|
|
115
|
+
asn: number | null;
|
|
116
|
+
org: string | null;
|
|
117
|
+
country: string | null;
|
|
118
|
+
city: string | null;
|
|
119
|
+
/** Cloud provider name, present on a provider-range hit */
|
|
120
|
+
cloud?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface LookupResponse {
|
|
124
|
+
ip: string;
|
|
125
|
+
/** Whether our reputation feeds hold data for this IP.
|
|
126
|
+
* false does NOT assert the IP is clean. */
|
|
127
|
+
known: boolean;
|
|
128
|
+
verdict: 'allow' | 'review' | 'block';
|
|
129
|
+
/** 0–100 */
|
|
130
|
+
risk_score: number;
|
|
131
|
+
/** Null when known is false */
|
|
132
|
+
signals: LookupSignals | null;
|
|
133
|
+
network: LookupNetwork | null;
|
|
134
|
+
latency_ms: number;
|
|
135
|
+
/** Present only when an exception pin changed `verdict` */
|
|
136
|
+
engine_verdict?: 'allow' | 'review' | 'block';
|
|
137
|
+
verdict_source?: 'exception';
|
|
138
|
+
exception_matched?: string[];
|
|
139
|
+
/** Present when the call used the per-account sk_test_ key */
|
|
140
|
+
test?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SentinelOptions {
|
|
144
|
+
/** Your Sentinel API key (starts with sk_live_). Get one free at https://sntlhq.com/signup */
|
|
145
|
+
apiKey: string;
|
|
146
|
+
/** Override the API base URL (default: https://sntlhq.com) */
|
|
147
|
+
endpoint?: string;
|
|
148
|
+
/** Per-request timeout in ms (default: 5000) */
|
|
149
|
+
timeoutMs?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface EvaluateInput {
|
|
153
|
+
/** Client-side Sentinel token from the frontend SDK */
|
|
154
|
+
token: string;
|
|
155
|
+
/** Optional Fingerprint event id for device-layer signals */
|
|
156
|
+
fingerprintEventId?: string;
|
|
157
|
+
/** Optional account/user id — enables multi-accounting detection
|
|
158
|
+
* (device.linked_accounts / device.multi_account) */
|
|
159
|
+
accountId?: string;
|
|
160
|
+
/** Optional signup email — adds `email.disposable` to the response
|
|
161
|
+
* (burner domains escalate allow → review). Checked transiently,
|
|
162
|
+
* never stored or logged. */
|
|
163
|
+
email?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class SentinelError extends Error {
|
|
167
|
+
status?: number;
|
|
168
|
+
body?: unknown;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default class Sentinel {
|
|
172
|
+
constructor(opts: SentinelOptions);
|
|
173
|
+
evaluate(input: EvaluateInput): Promise<EvaluateResult>;
|
|
174
|
+
/** Look up an arbitrary public IP address (GET /v1/lookup/{ip}).
|
|
175
|
+
* Shares the per-key hourly quota with evaluate(). */
|
|
176
|
+
lookup(ip: string): Promise<LookupResponse>;
|
|
177
|
+
shouldBlock(input: EvaluateInput, predicate?: (r: EvaluateResult) => boolean): Promise<boolean>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { Sentinel };
|
package/index.js
CHANGED
|
@@ -1,148 +1,198 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sentinel Node.js SDK — thin, zero-dependency wrapper around the
|
|
3
|
-
* Sentinel fraud detection API at https://sntlhq.com/v1/evaluate.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* const Sentinel = require('@sentinelsup/sdk');
|
|
7
|
-
* const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
|
|
8
|
-
* const result = await sentinel.evaluate({ token });
|
|
9
|
-
* if (result.decision === 'block') return res.status(403).end();
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const DEFAULT_ENDPOINT = 'https://sntlhq.com';
|
|
13
|
-
|
|
14
|
-
class SentinelError extends Error {
|
|
15
|
-
constructor(message, { status, body } = {}) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = 'SentinelError';
|
|
18
|
-
this.status = status;
|
|
19
|
-
this.body = body;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
class Sentinel {
|
|
24
|
-
/**
|
|
25
|
-
* @param {object} opts
|
|
26
|
-
* @param {string} opts.apiKey — your Sentinel API key (starts with sk_live_)
|
|
27
|
-
* @param {string} [opts.endpoint] — override the default API base URL (for testing)
|
|
28
|
-
* @param {number} [opts.timeoutMs=5000] — per-request timeout
|
|
29
|
-
*/
|
|
30
|
-
constructor(opts) {
|
|
31
|
-
if (!opts || typeof opts.apiKey !== 'string' || !opts.apiKey) {
|
|
32
|
-
throw new SentinelError('Sentinel: apiKey is required. Get one free at https://sntlhq.com/signup');
|
|
33
|
-
}
|
|
34
|
-
this.apiKey = opts.apiKey;
|
|
35
|
-
this.endpoint = (opts.endpoint || DEFAULT_ENDPOINT).replace(/\/$/, '');
|
|
36
|
-
this.timeoutMs = opts.timeoutMs || 5000;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
throw new SentinelError(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* @
|
|
140
|
-
* @property {
|
|
141
|
-
* @property {
|
|
142
|
-
* @property {boolean}
|
|
143
|
-
* @property {
|
|
144
|
-
* @property {
|
|
145
|
-
* @property {
|
|
146
|
-
* @property {
|
|
147
|
-
* @property {
|
|
148
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel Node.js SDK — thin, zero-dependency wrapper around the
|
|
3
|
+
* Sentinel fraud detection API at https://sntlhq.com/v1/evaluate.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const Sentinel = require('@sentinelsup/sdk');
|
|
7
|
+
* const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
|
|
8
|
+
* const result = await sentinel.evaluate({ token });
|
|
9
|
+
* if (result.decision === 'block') return res.status(403).end();
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ENDPOINT = 'https://sntlhq.com';
|
|
13
|
+
|
|
14
|
+
class SentinelError extends Error {
|
|
15
|
+
constructor(message, { status, body } = {}) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'SentinelError';
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.body = body;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class Sentinel {
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {string} opts.apiKey — your Sentinel API key (starts with sk_live_)
|
|
27
|
+
* @param {string} [opts.endpoint] — override the default API base URL (for testing)
|
|
28
|
+
* @param {number} [opts.timeoutMs=5000] — per-request timeout
|
|
29
|
+
*/
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
if (!opts || typeof opts.apiKey !== 'string' || !opts.apiKey) {
|
|
32
|
+
throw new SentinelError('Sentinel: apiKey is required. Get one free at https://sntlhq.com/signup');
|
|
33
|
+
}
|
|
34
|
+
this.apiKey = opts.apiKey;
|
|
35
|
+
this.endpoint = (opts.endpoint || DEFAULT_ENDPOINT).replace(/\/$/, '');
|
|
36
|
+
this.timeoutMs = opts.timeoutMs || 5000;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shared transport: auth header, timeout, JSON parsing, error mapping.
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
async _request(path, init) {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const abortTimer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
46
|
+
|
|
47
|
+
let res;
|
|
48
|
+
try {
|
|
49
|
+
res = await fetch(`${this.endpoint}${path}`, {
|
|
50
|
+
...init,
|
|
51
|
+
headers: {
|
|
52
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
53
|
+
...(init && init.headers)
|
|
54
|
+
},
|
|
55
|
+
signal: controller.signal
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.name === 'AbortError') {
|
|
59
|
+
throw new SentinelError(`Sentinel: request timed out after ${this.timeoutMs}ms`);
|
|
60
|
+
}
|
|
61
|
+
throw new SentinelError(`Sentinel: network error — ${err.message}`);
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(abortTimer);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let json;
|
|
67
|
+
try { json = await res.json(); } catch { json = null; }
|
|
68
|
+
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new SentinelError(
|
|
71
|
+
`Sentinel: API returned ${res.status}${json && json.error ? ` — ${json.error}` : ''}`,
|
|
72
|
+
{ status: res.status, body: json }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return json;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Evaluate a visitor session for fraud signals.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} input
|
|
83
|
+
* @param {string} input.token — Sentinel client-side token from the frontend SDK
|
|
84
|
+
* @param {string} [input.fingerprintEventId] — optional Fingerprint event id for device signals
|
|
85
|
+
* @param {string} [input.accountId] — optional account/user id for multi-accounting detection
|
|
86
|
+
* @param {string} [input.email] — optional signup email; adds `email.disposable` to the
|
|
87
|
+
* response (burner domains escalate allow → review). Checked transiently, never stored.
|
|
88
|
+
* @returns {Promise<EvaluateResult>}
|
|
89
|
+
*/
|
|
90
|
+
async evaluate({ token, fingerprintEventId, accountId, email } = {}) {
|
|
91
|
+
if (!token || typeof token !== 'string') {
|
|
92
|
+
throw new SentinelError('Sentinel.evaluate: token (client-side Sentinel token) is required');
|
|
93
|
+
}
|
|
94
|
+
return this._request('/v1/evaluate', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({ token, fingerprintEventId, accountId, email })
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Look up an arbitrary public IP address — no browser token needed.
|
|
103
|
+
* Wraps GET /v1/lookup/{ip}: allow/review/block verdict, 0–100 risk
|
|
104
|
+
* score, VPN/proxy/Tor/datacenter signals, and network attribution.
|
|
105
|
+
* Shares the per-key hourly quota with evaluate().
|
|
106
|
+
*
|
|
107
|
+
* @param {string} ip — public IPv4 or IPv6 address, e.g. '185.220.101.34'
|
|
108
|
+
* @returns {Promise<LookupResponse>}
|
|
109
|
+
*/
|
|
110
|
+
async lookup(ip) {
|
|
111
|
+
if (!ip || typeof ip !== 'string') {
|
|
112
|
+
throw new SentinelError('Sentinel.lookup: ip (public IPv4 or IPv6 address) is required');
|
|
113
|
+
}
|
|
114
|
+
return this._request(`/v1/lookup/${encodeURIComponent(ip.trim())}`, { method: 'GET' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convenience helper: returns true if the session should be blocked.
|
|
119
|
+
* Defaults to blocking when the API's own isSuspicious flag is set.
|
|
120
|
+
* Pass a custom predicate to build your own policy.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} input — same as evaluate()
|
|
123
|
+
* @param {(r: EvaluateResult) => boolean} [predicate]
|
|
124
|
+
* @returns {Promise<boolean>}
|
|
125
|
+
*/
|
|
126
|
+
async shouldBlock(input, predicate) {
|
|
127
|
+
const result = await this.evaluate(input);
|
|
128
|
+
return predicate ? !!predicate(result) : !!result.isSuspicious;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Sentinel.SentinelError = SentinelError;
|
|
133
|
+
module.exports = Sentinel;
|
|
134
|
+
module.exports.default = Sentinel;
|
|
135
|
+
module.exports.Sentinel = Sentinel;
|
|
136
|
+
module.exports.SentinelError = SentinelError;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @typedef {object} EvaluateResult
|
|
140
|
+
* @property {'allow'|'review'|'block'} decision — route on this
|
|
141
|
+
* @property {number} risk_score — 0–100 weighted risk score
|
|
142
|
+
* @property {boolean} isSuspicious — true if network or device signals flag the session
|
|
143
|
+
* @property {string|null} ip
|
|
144
|
+
* @property {string|null} country — 2-letter country code
|
|
145
|
+
* @property {object} network — { vpn, proxy, datacenter, anonymous, tor, residential, service }
|
|
146
|
+
* @property {object} [device] — { antidetect, automation, emulator, virtual_machine, incognito, visitor_id, tampering_score, ... } when fingerprintEventId was supplied
|
|
147
|
+
* @property {string[]} reasons — machine-readable reason codes (vpn_detected, proxy_detected, ...)
|
|
148
|
+
* @property {number} evaluated_in_ms
|
|
149
|
+
* @property {{disposable: boolean}} [email] — present when the optional email input was supplied
|
|
150
|
+
* @property {'allow'|'review'|'block'} [engine_decision] — present when your own rules/exceptions changed `decision`
|
|
151
|
+
* @property {'rules'|'exception'} [decision_source] — who authored the final decision when it was not the engine
|
|
152
|
+
* @property {string[]} [rule_matched] — signals that triggered a custom rule
|
|
153
|
+
* @property {string[]} [exception_matched] — matched per-IP/visitor pins
|
|
154
|
+
* @property {boolean} [test] — present on test-token / test-key responses (never billed)
|
|
155
|
+
* @property {EvaluateDetails} details — legacy network signals (backwards compatibility)
|
|
156
|
+
* @property {DeviceIntel|null} [deviceIntel] — legacy device signals (backwards compatibility)
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @typedef {object} LookupResponse
|
|
161
|
+
* @property {string} ip
|
|
162
|
+
* @property {boolean} known — whether our reputation feeds hold data for this IP (false ≠ clean)
|
|
163
|
+
* @property {'allow'|'review'|'block'} verdict
|
|
164
|
+
* @property {number} risk_score — 0–100
|
|
165
|
+
* @property {{vpn: boolean, proxied: boolean, tor: boolean, dch: boolean, anon: boolean}|null} signals — null when known is false
|
|
166
|
+
* @property {{asn: number|null, org: string|null, country: string|null, city: string|null, cloud?: string}|null} network
|
|
167
|
+
* @property {number} latency_ms
|
|
168
|
+
* @property {'allow'|'review'|'block'} [engine_verdict] — present when an exception pin changed `verdict`
|
|
169
|
+
* @property {'exception'} [verdict_source]
|
|
170
|
+
* @property {string[]} [exception_matched]
|
|
171
|
+
* @property {boolean} [test] — present when the call used the per-account sk_test_ key
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @typedef {object} EvaluateDetails
|
|
176
|
+
* @property {string} ip
|
|
177
|
+
* @property {string} cc — 2-letter country code
|
|
178
|
+
* @property {boolean} vpn
|
|
179
|
+
* @property {boolean} proxied
|
|
180
|
+
* @property {boolean} dch — datacenter flag
|
|
181
|
+
* @property {boolean} anon
|
|
182
|
+
* @property {boolean} [crawler]
|
|
183
|
+
* @property {string} [service]
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @typedef {object} DeviceIntel
|
|
188
|
+
* @property {string|null} visitorId
|
|
189
|
+
* @property {boolean} browserTampering
|
|
190
|
+
* @property {boolean} botDetected
|
|
191
|
+
* @property {boolean} vpnDetected
|
|
192
|
+
* @property {boolean} proxyDetected
|
|
193
|
+
* @property {boolean} torDetected
|
|
194
|
+
* @property {boolean} ipBlocklisted
|
|
195
|
+
* @property {boolean} incognito
|
|
196
|
+
* @property {boolean} virtualMachine
|
|
197
|
+
* @property {boolean} emulator
|
|
198
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sentinelsup/sdk",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Official Node.js SDK for Sentinel — real-time fraud detection, VPN/residential-proxy/antidetect-browser detection in under 40ms.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"fraud-detection",
|
|
7
|
-
"bot-detection",
|
|
8
|
-
"vpn-detection",
|
|
9
|
-
"proxy-detection",
|
|
10
|
-
"residential-proxy",
|
|
11
|
-
"antidetect-browser",
|
|
12
|
-
"stripe",
|
|
13
|
-
"shopify",
|
|
14
|
-
"chargeback",
|
|
15
|
-
"sentinel",
|
|
16
|
-
"sntlhq"
|
|
17
|
-
],
|
|
18
|
-
"main": "index.js",
|
|
19
|
-
"types": "index.d.ts",
|
|
20
|
-
"files": [
|
|
21
|
-
"index.js",
|
|
22
|
-
"index.d.ts",
|
|
23
|
-
"README.md",
|
|
24
|
-
"LICENSE"
|
|
25
|
-
],
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "https://github.com/sentinelsup/sentinel-node.git"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://sntlhq.com",
|
|
31
|
-
"bugs": {
|
|
32
|
-
"url": "https://github.com/sentinelsup/sentinel-node/issues"
|
|
33
|
-
},
|
|
34
|
-
"author": "Sentinel Edge Networks LTD <support@sntlhq.com>",
|
|
35
|
-
"license": "MIT",
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=14"
|
|
38
|
-
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"test": "node --test test.js"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentinelsup/sdk",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Official Node.js SDK for Sentinel — real-time fraud detection, VPN/residential-proxy/antidetect-browser detection in under 40ms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fraud-detection",
|
|
7
|
+
"bot-detection",
|
|
8
|
+
"vpn-detection",
|
|
9
|
+
"proxy-detection",
|
|
10
|
+
"residential-proxy",
|
|
11
|
+
"antidetect-browser",
|
|
12
|
+
"stripe",
|
|
13
|
+
"shopify",
|
|
14
|
+
"chargeback",
|
|
15
|
+
"sentinel",
|
|
16
|
+
"sntlhq"
|
|
17
|
+
],
|
|
18
|
+
"main": "index.js",
|
|
19
|
+
"types": "index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"index.js",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/sentinelsup/sentinel-node.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://sntlhq.com",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/sentinelsup/sentinel-node/issues"
|
|
33
|
+
},
|
|
34
|
+
"author": "Sentinel Edge Networks LTD <support@sntlhq.com>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test.js"
|
|
41
|
+
}
|
|
42
|
+
}
|