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.
@@ -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