@sentinelsup/sdk 0.1.1 → 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.
Files changed (5) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +248 -162
  3. package/index.d.ts +180 -60
  4. package/index.js +198 -140
  5. 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,162 +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
- [![npm](https://img.shields.io/npm/v/@sentinelsup/sdk.svg)](https://www.npmjs.com/package/@sentinelsup/sdk)
6
- [![license](https://img.shields.io/npm/l/@sentinelsup/sdk.svg)](./LICENSE)
7
-
8
- ## Install
9
-
10
- ```bash
11
- npm install @sentinelsup/sdk
12
- ```
13
-
14
- Zero dependencies. Works on Node 14+, Bun, Deno, Cloudflare Workers, and Vercel Edge (wherever `fetch` exists).
15
-
16
- ## Quick start
17
-
18
- ```js
19
- const Sentinel = require('@sentinelsup/sdk');
20
-
21
- const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
22
-
23
- const result = await sentinel.evaluate({
24
- token: req.body.sentinelToken // from the frontend SDK
25
- });
26
-
27
- if (result.isSuspicious) {
28
- return res.status(403).json({ error: 'blocked' });
29
- }
30
-
31
- // Safe — let the request through
32
- ```
33
-
34
- Get a free API key (no credit card) at [sntlhq.com/signup](https://sntlhq.com/signup).
35
-
36
- ## What you get back
37
-
38
- ```ts
39
- {
40
- isSuspicious: boolean, // combined network + device verdict
41
- details: {
42
- ip: '45.33.32.156',
43
- cc: 'US',
44
- vpn: false,
45
- proxied: true, // residential proxy detected
46
- dch: false, // datacenter
47
- anon: false,
48
- service: 'BrightData'
49
- },
50
- deviceIntel: { // null unless you pass fingerprintEventId
51
- visitorId: 'abc123...',
52
- browserTampering: true, // antidetect browser detected
53
- botDetected: false,
54
- incognito: false,
55
- virtualMachine: false,
56
- emulator: false
57
- }
58
- }
59
- ```
60
-
61
- ## Frontend setup
62
-
63
- Add the Sentinel Edge SDK to your frontend so Sentinel can collect the token:
64
-
65
- ```html
66
- <script async src="https://sntlhq.com/assets/edge.js" id="_mcl"></script>
67
-
68
- <!-- Add class="monocle-enriched" to any form you want evaluated -->
69
- <form class="monocle-enriched" id="checkout-form">
70
- <!-- The SDK injects: <input type="hidden" name="monocle" value="eyJ..."> -->
71
- </form>
72
- ```
73
-
74
- Read the token from the injected form field and send it to your backend:
75
-
76
- ```js
77
- const token = document.querySelector('input[name="monocle"]').value;
78
- fetch('/checkout', { method: 'POST', body: JSON.stringify({ sentinelToken: token }) });
79
- ```
80
-
81
- ## Examples
82
-
83
- ### Stripe Checkout block card testing
84
-
85
- ```js
86
- const Sentinel = require('@sentinelsup/sdk');
87
- const stripe = require('stripe')(process.env.STRIPE_KEY);
88
- const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
89
-
90
- app.post('/checkout', async (req, res) => {
91
- const { isSuspicious } = await sentinel.evaluate({ token: req.body.sentinelToken });
92
- if (isSuspicious) return res.status(403).json({ error: 'declined' });
93
-
94
- const intent = await stripe.paymentIntents.create({ /* ... */ });
95
- res.json({ clientSecret: intent.client_secret });
96
- });
97
- ```
98
-
99
- ### Signup — block fake Google sign-ins
100
-
101
- ```js
102
- app.post('/auth/google', async (req, res) => {
103
- const { credential, sentinelToken } = req.body;
104
- const ticket = await googleClient.verifyIdToken({ idToken: credential });
105
-
106
- const result = await sentinel.evaluate({ token: sentinelToken });
107
- if (result.isSuspicious) return res.status(403).json({ error: 'signup_blocked' });
108
-
109
- await createUser(ticket.getPayload().email, result.deviceIntel?.visitorId);
110
- });
111
- ```
112
-
113
- ### Custom policy with `shouldBlock`
114
-
115
- ```js
116
- // Only block when we see both residential proxy AND browser tampering
117
- const blocked = await sentinel.shouldBlock(
118
- { token },
119
- r => r.details.proxied && r.deviceIntel?.browserTampering
120
- );
121
- ```
122
-
123
- ## API
124
-
125
- ### `new Sentinel({ apiKey, endpoint?, timeoutMs? })`
126
-
127
- | Option | Type | Default | Description |
128
- |--------|------|---------|-------------|
129
- | `apiKey` | string | required | Your key starting with `sk_live_` |
130
- | `endpoint` | string | `https://sntlhq.com` | Override base URL |
131
- | `timeoutMs` | number | `5000` | Per-request timeout |
132
-
133
- ### `sentinel.evaluate({ token, fingerprintEventId? })`
134
-
135
- Returns `EvaluateResult`. Throws `SentinelError` on network/API failure — the error carries `.status` and `.body`.
136
-
137
- ### `sentinel.shouldBlock({ token, fingerprintEventId? }, predicate?)`
138
-
139
- Convenience: runs `evaluate()` and returns a boolean. Default predicate is `r => r.isSuspicious`. Pass your own to build custom policies.
140
-
141
- ## Rate limits
142
-
143
- Free tier: **1,000 requests/hour** per API key. No monthly cap, no credit card. Upgrade at [sntlhq.com](https://sntlhq.com) when you need more.
144
-
145
- ## TypeScript
146
-
147
- Full types ship with the package. Importing `Sentinel` gives you the class plus `EvaluateResult`, `DeviceIntel`, `EvaluateDetails`, and `SentinelError` types.
148
-
149
- ```ts
150
- import Sentinel, { EvaluateResult } from '@sentinelsup/sdk';
151
- ```
152
-
153
- ## License
154
-
155
- MIT © Sentinel Edge Networks LTD
156
-
157
- ## Links
158
-
159
- - Website[sntlhq.com](https://sntlhq.com)
160
- - API docs — [sntlhq.com/api](https://sntlhq.com/api)
161
- - Blog — [sntlhq.com/blog](https://sntlhq.com/blog)
162
- - X / Twitter [@SentinelSup](https://x.com/SentinelSup)
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
+ [![npm](https://img.shields.io/npm/v/@sentinelsup/sdk.svg)](https://www.npmjs.com/package/@sentinelsup/sdk)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@sentinelsup/sdk.svg)](https://www.npmjs.com/package/@sentinelsup/sdk)
7
+ [![types](https://img.shields.io/npm/types/@sentinelsup/sdk.svg)](./index.d.ts)
8
+ [![license](https://img.shields.io/npm/l/@sentinelsup/sdk.svg)](./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,60 +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 EvaluateResult {
27
- status: string;
28
- isSuspicious: boolean;
29
- details: EvaluateDetails;
30
- deviceIntel: DeviceIntel | null;
31
- }
32
-
33
- export interface SentinelOptions {
34
- /** Your Sentinel API key (starts with sk_live_). Get one free at https://sntlhq.com/signup */
35
- apiKey: string;
36
- /** Override the API base URL (default: https://sntlhq.com) */
37
- endpoint?: string;
38
- /** Per-request timeout in ms (default: 5000) */
39
- timeoutMs?: number;
40
- }
41
-
42
- export interface EvaluateInput {
43
- /** Client-side Sentinel token from the frontend SDK */
44
- token: string;
45
- /** Optional Fingerprint event id for device-layer signals */
46
- fingerprintEventId?: string;
47
- }
48
-
49
- export class SentinelError extends Error {
50
- status?: number;
51
- body?: unknown;
52
- }
53
-
54
- export default class Sentinel {
55
- constructor(opts: SentinelOptions);
56
- evaluate(input: EvaluateInput): Promise<EvaluateResult>;
57
- shouldBlock(input: EvaluateInput, predicate?: (r: EvaluateResult) => boolean): Promise<boolean>;
58
- }
59
-
60
- export { Sentinel };
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,140 +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.isSuspicious) 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
- * Evaluate a visitor session for fraud signals.
41
- *
42
- * @param {object} input
43
- * @param {string} input.token — Sentinel client-side token from the frontend SDK
44
- * @param {string} [input.fingerprintEventId] — optional Fingerprint event id for device signals
45
- * @returns {Promise<EvaluateResult>}
46
- */
47
- async evaluate({ token, fingerprintEventId } = {}) {
48
- if (!token || typeof token !== 'string') {
49
- throw new SentinelError('Sentinel.evaluate: token (client-side Sentinel token) is required');
50
- }
51
-
52
- const controller = new AbortController();
53
- const abortTimer = setTimeout(() => controller.abort(), this.timeoutMs);
54
-
55
- let res;
56
- try {
57
- res = await fetch(`${this.endpoint}/v1/evaluate`, {
58
- method: 'POST',
59
- headers: {
60
- 'Authorization': `Bearer ${this.apiKey}`,
61
- 'Content-Type': 'application/json'
62
- },
63
- body: JSON.stringify({ token, fingerprintEventId }),
64
- signal: controller.signal
65
- });
66
- } catch (err) {
67
- if (err.name === 'AbortError') {
68
- throw new SentinelError(`Sentinel: request timed out after ${this.timeoutMs}ms`);
69
- }
70
- throw new SentinelError(`Sentinel: network error — ${err.message}`);
71
- } finally {
72
- clearTimeout(abortTimer);
73
- }
74
-
75
- let json;
76
- try { json = await res.json(); } catch { json = null; }
77
-
78
- if (!res.ok) {
79
- throw new SentinelError(
80
- `Sentinel: API returned ${res.status}${json && json.error ? ` — ${json.error}` : ''}`,
81
- { status: res.status, body: json }
82
- );
83
- }
84
-
85
- return json;
86
- }
87
-
88
- /**
89
- * Convenience helper: returns true if the session should be blocked.
90
- * Defaults to blocking when the API's own isSuspicious flag is set.
91
- * Pass a custom predicate to build your own policy.
92
- *
93
- * @param {object} input — same as evaluate()
94
- * @param {(r: EvaluateResult) => boolean} [predicate]
95
- * @returns {Promise<boolean>}
96
- */
97
- async shouldBlock(input, predicate) {
98
- const result = await this.evaluate(input);
99
- return predicate ? !!predicate(result) : !!result.isSuspicious;
100
- }
101
- }
102
-
103
- Sentinel.SentinelError = SentinelError;
104
- module.exports = Sentinel;
105
- module.exports.default = Sentinel;
106
- module.exports.Sentinel = Sentinel;
107
- module.exports.SentinelError = SentinelError;
108
-
109
- /**
110
- * @typedef {object} EvaluateResult
111
- * @property {boolean} isSuspicious — true if network or device signals flag the session
112
- * @property {EvaluateDetails} details network / IP signals from Spur Monocle
113
- * @property {DeviceIntel|null} deviceIntel — device signals if fingerprintEventId was supplied
114
- */
115
-
116
- /**
117
- * @typedef {object} EvaluateDetails
118
- * @property {string} ip
119
- * @property {string} cc 2-letter country code
120
- * @property {boolean} vpn
121
- * @property {boolean} proxied
122
- * @property {boolean} dchdatacenter flag
123
- * @property {boolean} anon
124
- * @property {boolean} [crawler]
125
- * @property {string} [service]
126
- */
127
-
128
- /**
129
- * @typedef {object} DeviceIntel
130
- * @property {string|null} visitorId
131
- * @property {boolean} browserTampering
132
- * @property {boolean} botDetected
133
- * @property {boolean} vpnDetected
134
- * @property {boolean} proxyDetected
135
- * @property {boolean} torDetected
136
- * @property {boolean} ipBlocklisted
137
- * @property {boolean} incognito
138
- * @property {boolean} virtualMachine
139
- * @property {boolean} emulator
140
- */
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} inputsame 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.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
+ }