sentinelsup 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/.gitignore +7 -7
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/LICENSE +21 -21
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/PKG-INFO +24 -24
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/README.md +17 -17
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/examples/django_middleware.py +48 -48
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/examples/flask_signup_guard.py +46 -46
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/pyproject.toml +48 -48
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/sentinel/__init__.py +208 -208
- {sentinelsup-0.2.2 → sentinelsup-0.2.3}/sentinel/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
__pycache__/
|
|
2
|
-
*.py[cod]
|
|
3
|
-
*.egg-info/
|
|
4
|
-
build/
|
|
5
|
-
dist/
|
|
6
|
-
.venv/
|
|
7
|
-
.pytest_cache/
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
.venv/
|
|
7
|
+
.pytest_cache/
|
|
@@ -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.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sentinelsup
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary:
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Documentation, https://
|
|
7
|
-
Project-URL: Repository, https://github.com/sentinelsup/
|
|
8
|
-
Project-URL: Issues, https://github.com/sentinelsup/
|
|
9
|
-
Author-email: Sentinel Edge Networks LTD <support@
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Maskbreak — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response.
|
|
5
|
+
Project-URL: Homepage, https://maskbreak.com
|
|
6
|
+
Project-URL: Documentation, https://maskbreak.com/api
|
|
7
|
+
Project-URL: Repository, https://github.com/sentinelsup/maskbreak-python
|
|
8
|
+
Project-URL: Issues, https://github.com/sentinelsup/maskbreak-python/issues
|
|
9
|
+
Author-email: Sentinel Edge Networks LTD <support@maskbreak.com>
|
|
10
10
|
License: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: antidetect-browser,api,bot-detection,device-fingerprinting,fraud-detection,proxy-detection,sentinel,vpn-detection
|
|
@@ -27,9 +27,9 @@ Classifier: Typing :: Typed
|
|
|
27
27
|
Requires-Python: >=3.8
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
|
-
# sentinelsup —
|
|
30
|
+
# sentinelsup — Maskbreak Python SDK
|
|
31
31
|
|
|
32
|
-
Official Python SDK for [
|
|
32
|
+
Official Python SDK for [Maskbreak](https://maskbreak.com) — a real-time fraud detection API that flags VPNs, residential proxies, antidetect browsers (Kameleo, GoLogin, Multilogin), Tor exit nodes, and AI bots in under 40 ms.
|
|
33
33
|
|
|
34
34
|
[](https://pypi.org/project/sentinelsup/)
|
|
35
35
|
[](https://pypi.org/project/sentinelsup/)
|
|
@@ -43,12 +43,12 @@ Using Claude Code, Cursor, Copilot, or any AI coding assistant? Paste this one
|
|
|
43
43
|
prompt and it wires the whole integration — frontend script, backend check,
|
|
44
44
|
env var, and a test:
|
|
45
45
|
|
|
46
|
-
> Fetch https://
|
|
46
|
+
> Fetch https://maskbreak.com/integrate.md and follow it to add Maskbreak fraud
|
|
47
47
|
> protection to this app — protect signup, login, and checkout. My API key
|
|
48
48
|
> is sk_live_YOUR_KEY; put it in a SENTINEL_KEY env var, never in
|
|
49
49
|
> client-side code. Then show me how to test it.
|
|
50
50
|
|
|
51
|
-
[`integrate.md`](https://
|
|
51
|
+
[`integrate.md`](https://maskbreak.com/integrate.md) is the canonical
|
|
52
52
|
machine-readable integration guide, kept in sync with the live API.
|
|
53
53
|
|
|
54
54
|
## Install
|
|
@@ -57,7 +57,7 @@ machine-readable integration guide, kept in sync with the live API.
|
|
|
57
57
|
pip install sentinelsup
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Python 3.8+. Get a free API key (no credit card) at [
|
|
60
|
+
Python 3.8+. Get a free API key (no credit card) at [maskbreak.com/signup](https://maskbreak.com/signup).
|
|
61
61
|
|
|
62
62
|
## Quick start
|
|
63
63
|
|
|
@@ -119,18 +119,18 @@ class EvaluateResult:
|
|
|
119
119
|
Try the live sample (same shape, no key needed):
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
curl "https://
|
|
122
|
+
curl "https://maskbreak.com/v1/evaluate/sample?scenario=vpn"
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
Or use the [interactive playground](https://
|
|
125
|
+
Or use the [interactive playground](https://maskbreak.com/api#playground).
|
|
126
126
|
|
|
127
127
|
## Frontend setup
|
|
128
128
|
|
|
129
|
-
Add the
|
|
129
|
+
Add the Maskbreak SDK to your frontend. One script loads **both** layers —
|
|
130
130
|
network (VPN/proxy/datacenter) and device (antidetect/bot/tampering):
|
|
131
131
|
|
|
132
132
|
```html
|
|
133
|
-
<script async src="https://
|
|
133
|
+
<script async src="https://maskbreak.com/assets/sentinel.js"></script>
|
|
134
134
|
|
|
135
135
|
<!-- Add class="monocle-enriched" to any form you want evaluated -->
|
|
136
136
|
<form class="monocle-enriched" id="signup-form">
|
|
@@ -206,12 +206,12 @@ Runnable versions live in [`examples/`](./examples/).
|
|
|
206
206
|
|
|
207
207
|
## API
|
|
208
208
|
|
|
209
|
-
### `Sentinel(api_key=None, endpoint="https://
|
|
209
|
+
### `Sentinel(api_key=None, endpoint="https://maskbreak.com", timeout=5.0)`
|
|
210
210
|
|
|
211
211
|
| Option | Default | Description |
|
|
212
212
|
|--------|---------|-------------|
|
|
213
213
|
| `api_key` | `$SENTINEL_KEY` (falls back to `$SENTINEL_API_KEY`) | Your key starting with `sk_live_` |
|
|
214
|
-
| `endpoint` | `https://
|
|
214
|
+
| `endpoint` | `https://maskbreak.com` | Override base URL (for testing) |
|
|
215
215
|
| `timeout` | `5.0` | Per-request timeout in seconds |
|
|
216
216
|
|
|
217
217
|
### `sentinel.evaluate(token, fingerprint_event_id=None, account_id=None, email=None)`
|
|
@@ -265,9 +265,9 @@ except SentinelError as e:
|
|
|
265
265
|
## Rate limits
|
|
266
266
|
|
|
267
267
|
Free tier: **1,000 requests/hour** per API key. No monthly cap, no credit
|
|
268
|
-
card. Upgrade at [
|
|
268
|
+
card. Upgrade at [maskbreak.com](https://maskbreak.com) when you need more.
|
|
269
269
|
|
|
270
|
-
## What
|
|
270
|
+
## What Maskbreak detects
|
|
271
271
|
|
|
272
272
|
VPNs (commercial + self-hosted) · residential proxies (Bright Data, IPRoyal,
|
|
273
273
|
and similar networks) · datacenter IPs · Tor exit nodes · antidetect browsers
|
|
@@ -277,10 +277,10 @@ virtual machines · browser tampering.
|
|
|
277
277
|
|
|
278
278
|
## Related
|
|
279
279
|
|
|
280
|
-
- **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/
|
|
281
|
-
- **API docs** — [
|
|
282
|
-
- **Free IP lookup tool** — [
|
|
280
|
+
- **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/maskbreak-node) on npm
|
|
281
|
+
- **API docs** — [maskbreak.com/api](https://maskbreak.com/api)
|
|
282
|
+
- **Free IP lookup tool** — [maskbreak.com/ip-lookup](https://maskbreak.com/ip-lookup)
|
|
283
283
|
|
|
284
284
|
## License
|
|
285
285
|
|
|
286
|
-
MIT © [Sentinel Edge Networks LTD](https://
|
|
286
|
+
MIT © [Sentinel Edge Networks LTD](https://maskbreak.com). See [LICENSE](LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# sentinelsup —
|
|
1
|
+
# sentinelsup — Maskbreak Python SDK
|
|
2
2
|
|
|
3
|
-
Official Python SDK for [
|
|
3
|
+
Official Python SDK for [Maskbreak](https://maskbreak.com) — a real-time fraud detection API that flags VPNs, residential proxies, antidetect browsers (Kameleo, GoLogin, Multilogin), Tor exit nodes, and AI bots in under 40 ms.
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/sentinelsup/)
|
|
6
6
|
[](https://pypi.org/project/sentinelsup/)
|
|
@@ -14,12 +14,12 @@ Using Claude Code, Cursor, Copilot, or any AI coding assistant? Paste this one
|
|
|
14
14
|
prompt and it wires the whole integration — frontend script, backend check,
|
|
15
15
|
env var, and a test:
|
|
16
16
|
|
|
17
|
-
> Fetch https://
|
|
17
|
+
> Fetch https://maskbreak.com/integrate.md and follow it to add Maskbreak fraud
|
|
18
18
|
> protection to this app — protect signup, login, and checkout. My API key
|
|
19
19
|
> is sk_live_YOUR_KEY; put it in a SENTINEL_KEY env var, never in
|
|
20
20
|
> client-side code. Then show me how to test it.
|
|
21
21
|
|
|
22
|
-
[`integrate.md`](https://
|
|
22
|
+
[`integrate.md`](https://maskbreak.com/integrate.md) is the canonical
|
|
23
23
|
machine-readable integration guide, kept in sync with the live API.
|
|
24
24
|
|
|
25
25
|
## Install
|
|
@@ -28,7 +28,7 @@ machine-readable integration guide, kept in sync with the live API.
|
|
|
28
28
|
pip install sentinelsup
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Python 3.8+. Get a free API key (no credit card) at [
|
|
31
|
+
Python 3.8+. Get a free API key (no credit card) at [maskbreak.com/signup](https://maskbreak.com/signup).
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
@@ -90,18 +90,18 @@ class EvaluateResult:
|
|
|
90
90
|
Try the live sample (same shape, no key needed):
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
curl "https://
|
|
93
|
+
curl "https://maskbreak.com/v1/evaluate/sample?scenario=vpn"
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
Or use the [interactive playground](https://
|
|
96
|
+
Or use the [interactive playground](https://maskbreak.com/api#playground).
|
|
97
97
|
|
|
98
98
|
## Frontend setup
|
|
99
99
|
|
|
100
|
-
Add the
|
|
100
|
+
Add the Maskbreak SDK to your frontend. One script loads **both** layers —
|
|
101
101
|
network (VPN/proxy/datacenter) and device (antidetect/bot/tampering):
|
|
102
102
|
|
|
103
103
|
```html
|
|
104
|
-
<script async src="https://
|
|
104
|
+
<script async src="https://maskbreak.com/assets/sentinel.js"></script>
|
|
105
105
|
|
|
106
106
|
<!-- Add class="monocle-enriched" to any form you want evaluated -->
|
|
107
107
|
<form class="monocle-enriched" id="signup-form">
|
|
@@ -177,12 +177,12 @@ Runnable versions live in [`examples/`](./examples/).
|
|
|
177
177
|
|
|
178
178
|
## API
|
|
179
179
|
|
|
180
|
-
### `Sentinel(api_key=None, endpoint="https://
|
|
180
|
+
### `Sentinel(api_key=None, endpoint="https://maskbreak.com", timeout=5.0)`
|
|
181
181
|
|
|
182
182
|
| Option | Default | Description |
|
|
183
183
|
|--------|---------|-------------|
|
|
184
184
|
| `api_key` | `$SENTINEL_KEY` (falls back to `$SENTINEL_API_KEY`) | Your key starting with `sk_live_` |
|
|
185
|
-
| `endpoint` | `https://
|
|
185
|
+
| `endpoint` | `https://maskbreak.com` | Override base URL (for testing) |
|
|
186
186
|
| `timeout` | `5.0` | Per-request timeout in seconds |
|
|
187
187
|
|
|
188
188
|
### `sentinel.evaluate(token, fingerprint_event_id=None, account_id=None, email=None)`
|
|
@@ -236,9 +236,9 @@ except SentinelError as e:
|
|
|
236
236
|
## Rate limits
|
|
237
237
|
|
|
238
238
|
Free tier: **1,000 requests/hour** per API key. No monthly cap, no credit
|
|
239
|
-
card. Upgrade at [
|
|
239
|
+
card. Upgrade at [maskbreak.com](https://maskbreak.com) when you need more.
|
|
240
240
|
|
|
241
|
-
## What
|
|
241
|
+
## What Maskbreak detects
|
|
242
242
|
|
|
243
243
|
VPNs (commercial + self-hosted) · residential proxies (Bright Data, IPRoyal,
|
|
244
244
|
and similar networks) · datacenter IPs · Tor exit nodes · antidetect browsers
|
|
@@ -248,10 +248,10 @@ virtual machines · browser tampering.
|
|
|
248
248
|
|
|
249
249
|
## Related
|
|
250
250
|
|
|
251
|
-
- **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/
|
|
252
|
-
- **API docs** — [
|
|
253
|
-
- **Free IP lookup tool** — [
|
|
251
|
+
- **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/maskbreak-node) on npm
|
|
252
|
+
- **API docs** — [maskbreak.com/api](https://maskbreak.com/api)
|
|
253
|
+
- **Free IP lookup tool** — [maskbreak.com/ip-lookup](https://maskbreak.com/ip-lookup)
|
|
254
254
|
|
|
255
255
|
## License
|
|
256
256
|
|
|
257
|
-
MIT © [Sentinel Edge Networks LTD](https://
|
|
257
|
+
MIT © [Sentinel Edge Networks LTD](https://maskbreak.com). See [LICENSE](LICENSE).
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
"""Django middleware example — score every request to high-value endpoints.
|
|
2
|
-
|
|
3
|
-
Add to settings.py MIDDLEWARE:
|
|
4
|
-
"yourapp.middleware.SentinelMiddleware",
|
|
5
|
-
|
|
6
|
-
Then ensure your frontend forwards the Sentinel token via the
|
|
7
|
-
X-Sentinel-Token header (set after the SDK injects it on the client).
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import logging
|
|
11
|
-
|
|
12
|
-
from django.http import JsonResponse
|
|
13
|
-
|
|
14
|
-
from sentinel import Sentinel, SentinelError
|
|
15
|
-
|
|
16
|
-
log = logging.getLogger(__name__)
|
|
17
|
-
_GUARDED_PATHS = ("/api/checkout", "/api/withdraw", "/api/transfer")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SentinelMiddleware:
|
|
21
|
-
def __init__(self, get_response):
|
|
22
|
-
self.get_response = get_response
|
|
23
|
-
self.sentinel = Sentinel() # reads SENTINEL_API_KEY
|
|
24
|
-
|
|
25
|
-
def __call__(self, request):
|
|
26
|
-
if not request.path.startswith(_GUARDED_PATHS):
|
|
27
|
-
return self.get_response(request)
|
|
28
|
-
|
|
29
|
-
token = request.META.get("HTTP_X_SENTINEL_TOKEN")
|
|
30
|
-
if not token:
|
|
31
|
-
return JsonResponse({"error": "missing X-Sentinel-Token"}, status=400)
|
|
32
|
-
|
|
33
|
-
try:
|
|
34
|
-
result = self.sentinel.evaluate(token=token)
|
|
35
|
-
except SentinelError as e:
|
|
36
|
-
log.warning("Sentinel error: %s", e)
|
|
37
|
-
# Fail open on infra problems; switch to fail-closed for finance flows.
|
|
38
|
-
return self.get_response(request)
|
|
39
|
-
|
|
40
|
-
if result.is_blocked:
|
|
41
|
-
return JsonResponse(
|
|
42
|
-
{"error": "blocked", "risk_score": result.risk_score, "reasons": result.reasons},
|
|
43
|
-
status=403,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# Stash on request so the view can read decision/score
|
|
47
|
-
request.sentinel = result
|
|
48
|
-
return self.get_response(request)
|
|
1
|
+
"""Django middleware example — score every request to high-value endpoints.
|
|
2
|
+
|
|
3
|
+
Add to settings.py MIDDLEWARE:
|
|
4
|
+
"yourapp.middleware.SentinelMiddleware",
|
|
5
|
+
|
|
6
|
+
Then ensure your frontend forwards the Sentinel token via the
|
|
7
|
+
X-Sentinel-Token header (set after the SDK injects it on the client).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from django.http import JsonResponse
|
|
13
|
+
|
|
14
|
+
from sentinel import Sentinel, SentinelError
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
_GUARDED_PATHS = ("/api/checkout", "/api/withdraw", "/api/transfer")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SentinelMiddleware:
|
|
21
|
+
def __init__(self, get_response):
|
|
22
|
+
self.get_response = get_response
|
|
23
|
+
self.sentinel = Sentinel() # reads SENTINEL_API_KEY
|
|
24
|
+
|
|
25
|
+
def __call__(self, request):
|
|
26
|
+
if not request.path.startswith(_GUARDED_PATHS):
|
|
27
|
+
return self.get_response(request)
|
|
28
|
+
|
|
29
|
+
token = request.META.get("HTTP_X_SENTINEL_TOKEN")
|
|
30
|
+
if not token:
|
|
31
|
+
return JsonResponse({"error": "missing X-Sentinel-Token"}, status=400)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = self.sentinel.evaluate(token=token)
|
|
35
|
+
except SentinelError as e:
|
|
36
|
+
log.warning("Sentinel error: %s", e)
|
|
37
|
+
# Fail open on infra problems; switch to fail-closed for finance flows.
|
|
38
|
+
return self.get_response(request)
|
|
39
|
+
|
|
40
|
+
if result.is_blocked:
|
|
41
|
+
return JsonResponse(
|
|
42
|
+
{"error": "blocked", "risk_score": result.risk_score, "reasons": result.reasons},
|
|
43
|
+
status=403,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Stash on request so the view can read decision/score
|
|
47
|
+
request.sentinel = result
|
|
48
|
+
return self.get_response(request)
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
"""Flask example — block VPN/proxy signups using Sentinel.
|
|
2
|
-
|
|
3
|
-
Run:
|
|
4
|
-
pip install flask sentinelsup
|
|
5
|
-
export SENTINEL_API_KEY=sk_live_...
|
|
6
|
-
python flask_signup_guard.py
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import os
|
|
10
|
-
from flask import Flask, abort, jsonify, request
|
|
11
|
-
|
|
12
|
-
from sentinel import Sentinel, SentinelError
|
|
13
|
-
|
|
14
|
-
app = Flask(__name__)
|
|
15
|
-
sentinel = Sentinel() # reads SENTINEL_API_KEY
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@app.route("/signup", methods=["POST"])
|
|
19
|
-
def signup() -> object:
|
|
20
|
-
payload = request.get_json(force=True) or {}
|
|
21
|
-
email = (payload.get("email") or "").strip().lower()
|
|
22
|
-
token = payload.get("sentinelToken")
|
|
23
|
-
|
|
24
|
-
if not email or not token:
|
|
25
|
-
abort(400, "missing email or sentinelToken")
|
|
26
|
-
|
|
27
|
-
try:
|
|
28
|
-
result = sentinel.evaluate(token=token)
|
|
29
|
-
except SentinelError as e:
|
|
30
|
-
# Fail open if Sentinel is down — log and continue.
|
|
31
|
-
app.logger.warning("Sentinel unavailable: %s", e)
|
|
32
|
-
result = None
|
|
33
|
-
|
|
34
|
-
if result and result.is_blocked:
|
|
35
|
-
return jsonify({"error": "Signup blocked", "reasons": result.reasons}), 403
|
|
36
|
-
|
|
37
|
-
if result and result.decision == "review":
|
|
38
|
-
# Soft challenge: email verification, manual review, slower onboarding, etc.
|
|
39
|
-
return jsonify({"ok": True, "needs_verification": True})
|
|
40
|
-
|
|
41
|
-
# Normal signup flow
|
|
42
|
-
return jsonify({"ok": True})
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if __name__ == "__main__":
|
|
46
|
-
app.run(port=int(os.environ.get("PORT", 5000)), debug=False)
|
|
1
|
+
"""Flask example — block VPN/proxy signups using Sentinel.
|
|
2
|
+
|
|
3
|
+
Run:
|
|
4
|
+
pip install flask sentinelsup
|
|
5
|
+
export SENTINEL_API_KEY=sk_live_...
|
|
6
|
+
python flask_signup_guard.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from flask import Flask, abort, jsonify, request
|
|
11
|
+
|
|
12
|
+
from sentinel import Sentinel, SentinelError
|
|
13
|
+
|
|
14
|
+
app = Flask(__name__)
|
|
15
|
+
sentinel = Sentinel() # reads SENTINEL_API_KEY
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.route("/signup", methods=["POST"])
|
|
19
|
+
def signup() -> object:
|
|
20
|
+
payload = request.get_json(force=True) or {}
|
|
21
|
+
email = (payload.get("email") or "").strip().lower()
|
|
22
|
+
token = payload.get("sentinelToken")
|
|
23
|
+
|
|
24
|
+
if not email or not token:
|
|
25
|
+
abort(400, "missing email or sentinelToken")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
result = sentinel.evaluate(token=token)
|
|
29
|
+
except SentinelError as e:
|
|
30
|
+
# Fail open if Sentinel is down — log and continue.
|
|
31
|
+
app.logger.warning("Sentinel unavailable: %s", e)
|
|
32
|
+
result = None
|
|
33
|
+
|
|
34
|
+
if result and result.is_blocked:
|
|
35
|
+
return jsonify({"error": "Signup blocked", "reasons": result.reasons}), 403
|
|
36
|
+
|
|
37
|
+
if result and result.decision == "review":
|
|
38
|
+
# Soft challenge: email verification, manual review, slower onboarding, etc.
|
|
39
|
+
return jsonify({"ok": True, "needs_verification": True})
|
|
40
|
+
|
|
41
|
+
# Normal signup flow
|
|
42
|
+
return jsonify({"ok": True})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
app.run(port=int(os.environ.get("PORT", 5000)), debug=False)
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
# NOTE: "sentinel-sdk" on PyPI is squatted by an unrelated third party
|
|
7
|
-
# ("NAST0R"). Never publish or document that name — users following it
|
|
8
|
-
# would install a stranger's code. "sentinelsup" matches the npm scope
|
|
9
|
-
# (@sentinelsup/sdk).
|
|
10
|
-
name = "sentinelsup"
|
|
11
|
-
version = "0.2.
|
|
12
|
-
description = "
|
|
13
|
-
readme = "README.md"
|
|
14
|
-
requires-python = ">=3.8"
|
|
15
|
-
license = { text = "MIT" }
|
|
16
|
-
authors = [
|
|
17
|
-
{ name = "Sentinel Edge Networks LTD", email = "support@
|
|
18
|
-
]
|
|
19
|
-
keywords = [
|
|
20
|
-
"fraud-detection", "vpn-detection", "proxy-detection", "bot-detection",
|
|
21
|
-
"antidetect-browser", "device-fingerprinting", "sentinel", "api"
|
|
22
|
-
]
|
|
23
|
-
classifiers = [
|
|
24
|
-
"Development Status :: 4 - Beta",
|
|
25
|
-
"Intended Audience :: Developers",
|
|
26
|
-
"License :: OSI Approved :: MIT License",
|
|
27
|
-
"Programming Language :: Python :: 3",
|
|
28
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
-
"Programming Language :: Python :: 3.8",
|
|
30
|
-
"Programming Language :: Python :: 3.9",
|
|
31
|
-
"Programming Language :: Python :: 3.10",
|
|
32
|
-
"Programming Language :: Python :: 3.11",
|
|
33
|
-
"Programming Language :: Python :: 3.12",
|
|
34
|
-
"Programming Language :: Python :: 3.13",
|
|
35
|
-
"Topic :: Internet :: WWW/HTTP",
|
|
36
|
-
"Topic :: Security",
|
|
37
|
-
"Typing :: Typed",
|
|
38
|
-
]
|
|
39
|
-
dependencies = []
|
|
40
|
-
|
|
41
|
-
[project.urls]
|
|
42
|
-
Homepage = "https://
|
|
43
|
-
Documentation = "https://
|
|
44
|
-
Repository = "https://github.com/sentinelsup/
|
|
45
|
-
Issues = "https://github.com/sentinelsup/
|
|
46
|
-
|
|
47
|
-
[tool.hatch.build.targets.wheel]
|
|
48
|
-
packages = ["sentinel"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# NOTE: "sentinel-sdk" on PyPI is squatted by an unrelated third party
|
|
7
|
+
# ("NAST0R"). Never publish or document that name — users following it
|
|
8
|
+
# would install a stranger's code. "sentinelsup" matches the npm scope
|
|
9
|
+
# (@sentinelsup/sdk).
|
|
10
|
+
name = "sentinelsup"
|
|
11
|
+
version = "0.2.3"
|
|
12
|
+
description = "Maskbreak — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response."
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
license = { text = "MIT" }
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "Sentinel Edge Networks LTD", email = "support@maskbreak.com" }
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"fraud-detection", "vpn-detection", "proxy-detection", "bot-detection",
|
|
21
|
+
"antidetect-browser", "device-fingerprinting", "sentinel", "api"
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Programming Language :: Python :: 3.8",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
36
|
+
"Topic :: Security",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = []
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://maskbreak.com"
|
|
43
|
+
Documentation = "https://maskbreak.com/api"
|
|
44
|
+
Repository = "https://github.com/sentinelsup/maskbreak-python"
|
|
45
|
+
Issues = "https://github.com/sentinelsup/maskbreak-python/issues"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["sentinel"]
|
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Sentinel Python SDK — thin, dependency-free wrapper around the Sentinel
|
|
3
|
-
fraud detection API at https://
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
from sentinel import Sentinel
|
|
7
|
-
s = Sentinel() # reads SENTINEL_KEY or SENTINEL_API_KEY from the env
|
|
8
|
-
result = s.evaluate(token=request.json["sentinelToken"])
|
|
9
|
-
if result.is_blocked:
|
|
10
|
-
abort(403)
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from typing import Any, Dict, Optional
|
|
17
|
-
from urllib import error, parse, request
|
|
18
|
-
|
|
19
|
-
DEFAULT_ENDPOINT = "https://
|
|
20
|
-
DEFAULT_TIMEOUT = 5.0
|
|
21
|
-
__version__ = "0.2.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class SentinelError(Exception):
|
|
25
|
-
"""Raised on any Sentinel API or transport failure."""
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
message: str,
|
|
30
|
-
status: Optional[int] = None,
|
|
31
|
-
body: Optional[Dict[str, Any]] = None,
|
|
32
|
-
) -> None:
|
|
33
|
-
super().__init__(message)
|
|
34
|
-
self.status = status
|
|
35
|
-
self.body = body or {}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@dataclass
|
|
39
|
-
class EvaluateResult:
|
|
40
|
-
"""Structured response from /v1/evaluate."""
|
|
41
|
-
|
|
42
|
-
decision: Optional[str] = None
|
|
43
|
-
risk_score: Optional[int] = None
|
|
44
|
-
ip: Optional[str] = None
|
|
45
|
-
country: Optional[str] = None
|
|
46
|
-
network: Dict[str, Any] = field(default_factory=dict)
|
|
47
|
-
device: Dict[str, Any] = field(default_factory=dict)
|
|
48
|
-
reasons: list = field(default_factory=list)
|
|
49
|
-
email: Optional[Dict[str, Any]] = None
|
|
50
|
-
decision_source: Optional[str] = None
|
|
51
|
-
engine_decision: Optional[str] = None
|
|
52
|
-
test: bool = False
|
|
53
|
-
raw: Dict[str, Any] = field(default_factory=dict)
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def is_suspicious(self) -> bool:
|
|
57
|
-
"""True if the decision is anything other than 'allow'."""
|
|
58
|
-
return self.decision is not None and self.decision != "allow"
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def is_blocked(self) -> bool:
|
|
62
|
-
return self.decision == "block"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class Sentinel:
|
|
66
|
-
"""Sentinel API client. Pass an API key from https://
|
|
67
|
-
|
|
68
|
-
def __init__(
|
|
69
|
-
self,
|
|
70
|
-
api_key: Optional[str] = None,
|
|
71
|
-
endpoint: str = DEFAULT_ENDPOINT,
|
|
72
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
73
|
-
) -> None:
|
|
74
|
-
# SENTINEL_KEY is the name every doc surface uses; SENTINEL_API_KEY
|
|
75
|
-
# is kept for existing installs that already adopted it.
|
|
76
|
-
api_key = api_key or os.environ.get("SENTINEL_KEY") or os.environ.get("SENTINEL_API_KEY")
|
|
77
|
-
if not api_key or not isinstance(api_key, str):
|
|
78
|
-
raise SentinelError(
|
|
79
|
-
"Sentinel: api_key is required. "
|
|
80
|
-
"Pass it explicitly or set SENTINEL_KEY. "
|
|
81
|
-
"Get one free at https://
|
|
82
|
-
)
|
|
83
|
-
self.api_key = api_key
|
|
84
|
-
self.endpoint = endpoint.rstrip("/")
|
|
85
|
-
self.timeout = timeout
|
|
86
|
-
|
|
87
|
-
def _request(
|
|
88
|
-
self,
|
|
89
|
-
path: str,
|
|
90
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
91
|
-
) -> Dict[str, Any]:
|
|
92
|
-
"""Shared transport: auth header, timeout, JSON parsing, error mapping."""
|
|
93
|
-
body = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
94
|
-
req = request.Request(
|
|
95
|
-
f"{self.endpoint}{path}",
|
|
96
|
-
data=body,
|
|
97
|
-
method="POST" if payload is not None else "GET",
|
|
98
|
-
headers={
|
|
99
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
100
|
-
"Content-Type": "application/json",
|
|
101
|
-
"User-Agent": f"sentinel-python/{__version__}",
|
|
102
|
-
},
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
107
|
-
raw = resp.read().decode("utf-8")
|
|
108
|
-
return json.loads(raw) if raw else {}
|
|
109
|
-
except error.HTTPError as e:
|
|
110
|
-
try:
|
|
111
|
-
err_body = json.loads(e.read().decode("utf-8"))
|
|
112
|
-
except Exception:
|
|
113
|
-
err_body = {}
|
|
114
|
-
msg = err_body.get("error") if isinstance(err_body, dict) else None
|
|
115
|
-
raise SentinelError(
|
|
116
|
-
f"Sentinel: API returned {e.code}"
|
|
117
|
-
+ (f" — {msg}" if msg else ""),
|
|
118
|
-
status=e.code,
|
|
119
|
-
body=err_body,
|
|
120
|
-
) from None
|
|
121
|
-
except error.URLError as e:
|
|
122
|
-
raise SentinelError(f"Sentinel: network error — {e.reason}") from None
|
|
123
|
-
except Exception as e:
|
|
124
|
-
raise SentinelError(f"Sentinel: unexpected error — {e}") from None
|
|
125
|
-
|
|
126
|
-
def evaluate(
|
|
127
|
-
self,
|
|
128
|
-
token: str,
|
|
129
|
-
fingerprint_event_id: Optional[str] = None,
|
|
130
|
-
account_id: Optional[str] = None,
|
|
131
|
-
email: Optional[str] = None,
|
|
132
|
-
) -> EvaluateResult:
|
|
133
|
-
"""Evaluate a visitor session for fraud signals.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
token: Sentinel client-side token from the frontend SDK.
|
|
137
|
-
fingerprint_event_id: Optional Fingerprint event id for device signals.
|
|
138
|
-
account_id: Optional account/user id — enables multi-accounting
|
|
139
|
-
detection (device.linked_accounts / device.multi_account).
|
|
140
|
-
email: Optional signup email. Adds ``email.disposable`` to the raw
|
|
141
|
-
response (burner domains escalate allow to review). Checked
|
|
142
|
-
transiently — never stored or logged.
|
|
143
|
-
|
|
144
|
-
Returns:
|
|
145
|
-
EvaluateResult with decision, risk_score, network, device, and reasons.
|
|
146
|
-
|
|
147
|
-
Raises:
|
|
148
|
-
SentinelError: on network failure, timeout, or non-2xx response.
|
|
149
|
-
"""
|
|
150
|
-
if not token or not isinstance(token, str):
|
|
151
|
-
raise SentinelError(
|
|
152
|
-
"Sentinel.evaluate: token (client-side Sentinel token) is required"
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
payload: Dict[str, Any] = {"token": token}
|
|
156
|
-
if fingerprint_event_id:
|
|
157
|
-
payload["fingerprintEventId"] = fingerprint_event_id
|
|
158
|
-
if account_id:
|
|
159
|
-
payload["accountId"] = account_id
|
|
160
|
-
if email:
|
|
161
|
-
payload["email"] = email
|
|
162
|
-
|
|
163
|
-
data = self._request("/v1/evaluate", payload)
|
|
164
|
-
|
|
165
|
-
return EvaluateResult(
|
|
166
|
-
decision=data.get("decision"),
|
|
167
|
-
risk_score=data.get("risk_score"),
|
|
168
|
-
ip=data.get("ip"),
|
|
169
|
-
country=data.get("country"),
|
|
170
|
-
network=data.get("network") or {},
|
|
171
|
-
device=data.get("device") or {},
|
|
172
|
-
reasons=data.get("reasons") or [],
|
|
173
|
-
email=data.get("email"),
|
|
174
|
-
decision_source=data.get("decision_source"),
|
|
175
|
-
engine_decision=data.get("engine_decision"),
|
|
176
|
-
test=bool(data.get("test")),
|
|
177
|
-
raw=data,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
def lookup(self, ip: str) -> Dict[str, Any]:
|
|
181
|
-
"""Look up an arbitrary public IP address — no browser token needed.
|
|
182
|
-
|
|
183
|
-
Wraps ``GET /v1/lookup/{ip}``. Useful for batch scoring, log
|
|
184
|
-
enrichment, and server-side screening. Shares the per-key hourly
|
|
185
|
-
quota with :meth:`evaluate`.
|
|
186
|
-
|
|
187
|
-
Args:
|
|
188
|
-
ip: Public IPv4 or IPv6 address, e.g. ``"185.220.101.34"``.
|
|
189
|
-
|
|
190
|
-
Returns:
|
|
191
|
-
The raw response dict: ``verdict`` ('allow' | 'review' | 'block'),
|
|
192
|
-
``risk_score`` (0-100), ``known``, ``signals``
|
|
193
|
-
({vpn, proxied, tor, dch, anon} or None), ``network``
|
|
194
|
-
({asn, org, country, city}), ``latency_ms``. Note ``known: False``
|
|
195
|
-
means our feeds hold no data for the IP — it is NOT a clean
|
|
196
|
-
guarantee.
|
|
197
|
-
|
|
198
|
-
Raises:
|
|
199
|
-
SentinelError: on network failure, timeout, or non-2xx response.
|
|
200
|
-
"""
|
|
201
|
-
if not ip or not isinstance(ip, str):
|
|
202
|
-
raise SentinelError(
|
|
203
|
-
"Sentinel.lookup: ip (public IPv4 or IPv6 address) is required"
|
|
204
|
-
)
|
|
205
|
-
return self._request(f"/v1/lookup/{parse.quote(ip.strip(), safe='')}")
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
__all__ = ["Sentinel", "SentinelError", "EvaluateResult", "__version__"]
|
|
1
|
+
"""
|
|
2
|
+
Sentinel Python SDK — thin, dependency-free wrapper around the Sentinel
|
|
3
|
+
fraud detection API at https://maskbreak.com/v1/evaluate.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from sentinel import Sentinel
|
|
7
|
+
s = Sentinel() # reads SENTINEL_KEY or SENTINEL_API_KEY from the env
|
|
8
|
+
result = s.evaluate(token=request.json["sentinelToken"])
|
|
9
|
+
if result.is_blocked:
|
|
10
|
+
abort(403)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
from urllib import error, parse, request
|
|
18
|
+
|
|
19
|
+
DEFAULT_ENDPOINT = "https://maskbreak.com"
|
|
20
|
+
DEFAULT_TIMEOUT = 5.0
|
|
21
|
+
__version__ = "0.2.3"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SentinelError(Exception):
|
|
25
|
+
"""Raised on any Sentinel API or transport failure."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
message: str,
|
|
30
|
+
status: Optional[int] = None,
|
|
31
|
+
body: Optional[Dict[str, Any]] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.status = status
|
|
35
|
+
self.body = body or {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class EvaluateResult:
|
|
40
|
+
"""Structured response from /v1/evaluate."""
|
|
41
|
+
|
|
42
|
+
decision: Optional[str] = None
|
|
43
|
+
risk_score: Optional[int] = None
|
|
44
|
+
ip: Optional[str] = None
|
|
45
|
+
country: Optional[str] = None
|
|
46
|
+
network: Dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
device: Dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
reasons: list = field(default_factory=list)
|
|
49
|
+
email: Optional[Dict[str, Any]] = None
|
|
50
|
+
decision_source: Optional[str] = None
|
|
51
|
+
engine_decision: Optional[str] = None
|
|
52
|
+
test: bool = False
|
|
53
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_suspicious(self) -> bool:
|
|
57
|
+
"""True if the decision is anything other than 'allow'."""
|
|
58
|
+
return self.decision is not None and self.decision != "allow"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_blocked(self) -> bool:
|
|
62
|
+
return self.decision == "block"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Sentinel:
|
|
66
|
+
"""Sentinel API client. Pass an API key from https://maskbreak.com/dashboard."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
api_key: Optional[str] = None,
|
|
71
|
+
endpoint: str = DEFAULT_ENDPOINT,
|
|
72
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
73
|
+
) -> None:
|
|
74
|
+
# SENTINEL_KEY is the name every doc surface uses; SENTINEL_API_KEY
|
|
75
|
+
# is kept for existing installs that already adopted it.
|
|
76
|
+
api_key = api_key or os.environ.get("SENTINEL_KEY") or os.environ.get("SENTINEL_API_KEY")
|
|
77
|
+
if not api_key or not isinstance(api_key, str):
|
|
78
|
+
raise SentinelError(
|
|
79
|
+
"Sentinel: api_key is required. "
|
|
80
|
+
"Pass it explicitly or set SENTINEL_KEY. "
|
|
81
|
+
"Get one free at https://maskbreak.com/signup"
|
|
82
|
+
)
|
|
83
|
+
self.api_key = api_key
|
|
84
|
+
self.endpoint = endpoint.rstrip("/")
|
|
85
|
+
self.timeout = timeout
|
|
86
|
+
|
|
87
|
+
def _request(
|
|
88
|
+
self,
|
|
89
|
+
path: str,
|
|
90
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
91
|
+
) -> Dict[str, Any]:
|
|
92
|
+
"""Shared transport: auth header, timeout, JSON parsing, error mapping."""
|
|
93
|
+
body = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
94
|
+
req = request.Request(
|
|
95
|
+
f"{self.endpoint}{path}",
|
|
96
|
+
data=body,
|
|
97
|
+
method="POST" if payload is not None else "GET",
|
|
98
|
+
headers={
|
|
99
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"User-Agent": f"sentinel-python/{__version__}",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
107
|
+
raw = resp.read().decode("utf-8")
|
|
108
|
+
return json.loads(raw) if raw else {}
|
|
109
|
+
except error.HTTPError as e:
|
|
110
|
+
try:
|
|
111
|
+
err_body = json.loads(e.read().decode("utf-8"))
|
|
112
|
+
except Exception:
|
|
113
|
+
err_body = {}
|
|
114
|
+
msg = err_body.get("error") if isinstance(err_body, dict) else None
|
|
115
|
+
raise SentinelError(
|
|
116
|
+
f"Sentinel: API returned {e.code}"
|
|
117
|
+
+ (f" — {msg}" if msg else ""),
|
|
118
|
+
status=e.code,
|
|
119
|
+
body=err_body,
|
|
120
|
+
) from None
|
|
121
|
+
except error.URLError as e:
|
|
122
|
+
raise SentinelError(f"Sentinel: network error — {e.reason}") from None
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise SentinelError(f"Sentinel: unexpected error — {e}") from None
|
|
125
|
+
|
|
126
|
+
def evaluate(
|
|
127
|
+
self,
|
|
128
|
+
token: str,
|
|
129
|
+
fingerprint_event_id: Optional[str] = None,
|
|
130
|
+
account_id: Optional[str] = None,
|
|
131
|
+
email: Optional[str] = None,
|
|
132
|
+
) -> EvaluateResult:
|
|
133
|
+
"""Evaluate a visitor session for fraud signals.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
token: Sentinel client-side token from the frontend SDK.
|
|
137
|
+
fingerprint_event_id: Optional Fingerprint event id for device signals.
|
|
138
|
+
account_id: Optional account/user id — enables multi-accounting
|
|
139
|
+
detection (device.linked_accounts / device.multi_account).
|
|
140
|
+
email: Optional signup email. Adds ``email.disposable`` to the raw
|
|
141
|
+
response (burner domains escalate allow to review). Checked
|
|
142
|
+
transiently — never stored or logged.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
EvaluateResult with decision, risk_score, network, device, and reasons.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
SentinelError: on network failure, timeout, or non-2xx response.
|
|
149
|
+
"""
|
|
150
|
+
if not token or not isinstance(token, str):
|
|
151
|
+
raise SentinelError(
|
|
152
|
+
"Sentinel.evaluate: token (client-side Sentinel token) is required"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
payload: Dict[str, Any] = {"token": token}
|
|
156
|
+
if fingerprint_event_id:
|
|
157
|
+
payload["fingerprintEventId"] = fingerprint_event_id
|
|
158
|
+
if account_id:
|
|
159
|
+
payload["accountId"] = account_id
|
|
160
|
+
if email:
|
|
161
|
+
payload["email"] = email
|
|
162
|
+
|
|
163
|
+
data = self._request("/v1/evaluate", payload)
|
|
164
|
+
|
|
165
|
+
return EvaluateResult(
|
|
166
|
+
decision=data.get("decision"),
|
|
167
|
+
risk_score=data.get("risk_score"),
|
|
168
|
+
ip=data.get("ip"),
|
|
169
|
+
country=data.get("country"),
|
|
170
|
+
network=data.get("network") or {},
|
|
171
|
+
device=data.get("device") or {},
|
|
172
|
+
reasons=data.get("reasons") or [],
|
|
173
|
+
email=data.get("email"),
|
|
174
|
+
decision_source=data.get("decision_source"),
|
|
175
|
+
engine_decision=data.get("engine_decision"),
|
|
176
|
+
test=bool(data.get("test")),
|
|
177
|
+
raw=data,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def lookup(self, ip: str) -> Dict[str, Any]:
|
|
181
|
+
"""Look up an arbitrary public IP address — no browser token needed.
|
|
182
|
+
|
|
183
|
+
Wraps ``GET /v1/lookup/{ip}``. Useful for batch scoring, log
|
|
184
|
+
enrichment, and server-side screening. Shares the per-key hourly
|
|
185
|
+
quota with :meth:`evaluate`.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
ip: Public IPv4 or IPv6 address, e.g. ``"185.220.101.34"``.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The raw response dict: ``verdict`` ('allow' | 'review' | 'block'),
|
|
192
|
+
``risk_score`` (0-100), ``known``, ``signals``
|
|
193
|
+
({vpn, proxied, tor, dch, anon} or None), ``network``
|
|
194
|
+
({asn, org, country, city}), ``latency_ms``. Note ``known: False``
|
|
195
|
+
means our feeds hold no data for the IP — it is NOT a clean
|
|
196
|
+
guarantee.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
SentinelError: on network failure, timeout, or non-2xx response.
|
|
200
|
+
"""
|
|
201
|
+
if not ip or not isinstance(ip, str):
|
|
202
|
+
raise SentinelError(
|
|
203
|
+
"Sentinel.lookup: ip (public IPv4 or IPv6 address) is required"
|
|
204
|
+
)
|
|
205
|
+
return self._request(f"/v1/lookup/{parse.quote(ip.strip(), safe='')}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = ["Sentinel", "SentinelError", "EvaluateResult", "__version__"]
|
|
File without changes
|