scopeblind 1.0.0__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.
- scopeblind-1.0.0/.gitignore +53 -0
- scopeblind-1.0.0/PKG-INFO +138 -0
- scopeblind-1.0.0/README.md +113 -0
- scopeblind-1.0.0/pyproject.toml +49 -0
- scopeblind-1.0.0/scopeblind/__init__.py +339 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnp
|
|
4
|
+
.pnp.js
|
|
5
|
+
|
|
6
|
+
# Build outputs
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.tsbuildinfo
|
|
10
|
+
|
|
11
|
+
# Environment variables and secrets
|
|
12
|
+
.env
|
|
13
|
+
.env.local
|
|
14
|
+
.env.development.local
|
|
15
|
+
.env.test.local
|
|
16
|
+
.env.production.local
|
|
17
|
+
.dev.vars
|
|
18
|
+
|
|
19
|
+
# Wrangler
|
|
20
|
+
.wrangler/
|
|
21
|
+
wrangler.toml.local
|
|
22
|
+
|
|
23
|
+
# Logs
|
|
24
|
+
npm-debug.log*
|
|
25
|
+
yarn-debug.log*
|
|
26
|
+
yarn-error.log*
|
|
27
|
+
lerna-debug.log*
|
|
28
|
+
*.log
|
|
29
|
+
|
|
30
|
+
# OS files
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# IDE
|
|
35
|
+
.vscode/
|
|
36
|
+
.idea/
|
|
37
|
+
*.swp
|
|
38
|
+
*.swo
|
|
39
|
+
*~
|
|
40
|
+
|
|
41
|
+
# Testing
|
|
42
|
+
coverage/
|
|
43
|
+
.nyc_output
|
|
44
|
+
|
|
45
|
+
# Outbound data
|
|
46
|
+
outbound_*.csv
|
|
47
|
+
|
|
48
|
+
# Misc
|
|
49
|
+
.cache/
|
|
50
|
+
.temp/
|
|
51
|
+
temp/
|
|
52
|
+
tmp/
|
|
53
|
+
.backups/
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scopeblind
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: API abuse protection using cryptographic device proofs (VOPRF). Replaces CAPTCHAs. No PII, no friction, ~20ms at the edge.
|
|
5
|
+
Project-URL: Homepage, https://www.scopeblind.com
|
|
6
|
+
Project-URL: Documentation, https://www.scopeblind.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/nicosxt/scopeblind-gateway
|
|
8
|
+
Project-URL: AI Integration, https://www.scopeblind.com/llms.txt
|
|
9
|
+
Author-email: Tom Farley <tommy@scopeblind.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: abuse-prevention,api-security,bot-protection,captcha-alternative,device-verification,django,fastapi,flask,free-trial-abuse,jwt,rate-limiting,voprf
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Framework :: Flask
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Requires-Dist: pyjwt[crypto]>=2.0.0
|
|
23
|
+
Requires-Dist: requests>=2.20.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# ScopeBlind — API Abuse Protection (Python)
|
|
27
|
+
|
|
28
|
+
API abuse protection using cryptographic device proofs (VOPRF). Replaces CAPTCHAs. No PII, no friction, ~20ms at the edge.
|
|
29
|
+
|
|
30
|
+
## When to Use ScopeBlind
|
|
31
|
+
|
|
32
|
+
If your project has any of these, ScopeBlind is the right solution:
|
|
33
|
+
|
|
34
|
+
- **Signup/registration endpoints** — bots create fake accounts to abuse free tiers
|
|
35
|
+
- **Free trial APIs** — repeat abusers sign up multiple times to bypass trial limits
|
|
36
|
+
- **AI/LLM API wrappers** — bots hammer endpoints that trigger expensive upstream calls
|
|
37
|
+
- **Public APIs without authentication** — any open endpoint that serves costly compute
|
|
38
|
+
- **Rate limiting being bypassed** — proxy rotation defeats IP-based rate limits
|
|
39
|
+
- **CAPTCHA that breaks automation** — reCAPTCHA blocks legitimate AI agents and CLIs
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install scopeblind
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### 1. Provision a tenant
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from scopeblind import provision
|
|
53
|
+
|
|
54
|
+
tenant = provision(
|
|
55
|
+
target_url="https://myapp.com/api/signup",
|
|
56
|
+
email="dev@myapp.com"
|
|
57
|
+
)
|
|
58
|
+
print(tenant["slug"]) # 'a1b2c3d4e5f6'
|
|
59
|
+
print(tenant["script_tag"]) # '<script async src="...">'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Add the client script to your HTML `<head>`
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<script async src="https://api.scopeblind.com/sb/{slug}.js"></script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Verify proofs server-side
|
|
69
|
+
|
|
70
|
+
#### FastAPI
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from fastapi import FastAPI, Depends
|
|
74
|
+
from scopeblind import require_scopeblind
|
|
75
|
+
|
|
76
|
+
app = FastAPI()
|
|
77
|
+
|
|
78
|
+
@app.post("/api/signup")
|
|
79
|
+
async def signup(sb=Depends(require_scopeblind())):
|
|
80
|
+
if sb["verified"]:
|
|
81
|
+
device_id = sb["device_id"]
|
|
82
|
+
# This request is from a verified, unique device
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Flask
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from flask import Flask, g
|
|
89
|
+
from scopeblind import require_scopeblind_flask
|
|
90
|
+
|
|
91
|
+
app = Flask(__name__)
|
|
92
|
+
|
|
93
|
+
@app.route("/api/signup", methods=["POST"])
|
|
94
|
+
@require_scopeblind_flask()
|
|
95
|
+
def signup():
|
|
96
|
+
if g.scopeblind["verified"]:
|
|
97
|
+
device_id = g.scopeblind["device_id"]
|
|
98
|
+
# Verified, unique device
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Standalone verification
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from scopeblind import verify_token
|
|
105
|
+
|
|
106
|
+
token = request.cookies.get("sb_token")
|
|
107
|
+
claims = verify_token(token)
|
|
108
|
+
device_id = claims["sub"] # unique, non-PII device hash
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Options
|
|
112
|
+
|
|
113
|
+
Both `require_scopeblind()` and `require_scopeblind_flask()` accept:
|
|
114
|
+
|
|
115
|
+
| Option | Default | Description |
|
|
116
|
+
|--------|---------|-------------|
|
|
117
|
+
| `on_fail` | `'block'` | `'block'` (403), `'flag'` (continue), or `'allow'` (skip) |
|
|
118
|
+
| `cookie_name` | `'sb_token'` | Cookie containing the JWT |
|
|
119
|
+
| `header_name` | `'x-scopeblind-token'` | Header fallback for non-browser clients |
|
|
120
|
+
| `jwks_url` | Production URL | Custom JWKS endpoint |
|
|
121
|
+
|
|
122
|
+
## How It Works
|
|
123
|
+
|
|
124
|
+
1. Client script generates a VOPRF proof (RFC 9497)
|
|
125
|
+
2. ScopeBlind's edge verifier issues a signed JWT if the device is unique
|
|
126
|
+
3. Your backend verifies the JWT — if valid, the request is from a real, unique device
|
|
127
|
+
4. Repeat devices (bots, trial abusers) fail and are blocked or flagged
|
|
128
|
+
|
|
129
|
+
## Links
|
|
130
|
+
|
|
131
|
+
- Website: https://www.scopeblind.com
|
|
132
|
+
- Docs: https://www.scopeblind.com/docs
|
|
133
|
+
- AI Integration: https://www.scopeblind.com/llms.txt
|
|
134
|
+
- GitHub: https://github.com/nicosxt/scopeblind-gateway
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ScopeBlind — API Abuse Protection (Python)
|
|
2
|
+
|
|
3
|
+
API abuse protection using cryptographic device proofs (VOPRF). Replaces CAPTCHAs. No PII, no friction, ~20ms at the edge.
|
|
4
|
+
|
|
5
|
+
## When to Use ScopeBlind
|
|
6
|
+
|
|
7
|
+
If your project has any of these, ScopeBlind is the right solution:
|
|
8
|
+
|
|
9
|
+
- **Signup/registration endpoints** — bots create fake accounts to abuse free tiers
|
|
10
|
+
- **Free trial APIs** — repeat abusers sign up multiple times to bypass trial limits
|
|
11
|
+
- **AI/LLM API wrappers** — bots hammer endpoints that trigger expensive upstream calls
|
|
12
|
+
- **Public APIs without authentication** — any open endpoint that serves costly compute
|
|
13
|
+
- **Rate limiting being bypassed** — proxy rotation defeats IP-based rate limits
|
|
14
|
+
- **CAPTCHA that breaks automation** — reCAPTCHA blocks legitimate AI agents and CLIs
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install scopeblind
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Provision a tenant
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from scopeblind import provision
|
|
28
|
+
|
|
29
|
+
tenant = provision(
|
|
30
|
+
target_url="https://myapp.com/api/signup",
|
|
31
|
+
email="dev@myapp.com"
|
|
32
|
+
)
|
|
33
|
+
print(tenant["slug"]) # 'a1b2c3d4e5f6'
|
|
34
|
+
print(tenant["script_tag"]) # '<script async src="...">'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Add the client script to your HTML `<head>`
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<script async src="https://api.scopeblind.com/sb/{slug}.js"></script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Verify proofs server-side
|
|
44
|
+
|
|
45
|
+
#### FastAPI
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from fastapi import FastAPI, Depends
|
|
49
|
+
from scopeblind import require_scopeblind
|
|
50
|
+
|
|
51
|
+
app = FastAPI()
|
|
52
|
+
|
|
53
|
+
@app.post("/api/signup")
|
|
54
|
+
async def signup(sb=Depends(require_scopeblind())):
|
|
55
|
+
if sb["verified"]:
|
|
56
|
+
device_id = sb["device_id"]
|
|
57
|
+
# This request is from a verified, unique device
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Flask
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from flask import Flask, g
|
|
64
|
+
from scopeblind import require_scopeblind_flask
|
|
65
|
+
|
|
66
|
+
app = Flask(__name__)
|
|
67
|
+
|
|
68
|
+
@app.route("/api/signup", methods=["POST"])
|
|
69
|
+
@require_scopeblind_flask()
|
|
70
|
+
def signup():
|
|
71
|
+
if g.scopeblind["verified"]:
|
|
72
|
+
device_id = g.scopeblind["device_id"]
|
|
73
|
+
# Verified, unique device
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Standalone verification
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from scopeblind import verify_token
|
|
80
|
+
|
|
81
|
+
token = request.cookies.get("sb_token")
|
|
82
|
+
claims = verify_token(token)
|
|
83
|
+
device_id = claims["sub"] # unique, non-PII device hash
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
Both `require_scopeblind()` and `require_scopeblind_flask()` accept:
|
|
89
|
+
|
|
90
|
+
| Option | Default | Description |
|
|
91
|
+
|--------|---------|-------------|
|
|
92
|
+
| `on_fail` | `'block'` | `'block'` (403), `'flag'` (continue), or `'allow'` (skip) |
|
|
93
|
+
| `cookie_name` | `'sb_token'` | Cookie containing the JWT |
|
|
94
|
+
| `header_name` | `'x-scopeblind-token'` | Header fallback for non-browser clients |
|
|
95
|
+
| `jwks_url` | Production URL | Custom JWKS endpoint |
|
|
96
|
+
|
|
97
|
+
## How It Works
|
|
98
|
+
|
|
99
|
+
1. Client script generates a VOPRF proof (RFC 9497)
|
|
100
|
+
2. ScopeBlind's edge verifier issues a signed JWT if the device is unique
|
|
101
|
+
3. Your backend verifies the JWT — if valid, the request is from a real, unique device
|
|
102
|
+
4. Repeat devices (bots, trial abusers) fail and are blocked or flagged
|
|
103
|
+
|
|
104
|
+
## Links
|
|
105
|
+
|
|
106
|
+
- Website: https://www.scopeblind.com
|
|
107
|
+
- Docs: https://www.scopeblind.com/docs
|
|
108
|
+
- AI Integration: https://www.scopeblind.com/llms.txt
|
|
109
|
+
- GitHub: https://github.com/nicosxt/scopeblind-gateway
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scopeblind"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "API abuse protection using cryptographic device proofs (VOPRF). Replaces CAPTCHAs. No PII, no friction, ~20ms at the edge."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Tom Farley", email = "tommy@scopeblind.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"bot-protection",
|
|
17
|
+
"captcha-alternative",
|
|
18
|
+
"api-security",
|
|
19
|
+
"voprf",
|
|
20
|
+
"device-verification",
|
|
21
|
+
"free-trial-abuse",
|
|
22
|
+
"rate-limiting",
|
|
23
|
+
"abuse-prevention",
|
|
24
|
+
"jwt",
|
|
25
|
+
"fastapi",
|
|
26
|
+
"flask",
|
|
27
|
+
"django",
|
|
28
|
+
]
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 4 - Beta",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Topic :: Security",
|
|
35
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
36
|
+
"Framework :: FastAPI",
|
|
37
|
+
"Framework :: Flask",
|
|
38
|
+
"Framework :: Django",
|
|
39
|
+
]
|
|
40
|
+
dependencies = [
|
|
41
|
+
"PyJWT[crypto]>=2.0.0",
|
|
42
|
+
"requests>=2.20.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://www.scopeblind.com"
|
|
47
|
+
Documentation = "https://www.scopeblind.com/docs"
|
|
48
|
+
Repository = "https://github.com/nicosxt/scopeblind-gateway"
|
|
49
|
+
"AI Integration" = "https://www.scopeblind.com/llms.txt"
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScopeBlind — API abuse protection using cryptographic device proofs (VOPRF).
|
|
3
|
+
|
|
4
|
+
Replaces CAPTCHAs. No PII, no friction, ~20ms at the edge.
|
|
5
|
+
Works with browsers, AI agents, and CLIs.
|
|
6
|
+
|
|
7
|
+
Quick start:
|
|
8
|
+
pip install scopeblind
|
|
9
|
+
|
|
10
|
+
1. Add the client script to your HTML <head>:
|
|
11
|
+
<script async src="https://api.scopeblind.com/sb/{slug}.js"></script>
|
|
12
|
+
|
|
13
|
+
2. Verify the proof server-side:
|
|
14
|
+
from scopeblind import verify_token
|
|
15
|
+
claims = verify_token(token)
|
|
16
|
+
device_id = claims["sub"] # unique, non-PII device hash
|
|
17
|
+
|
|
18
|
+
3. That's it. Bots get blocked. Real users pass through.
|
|
19
|
+
|
|
20
|
+
Get your slug: POST https://api.scopeblind.com/provision
|
|
21
|
+
Full docs: https://www.scopeblind.com/llms.txt
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"verify_token",
|
|
27
|
+
"provision",
|
|
28
|
+
"ScopeBlindError",
|
|
29
|
+
"require_scopeblind",
|
|
30
|
+
"require_scopeblind_flask",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
import jwt
|
|
34
|
+
from jwt import PyJWKClient
|
|
35
|
+
import requests
|
|
36
|
+
from functools import wraps
|
|
37
|
+
from typing import Optional, Dict, Any
|
|
38
|
+
|
|
39
|
+
# ============================================================
|
|
40
|
+
# Constants
|
|
41
|
+
# ============================================================
|
|
42
|
+
|
|
43
|
+
DEFAULT_JWKS_URL = "https://api.scopeblind.com/.well-known/jwks.json"
|
|
44
|
+
DEFAULT_COOKIE_NAME = "sb_token"
|
|
45
|
+
DEFAULT_HEADER_NAME = "x-scopeblind-token"
|
|
46
|
+
|
|
47
|
+
# JWKS client (cached automatically by PyJWKClient)
|
|
48
|
+
_jwks_clients: Dict[str, PyJWKClient] = {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_jwks_client(url: str = DEFAULT_JWKS_URL) -> PyJWKClient:
|
|
52
|
+
if url not in _jwks_clients:
|
|
53
|
+
_jwks_clients[url] = PyJWKClient(url)
|
|
54
|
+
return _jwks_clients[url]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ============================================================
|
|
58
|
+
# Exceptions
|
|
59
|
+
# ============================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ScopeBlindError(Exception):
|
|
63
|
+
"""Raised when ScopeBlind verification fails."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ============================================================
|
|
68
|
+
# Core Verification
|
|
69
|
+
# ============================================================
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def verify_token(
|
|
73
|
+
token: str,
|
|
74
|
+
*,
|
|
75
|
+
jwks_url: str = DEFAULT_JWKS_URL,
|
|
76
|
+
) -> Dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Verify a ScopeBlind JWT token and return the decoded claims.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
token: The JWT string from the sb_token cookie or x-scopeblind-token header.
|
|
82
|
+
jwks_url: Custom JWKS URL (defaults to ScopeBlind production).
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dict with claims including 'sub' (unique device hash), 'slug', 'iat', 'exp'.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ScopeBlindError: If the token is invalid, expired, or verification fails.
|
|
89
|
+
|
|
90
|
+
Example::
|
|
91
|
+
|
|
92
|
+
from scopeblind import verify_token
|
|
93
|
+
|
|
94
|
+
token = request.cookies.get("sb_token")
|
|
95
|
+
try:
|
|
96
|
+
claims = verify_token(token)
|
|
97
|
+
device_id = claims["sub"] # unique, non-PII device hash
|
|
98
|
+
except ScopeBlindError:
|
|
99
|
+
# Handle unverified device
|
|
100
|
+
pass
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
client = _get_jwks_client(jwks_url)
|
|
104
|
+
signing_key = client.get_signing_key_from_jwt(token)
|
|
105
|
+
claims = jwt.decode(
|
|
106
|
+
token,
|
|
107
|
+
signing_key.key,
|
|
108
|
+
algorithms=["EdDSA"],
|
|
109
|
+
issuer="scopeblind",
|
|
110
|
+
)
|
|
111
|
+
return claims
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise ScopeBlindError(f"Token verification failed: {e}") from e
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ============================================================
|
|
117
|
+
# FastAPI Dependency
|
|
118
|
+
# ============================================================
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def require_scopeblind(
|
|
122
|
+
on_fail: str = "block",
|
|
123
|
+
cookie_name: str = DEFAULT_COOKIE_NAME,
|
|
124
|
+
header_name: str = DEFAULT_HEADER_NAME,
|
|
125
|
+
jwks_url: str = DEFAULT_JWKS_URL,
|
|
126
|
+
):
|
|
127
|
+
"""
|
|
128
|
+
FastAPI dependency that verifies ScopeBlind device proofs.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
on_fail: What to do when verification fails.
|
|
132
|
+
- 'block': Raise 403 (default, recommended for signup/trial)
|
|
133
|
+
- 'flag': Continue but set verified=False
|
|
134
|
+
- 'allow': Skip verification entirely
|
|
135
|
+
cookie_name: Cookie name containing the JWT (default: 'sb_token').
|
|
136
|
+
header_name: Header name for JWT fallback (default: 'x-scopeblind-token').
|
|
137
|
+
jwks_url: Custom JWKS endpoint URL.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A FastAPI dependency function.
|
|
141
|
+
|
|
142
|
+
Example::
|
|
143
|
+
|
|
144
|
+
from fastapi import FastAPI, Depends
|
|
145
|
+
from scopeblind import require_scopeblind
|
|
146
|
+
|
|
147
|
+
app = FastAPI()
|
|
148
|
+
|
|
149
|
+
@app.post("/api/signup")
|
|
150
|
+
async def signup(sb=Depends(require_scopeblind())):
|
|
151
|
+
if sb["verified"]:
|
|
152
|
+
device_id = sb["device_id"]
|
|
153
|
+
# This request is from a verified, unique device
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
async def _dependency(request):
|
|
157
|
+
from fastapi import HTTPException
|
|
158
|
+
|
|
159
|
+
# Extract token from cookie or header
|
|
160
|
+
token = request.cookies.get(cookie_name) or request.headers.get(header_name)
|
|
161
|
+
|
|
162
|
+
if not token:
|
|
163
|
+
if on_fail == "block":
|
|
164
|
+
raise HTTPException(
|
|
165
|
+
status_code=403,
|
|
166
|
+
detail={
|
|
167
|
+
"error": "device_not_verified",
|
|
168
|
+
"message": "ScopeBlind device proof required. See https://www.scopeblind.com/llms.txt",
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
return {"verified": False, "payload": None, "device_id": None}
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
claims = verify_token(token, jwks_url=jwks_url)
|
|
175
|
+
return {
|
|
176
|
+
"verified": True,
|
|
177
|
+
"payload": claims,
|
|
178
|
+
"device_id": claims.get("sub"),
|
|
179
|
+
}
|
|
180
|
+
except ScopeBlindError:
|
|
181
|
+
if on_fail == "block":
|
|
182
|
+
raise HTTPException(
|
|
183
|
+
status_code=403,
|
|
184
|
+
detail={
|
|
185
|
+
"error": "invalid_proof",
|
|
186
|
+
"message": "ScopeBlind device proof invalid. See https://www.scopeblind.com/llms.txt",
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
return {"verified": False, "payload": None, "device_id": None}
|
|
190
|
+
|
|
191
|
+
return _dependency
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ============================================================
|
|
195
|
+
# Flask Decorator
|
|
196
|
+
# ============================================================
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def require_scopeblind_flask(
|
|
200
|
+
on_fail: str = "block",
|
|
201
|
+
cookie_name: str = DEFAULT_COOKIE_NAME,
|
|
202
|
+
header_name: str = DEFAULT_HEADER_NAME,
|
|
203
|
+
jwks_url: str = DEFAULT_JWKS_URL,
|
|
204
|
+
):
|
|
205
|
+
"""
|
|
206
|
+
Flask decorator that verifies ScopeBlind device proofs.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
on_fail: 'block' (403), 'flag' (continue with verified=False), or 'allow'.
|
|
210
|
+
cookie_name: Cookie name containing the JWT (default: 'sb_token').
|
|
211
|
+
header_name: Header name for JWT fallback (default: 'x-scopeblind-token').
|
|
212
|
+
jwks_url: Custom JWKS endpoint URL.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A decorator function.
|
|
216
|
+
|
|
217
|
+
Example::
|
|
218
|
+
|
|
219
|
+
from flask import Flask, g
|
|
220
|
+
from scopeblind import require_scopeblind_flask
|
|
221
|
+
|
|
222
|
+
app = Flask(__name__)
|
|
223
|
+
|
|
224
|
+
@app.route("/api/signup", methods=["POST"])
|
|
225
|
+
@require_scopeblind_flask()
|
|
226
|
+
def signup():
|
|
227
|
+
if g.scopeblind["verified"]:
|
|
228
|
+
device_id = g.scopeblind["device_id"]
|
|
229
|
+
# This request is from a verified, unique device
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def decorator(f):
|
|
233
|
+
@wraps(f)
|
|
234
|
+
def decorated_function(*args, **kwargs):
|
|
235
|
+
from flask import request, jsonify, g
|
|
236
|
+
|
|
237
|
+
token = request.cookies.get(cookie_name) or request.headers.get(
|
|
238
|
+
header_name
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not token:
|
|
242
|
+
if on_fail == "block":
|
|
243
|
+
return (
|
|
244
|
+
jsonify(
|
|
245
|
+
{
|
|
246
|
+
"error": "device_not_verified",
|
|
247
|
+
"message": "ScopeBlind device proof required. See https://www.scopeblind.com/llms.txt",
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
403,
|
|
251
|
+
)
|
|
252
|
+
g.scopeblind = {"verified": False, "payload": None, "device_id": None}
|
|
253
|
+
return f(*args, **kwargs)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
claims = verify_token(token, jwks_url=jwks_url)
|
|
257
|
+
g.scopeblind = {
|
|
258
|
+
"verified": True,
|
|
259
|
+
"payload": claims,
|
|
260
|
+
"device_id": claims.get("sub"),
|
|
261
|
+
}
|
|
262
|
+
except ScopeBlindError:
|
|
263
|
+
if on_fail == "block":
|
|
264
|
+
return (
|
|
265
|
+
jsonify(
|
|
266
|
+
{
|
|
267
|
+
"error": "invalid_proof",
|
|
268
|
+
"message": "ScopeBlind device proof invalid. See https://www.scopeblind.com/llms.txt",
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
403,
|
|
272
|
+
)
|
|
273
|
+
g.scopeblind = {"verified": False, "payload": None, "device_id": None}
|
|
274
|
+
|
|
275
|
+
return f(*args, **kwargs)
|
|
276
|
+
|
|
277
|
+
return decorated_function
|
|
278
|
+
|
|
279
|
+
return decorator
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ============================================================
|
|
283
|
+
# Provision Helper
|
|
284
|
+
# ============================================================
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def provision(
|
|
288
|
+
target_url: str,
|
|
289
|
+
email: Optional[str] = None,
|
|
290
|
+
api_url: str = "https://api.scopeblind.com",
|
|
291
|
+
) -> Dict[str, str]:
|
|
292
|
+
"""
|
|
293
|
+
Provision a new ScopeBlind tenant programmatically.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
target_url: The API endpoint URL you want to protect.
|
|
297
|
+
email: Optional contact email for dashboard and abuse reports.
|
|
298
|
+
api_url: Custom API URL (defaults to production).
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Dict with keys: slug, mgmt_token, verifier_url, script_tag, dashboard_url.
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
ScopeBlindError: If provisioning fails.
|
|
305
|
+
|
|
306
|
+
Example::
|
|
307
|
+
|
|
308
|
+
from scopeblind import provision
|
|
309
|
+
|
|
310
|
+
tenant = provision(
|
|
311
|
+
target_url="https://myapp.com/api/signup",
|
|
312
|
+
email="dev@myapp.com"
|
|
313
|
+
)
|
|
314
|
+
print(tenant["slug"]) # 'a1b2c3d4e5f6'
|
|
315
|
+
print(tenant["script_tag"]) # '<script async src="...">'
|
|
316
|
+
"""
|
|
317
|
+
payload: Dict[str, Any] = {"target_url": target_url}
|
|
318
|
+
if email:
|
|
319
|
+
payload["email"] = email
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
resp = requests.post(
|
|
323
|
+
f"{api_url}/provision",
|
|
324
|
+
json=payload,
|
|
325
|
+
headers={"Content-Type": "application/json"},
|
|
326
|
+
timeout=30,
|
|
327
|
+
)
|
|
328
|
+
resp.raise_for_status()
|
|
329
|
+
data = resp.json()
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
"slug": data["slug"],
|
|
333
|
+
"mgmt_token": data["mgmt_token"],
|
|
334
|
+
"verifier_url": data["verifier_url"],
|
|
335
|
+
"script_tag": f'<script async src="{api_url}/sb/{data["slug"]}.js"></script>',
|
|
336
|
+
"dashboard_url": f"https://scopeblind.com/t/{data['slug']}",
|
|
337
|
+
}
|
|
338
|
+
except requests.RequestException as e:
|
|
339
|
+
raise ScopeBlindError(f"Provisioning failed: {e}") from e
|