@sentinelsup/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/index.d.ts +60 -0
- package/index.js +140 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
[](./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 client-side SDK to your frontend so Sentinel can collect the token:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<script async src="https://fp.sntlhq.com/agent"></script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Read the token from the rendered form field and send it to your backend:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const token = document.querySelector('input[name="monocle"]').value;
|
|
73
|
+
fetch('/checkout', { method: 'POST', body: JSON.stringify({ sentinelToken: token }) });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
### Stripe Checkout — block card testing
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const Sentinel = require('@sentinelsup/sdk');
|
|
82
|
+
const stripe = require('stripe')(process.env.STRIPE_KEY);
|
|
83
|
+
const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });
|
|
84
|
+
|
|
85
|
+
app.post('/checkout', async (req, res) => {
|
|
86
|
+
const { isSuspicious } = await sentinel.evaluate({ token: req.body.sentinelToken });
|
|
87
|
+
if (isSuspicious) return res.status(403).json({ error: 'declined' });
|
|
88
|
+
|
|
89
|
+
const intent = await stripe.paymentIntents.create({ /* ... */ });
|
|
90
|
+
res.json({ clientSecret: intent.client_secret });
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Signup — block fake Google sign-ins
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
app.post('/auth/google', async (req, res) => {
|
|
98
|
+
const { credential, sentinelToken } = req.body;
|
|
99
|
+
const ticket = await googleClient.verifyIdToken({ idToken: credential });
|
|
100
|
+
|
|
101
|
+
const result = await sentinel.evaluate({ token: sentinelToken });
|
|
102
|
+
if (result.isSuspicious) return res.status(403).json({ error: 'signup_blocked' });
|
|
103
|
+
|
|
104
|
+
await createUser(ticket.getPayload().email, result.deviceIntel?.visitorId);
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Custom policy with `shouldBlock`
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
// Only block when we see both residential proxy AND browser tampering
|
|
112
|
+
const blocked = await sentinel.shouldBlock(
|
|
113
|
+
{ token },
|
|
114
|
+
r => r.details.proxied && r.deviceIntel?.browserTampering
|
|
115
|
+
);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API
|
|
119
|
+
|
|
120
|
+
### `new Sentinel({ apiKey, endpoint?, timeoutMs? })`
|
|
121
|
+
|
|
122
|
+
| Option | Type | Default | Description |
|
|
123
|
+
|--------|------|---------|-------------|
|
|
124
|
+
| `apiKey` | string | required | Your key starting with `sk_live_` |
|
|
125
|
+
| `endpoint` | string | `https://sntlhq.com` | Override base URL |
|
|
126
|
+
| `timeoutMs` | number | `5000` | Per-request timeout |
|
|
127
|
+
|
|
128
|
+
### `sentinel.evaluate({ token, fingerprintEventId? })`
|
|
129
|
+
|
|
130
|
+
Returns `EvaluateResult`. Throws `SentinelError` on network/API failure — the error carries `.status` and `.body`.
|
|
131
|
+
|
|
132
|
+
### `sentinel.shouldBlock({ token, fingerprintEventId? }, predicate?)`
|
|
133
|
+
|
|
134
|
+
Convenience: runs `evaluate()` and returns a boolean. Default predicate is `r => r.isSuspicious`. Pass your own to build custom policies.
|
|
135
|
+
|
|
136
|
+
## Rate limits
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
## TypeScript
|
|
141
|
+
|
|
142
|
+
Full types ship with the package. Importing `Sentinel` gives you the class plus `EvaluateResult`, `DeviceIntel`, `EvaluateDetails`, and `SentinelError` types.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import Sentinel, { EvaluateResult } from '@sentinelsup/sdk';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT © Sentinel Edge Networks LTD
|
|
151
|
+
|
|
152
|
+
## Links
|
|
153
|
+
|
|
154
|
+
- Website — [sntlhq.com](https://sntlhq.com)
|
|
155
|
+
- API docs — [sntlhq.com/api](https://sntlhq.com/api)
|
|
156
|
+
- Blog — [sntlhq.com/blog](https://sntlhq.com/blog)
|
|
157
|
+
- X / Twitter — [@SentinelSup](https://x.com/SentinelSup)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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 };
|
package/index.js
ADDED
|
@@ -0,0 +1,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('@sentinel/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} dch — datacenter 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
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentinelsup/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
}
|