xlock-py 0.1.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,36 @@
1
+ node_modules/
2
+ *.pyc
3
+ __pycache__/
4
+ .env
5
+ .env.local
6
+ *.db
7
+ dist/
8
+ bun.lock
9
+ xlock-server
10
+ server/xlock-server
11
+ server/server
12
+
13
+ # OS
14
+ .DS_Store
15
+ Thumbs.db
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # WASM build tooling (cloned repos, venvs)
25
+ compiler/wasm/wasmixer/
26
+ compiler/wasm/.venv/
27
+
28
+ # WASM intermediate build artifacts (keep .wasm outputs)
29
+ compiler/wasm/build/*.wat
30
+ compiler/wasm/build/*.d.ts
31
+ compiler/wasm/build/*.js
32
+ .claude/worktrees/
33
+ capture.html
34
+ mouse_capture.js
35
+ seo-outreach-training.html
36
+ test_cloakbrowser.ts
xlock_py-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 x-lock
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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: xlock-py
3
+ Version: 0.1.0
4
+ Summary: x-lock bot protection middleware for Python — invisible alternative to reCAPTCHA
5
+ Project-URL: Homepage, https://x-lock.dev
6
+ Project-URL: Repository, https://github.com/x-lock-dev/xlock-python
7
+ Project-URL: Issues, https://github.com/x-lock-dev/xlock-python/issues
8
+ Author-email: x-lock <dev@x-lock.dev>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bot-protection,captcha,django,fastapi,flask,middleware,proof-of-work,recaptcha-alternative,xlock
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: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
26
+ Classifier: Topic :: Security
27
+ Requires-Python: >=3.8
28
+ Provides-Extra: all
29
+ Requires-Dist: flask>=2.0.0; extra == 'all'
30
+ Requires-Dist: httpx>=0.24.0; extra == 'all'
31
+ Requires-Dist: starlette>=0.20.0; extra == 'all'
32
+ Provides-Extra: django
33
+ Provides-Extra: fastapi
34
+ Requires-Dist: httpx>=0.24.0; extra == 'fastapi'
35
+ Requires-Dist: starlette>=0.20.0; extra == 'fastapi'
36
+ Provides-Extra: flask
37
+ Requires-Dist: flask>=2.0.0; extra == 'flask'
38
+ Requires-Dist: httpx>=0.24.0; extra == 'flask'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # xlock-py
42
+
43
+ Invisible bot protection middleware for Python. Drop-in replacement for reCAPTCHA, Turnstile, and hCaptcha — no visual challenges, no user friction.
44
+
45
+ Supports **FastAPI**, **Django**, and **Flask**.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ # Core (Django — no extra deps)
51
+ pip install xlock-py
52
+
53
+ # FastAPI / Starlette
54
+ pip install xlock-py[fastapi]
55
+
56
+ # Flask
57
+ pip install xlock-py[flask]
58
+
59
+ # Everything
60
+ pip install xlock-py[all]
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ ### FastAPI / Starlette
66
+
67
+ ```python
68
+ from fastapi import FastAPI
69
+ from xlock import XLockMiddleware
70
+
71
+ app = FastAPI()
72
+ app.add_middleware(
73
+ XLockMiddleware,
74
+ site_key="sk_...",
75
+ protected_paths=["/api/auth", "/api/checkout"],
76
+ )
77
+ ```
78
+
79
+ ### Django
80
+
81
+ ```python
82
+ # settings.py
83
+ MIDDLEWARE = [
84
+ "xlock.XLockDjangoMiddleware",
85
+ # ... other middleware
86
+ ]
87
+
88
+ XLOCK_SITE_KEY = "sk_..."
89
+ XLOCK_PROTECTED_PATHS = ["/api/auth/", "/api/checkout/"]
90
+ ```
91
+
92
+ ### Flask
93
+
94
+ ```python
95
+ from flask import Flask
96
+ from xlock import XLockFlask
97
+
98
+ app = Flask(__name__)
99
+ xlock = XLockFlask(app, site_key="sk_...", protected_paths=["/api/auth"])
100
+ ```
101
+
102
+ ### Direct Verification
103
+
104
+ ```python
105
+ from xlock import verify
106
+
107
+ result = verify(token="v3.abc123...", site_key="sk_...", path="/api/login")
108
+ if result.blocked:
109
+ print(f"Blocked: {result.reason}")
110
+ ```
111
+
112
+ ## Configuration
113
+
114
+ | Option | Env Var | Default | Description |
115
+ |--------|---------|---------|-------------|
116
+ | `site_key` | `XLOCK_SITE_KEY` | — | Your x-lock site key |
117
+ | `api_url` | `XLOCK_API_URL` | `https://api.x-lock.dev` | API endpoint |
118
+ | `fail_open` | `XLOCK_FAIL_OPEN` | `True` | Allow requests on API errors |
119
+ | `protected_paths` | `XLOCK_PROTECTED_PATHS` | `[]` | Path prefixes to protect |
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,83 @@
1
+ # xlock-py
2
+
3
+ Invisible bot protection middleware for Python. Drop-in replacement for reCAPTCHA, Turnstile, and hCaptcha — no visual challenges, no user friction.
4
+
5
+ Supports **FastAPI**, **Django**, and **Flask**.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # Core (Django — no extra deps)
11
+ pip install xlock-py
12
+
13
+ # FastAPI / Starlette
14
+ pip install xlock-py[fastapi]
15
+
16
+ # Flask
17
+ pip install xlock-py[flask]
18
+
19
+ # Everything
20
+ pip install xlock-py[all]
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### FastAPI / Starlette
26
+
27
+ ```python
28
+ from fastapi import FastAPI
29
+ from xlock import XLockMiddleware
30
+
31
+ app = FastAPI()
32
+ app.add_middleware(
33
+ XLockMiddleware,
34
+ site_key="sk_...",
35
+ protected_paths=["/api/auth", "/api/checkout"],
36
+ )
37
+ ```
38
+
39
+ ### Django
40
+
41
+ ```python
42
+ # settings.py
43
+ MIDDLEWARE = [
44
+ "xlock.XLockDjangoMiddleware",
45
+ # ... other middleware
46
+ ]
47
+
48
+ XLOCK_SITE_KEY = "sk_..."
49
+ XLOCK_PROTECTED_PATHS = ["/api/auth/", "/api/checkout/"]
50
+ ```
51
+
52
+ ### Flask
53
+
54
+ ```python
55
+ from flask import Flask
56
+ from xlock import XLockFlask
57
+
58
+ app = Flask(__name__)
59
+ xlock = XLockFlask(app, site_key="sk_...", protected_paths=["/api/auth"])
60
+ ```
61
+
62
+ ### Direct Verification
63
+
64
+ ```python
65
+ from xlock import verify
66
+
67
+ result = verify(token="v3.abc123...", site_key="sk_...", path="/api/login")
68
+ if result.blocked:
69
+ print(f"Blocked: {result.reason}")
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ | Option | Env Var | Default | Description |
75
+ |--------|---------|---------|-------------|
76
+ | `site_key` | `XLOCK_SITE_KEY` | — | Your x-lock site key |
77
+ | `api_url` | `XLOCK_API_URL` | `https://api.x-lock.dev` | API endpoint |
78
+ | `fail_open` | `XLOCK_FAIL_OPEN` | `True` | Allow requests on API errors |
79
+ | `protected_paths` | `XLOCK_PROTECTED_PATHS` | `[]` | Path prefixes to protect |
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "xlock-py"
7
+ version = "0.1.0"
8
+ description = "x-lock bot protection middleware for Python — invisible alternative to reCAPTCHA"
9
+ requires-python = ">=3.8"
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ authors = [{ name = "x-lock", email = "dev@x-lock.dev" }]
13
+ keywords = [
14
+ "xlock",
15
+ "bot-protection",
16
+ "captcha",
17
+ "middleware",
18
+ "fastapi",
19
+ "django",
20
+ "flask",
21
+ "recaptcha-alternative",
22
+ "proof-of-work",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Framework :: Django",
27
+ "Framework :: FastAPI",
28
+ "Framework :: Flask",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.8",
33
+ "Programming Language :: Python :: 3.9",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
39
+ "Topic :: Security",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ fastapi = ["httpx>=0.24.0", "starlette>=0.20.0"]
44
+ flask = ["httpx>=0.24.0", "flask>=2.0.0"]
45
+ django = []
46
+ all = ["httpx>=0.24.0", "starlette>=0.20.0", "flask>=2.0.0"]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["xlock"]
50
+
51
+ [project.urls]
52
+ Homepage = "https://x-lock.dev"
53
+ Repository = "https://github.com/x-lock-dev/xlock-python"
54
+ Issues = "https://github.com/x-lock-dev/xlock-python/issues"
@@ -0,0 +1,38 @@
1
+ """
2
+ xlock — invisible bot protection middleware for Python
3
+
4
+ Supports FastAPI/Starlette, Django, and Flask.
5
+
6
+ FastAPI:
7
+ from xlock import XLockMiddleware
8
+ app.add_middleware(XLockMiddleware, site_key="sk_...", protected_paths=["/api/auth"])
9
+
10
+ Django (settings.py):
11
+ MIDDLEWARE = ["xlock.XLockDjangoMiddleware", ...]
12
+ XLOCK_SITE_KEY = "sk_..."
13
+ XLOCK_PROTECTED_PATHS = ["/api/auth/"]
14
+
15
+ Flask:
16
+ from xlock import XLockFlask
17
+ xlock = XLockFlask(app, site_key="sk_...", protected_paths=["/api/auth"])
18
+ """
19
+
20
+ from xlock.middleware import XLockDjangoMiddleware, XLockFlask
21
+ from xlock.verify import verify, verify_async
22
+
23
+ __all__ = [
24
+ "XLockMiddleware",
25
+ "XLockDjangoMiddleware",
26
+ "XLockFlask",
27
+ "verify",
28
+ "verify_async",
29
+ ]
30
+ __version__ = "0.1.0"
31
+
32
+
33
+ def __getattr__(name: str):
34
+ if name == "XLockMiddleware":
35
+ from xlock.middleware import XLockMiddleware
36
+
37
+ return XLockMiddleware
38
+ raise AttributeError(f"module 'xlock' has no attribute {name!r}")
@@ -0,0 +1,282 @@
1
+ """
2
+ x-lock middleware implementations for FastAPI/Starlette, Django, and Flask.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import urllib.request
9
+ import urllib.error
10
+
11
+ logger = logging.getLogger("x-lock")
12
+
13
+ DEFAULT_API_URL = "https://api.x-lock.dev"
14
+
15
+
16
+ # ─── FastAPI / Starlette ───
17
+
18
+ try:
19
+ from starlette.middleware.base import BaseHTTPMiddleware
20
+ from starlette.requests import Request
21
+ from starlette.responses import JSONResponse
22
+ import httpx
23
+
24
+ class XLockMiddleware(BaseHTTPMiddleware):
25
+ """
26
+ x-lock middleware for FastAPI / Starlette.
27
+
28
+ Usage:
29
+ from xlock import XLockMiddleware
30
+ app.add_middleware(
31
+ XLockMiddleware,
32
+ site_key="sk_...",
33
+ protected_paths=["/api/auth", "/api/checkout"],
34
+ )
35
+ """
36
+
37
+ def __init__(self, app, **kwargs):
38
+ super().__init__(app)
39
+ self.api_url = kwargs.get("api_url", DEFAULT_API_URL)
40
+ self.site_key = kwargs.get("site_key") or os.environ.get("XLOCK_SITE_KEY")
41
+ self.fail_open = kwargs.get("fail_open", True)
42
+ self.protected_paths: list = kwargs.get("protected_paths", [])
43
+
44
+ if not self.site_key:
45
+ logger.warning("No site key configured — skipping enforcement")
46
+
47
+ def _matches(self, path: str) -> bool:
48
+ return any(path.startswith(p) for p in self.protected_paths)
49
+
50
+ async def dispatch(self, request: Request, call_next):
51
+ if not self.site_key:
52
+ return await call_next(request)
53
+ if request.method != "POST":
54
+ return await call_next(request)
55
+ if self.protected_paths and not self._matches(request.url.path):
56
+ return await call_next(request)
57
+
58
+ token = request.headers.get("x-lock")
59
+
60
+ if not token:
61
+ return JSONResponse(
62
+ {"error": "Blocked by x-lock: missing token"}, status_code=403
63
+ )
64
+
65
+ try:
66
+ if token.startswith("v3."):
67
+ session_id = token.split(".")[1]
68
+ enforce_url = f"{self.api_url}/v3/session/enforce"
69
+ enforce_body = {"sessionId": session_id, "siteKey": self.site_key, "path": str(request.url.path)}
70
+ else:
71
+ enforce_url = f"{self.api_url}/v1/enforce"
72
+ enforce_body = {"token": token, "siteKey": self.site_key, "path": str(request.url.path)}
73
+
74
+ async with httpx.AsyncClient(timeout=5) as client:
75
+ res = await client.post(
76
+ enforce_url,
77
+ json=enforce_body,
78
+ )
79
+
80
+ if res.status_code == 403:
81
+ data = res.json()
82
+ return JSONResponse(
83
+ {"error": "Blocked by x-lock", "reason": data.get("reason")},
84
+ status_code=403,
85
+ )
86
+
87
+ if not res.is_success and not self.fail_open:
88
+ return JSONResponse(
89
+ {"error": "x-lock verification failed"}, status_code=403
90
+ )
91
+
92
+ except Exception as e:
93
+ logger.error(f"Enforcement error: {e}")
94
+ if not self.fail_open:
95
+ return JSONResponse(
96
+ {"error": "x-lock verification failed"}, status_code=403
97
+ )
98
+
99
+ return await call_next(request)
100
+
101
+ except ImportError:
102
+ # Starlette/httpx not installed — provide a stub
103
+ class XLockMiddleware: # type: ignore[no-redef]
104
+ """Stub — install starlette and httpx for FastAPI support."""
105
+
106
+ def __init__(self, *args, **kwargs):
107
+ raise ImportError(
108
+ "Install starlette and httpx for FastAPI/Starlette support: "
109
+ "pip install xlock[fastapi]"
110
+ )
111
+
112
+
113
+ # ─── Django ───
114
+
115
+ class XLockDjangoMiddleware:
116
+ """
117
+ x-lock middleware for Django.
118
+
119
+ Usage (settings.py):
120
+ MIDDLEWARE = ["xlock.XLockDjangoMiddleware", ...]
121
+ XLOCK_SITE_KEY = "sk_..."
122
+ XLOCK_PROTECTED_PATHS = ["/api/auth/", "/api/checkout/"]
123
+ XLOCK_API_URL = "https://api.x-lock.dev" # optional
124
+ XLOCK_FAIL_OPEN = True # optional
125
+ """
126
+
127
+ def __init__(self, get_response):
128
+ self.get_response = get_response
129
+
130
+ try:
131
+ from django.conf import settings as django_settings
132
+
133
+ self.api_url = getattr(django_settings, "XLOCK_API_URL", DEFAULT_API_URL)
134
+ self.site_key = getattr(django_settings, "XLOCK_SITE_KEY", None) or os.environ.get("XLOCK_SITE_KEY")
135
+ self.fail_open = getattr(django_settings, "XLOCK_FAIL_OPEN", True)
136
+ self.protected_paths = getattr(django_settings, "XLOCK_PROTECTED_PATHS", [])
137
+ except Exception:
138
+ self.api_url = os.environ.get("XLOCK_API_URL", DEFAULT_API_URL)
139
+ self.site_key = os.environ.get("XLOCK_SITE_KEY")
140
+ self.fail_open = True
141
+ self.protected_paths = []
142
+
143
+ if not self.site_key:
144
+ logger.warning("No site key configured — skipping enforcement")
145
+
146
+ def __call__(self, request):
147
+ if self.site_key and request.method == "POST":
148
+ if not self.protected_paths or self._matches(request.path):
149
+ result = self._enforce(request)
150
+ if result is not None:
151
+ return result
152
+
153
+ return self.get_response(request)
154
+
155
+ def _matches(self, path: str) -> bool:
156
+ return any(path.startswith(p) for p in self.protected_paths)
157
+
158
+ def _enforce(self, request):
159
+ try:
160
+ from django.http import JsonResponse
161
+ except ImportError:
162
+ return None
163
+
164
+ token = request.META.get("HTTP_X_LOCK")
165
+
166
+ if not token:
167
+ return JsonResponse({"error": "Blocked by x-lock: missing token"}, status=403)
168
+
169
+ try:
170
+ if token.startswith("v3."):
171
+ session_id = token.split(".")[1]
172
+ enforce_url = f"{self.api_url}/v3/session/enforce"
173
+ payload = json.dumps({"sessionId": session_id, "siteKey": self.site_key, "path": request.path}).encode()
174
+ else:
175
+ enforce_url = f"{self.api_url}/v1/enforce"
176
+ payload = json.dumps({"token": token, "siteKey": self.site_key, "path": request.path}).encode()
177
+
178
+ req = urllib.request.Request(
179
+ enforce_url,
180
+ data=payload,
181
+ headers={"Content-Type": "application/json"},
182
+ method="POST",
183
+ )
184
+
185
+ with urllib.request.urlopen(req, timeout=5) as res:
186
+ pass # 200 = allowed
187
+
188
+ except urllib.error.HTTPError as e:
189
+ if e.code == 403:
190
+ data = json.loads(e.read().decode())
191
+ return JsonResponse(
192
+ {"error": "Blocked by x-lock", "reason": data.get("reason")},
193
+ status=403,
194
+ )
195
+ if not self.fail_open:
196
+ return JsonResponse({"error": "x-lock verification failed"}, status=403)
197
+
198
+ except Exception as e:
199
+ logger.error(f"Enforcement error: {e}")
200
+ if not self.fail_open:
201
+ return JsonResponse({"error": "x-lock verification failed"}, status=403)
202
+
203
+ return None
204
+
205
+
206
+ # ─── Flask ───
207
+
208
+ class XLockFlask:
209
+ """
210
+ x-lock middleware for Flask.
211
+
212
+ Usage:
213
+ from xlock import XLockFlask
214
+ app = Flask(__name__)
215
+ xlock = XLockFlask(app, site_key="sk_...", protected_paths=["/api/auth"])
216
+ """
217
+
218
+ def __init__(self, app=None, **kwargs):
219
+ self.api_url = kwargs.get("api_url", DEFAULT_API_URL)
220
+ self.site_key = kwargs.get("site_key") or os.environ.get("XLOCK_SITE_KEY")
221
+ self.fail_open = kwargs.get("fail_open", True)
222
+ self.protected_paths: list = kwargs.get("protected_paths", [])
223
+
224
+ if app:
225
+ self.init_app(app)
226
+
227
+ def init_app(self, app):
228
+ if not self.site_key:
229
+ app.logger.warning("[x-lock] No site key configured — skipping enforcement")
230
+ return
231
+
232
+ app.before_request(self._enforce)
233
+
234
+ def _matches(self, path: str) -> bool:
235
+ return any(path.startswith(p) for p in self.protected_paths)
236
+
237
+ def _enforce(self):
238
+ try:
239
+ from flask import request, jsonify
240
+ except ImportError:
241
+ return None
242
+
243
+ if request.method != "POST":
244
+ return None
245
+ if self.protected_paths and not self._matches(request.path):
246
+ return None
247
+
248
+ token = request.headers.get("x-lock")
249
+
250
+ if not token:
251
+ return jsonify(error="Blocked by x-lock: missing token"), 403
252
+
253
+ try:
254
+ import httpx
255
+
256
+ if token.startswith("v3."):
257
+ session_id = token.split(".")[1]
258
+ enforce_url = f"{self.api_url}/v3/session/enforce"
259
+ enforce_body = {"sessionId": session_id, "siteKey": self.site_key, "path": request.path}
260
+ else:
261
+ enforce_url = f"{self.api_url}/v1/enforce"
262
+ enforce_body = {"token": token, "siteKey": self.site_key, "path": request.path}
263
+
264
+ with httpx.Client(timeout=5) as client:
265
+ res = client.post(
266
+ enforce_url,
267
+ json=enforce_body,
268
+ )
269
+
270
+ if res.status_code == 403:
271
+ data = res.json()
272
+ return jsonify(error="Blocked by x-lock", reason=data.get("reason")), 403
273
+
274
+ if not res.ok and not self.fail_open:
275
+ return jsonify(error="x-lock verification failed"), 403
276
+
277
+ except Exception as e:
278
+ logger.error(f"Enforcement error: {e}")
279
+ if not self.fail_open:
280
+ return jsonify(error="x-lock verification failed"), 403
281
+
282
+ return None
File without changes
@@ -0,0 +1,90 @@
1
+ """
2
+ Core x-lock verification — usable standalone without any framework.
3
+ """
4
+
5
+ import json
6
+ import urllib.request
7
+ import urllib.error
8
+ from typing import Optional
9
+
10
+ DEFAULT_API_URL = "https://api.x-lock.dev"
11
+
12
+
13
+ class VerifyResult:
14
+ __slots__ = ("blocked", "reason", "error")
15
+
16
+ def __init__(self, blocked: bool = False, reason: Optional[str] = None, error: bool = False):
17
+ self.blocked = blocked
18
+ self.reason = reason
19
+ self.error = error
20
+
21
+
22
+ def verify(
23
+ token: str,
24
+ site_key: str,
25
+ path: Optional[str] = None,
26
+ api_url: str = DEFAULT_API_URL,
27
+ ) -> VerifyResult:
28
+ """Synchronously verify an x-lock token. Works with stdlib only."""
29
+ if token.startswith("v3."):
30
+ session_id = token.split(".")[1]
31
+ enforce_url = f"{api_url}/v3/session/enforce"
32
+ body = {"sessionId": session_id, "siteKey": site_key}
33
+ else:
34
+ enforce_url = f"{api_url}/v1/enforce"
35
+ body = {"token": token, "siteKey": site_key}
36
+
37
+ if path:
38
+ body["path"] = path
39
+
40
+ payload = json.dumps(body).encode()
41
+ req = urllib.request.Request(
42
+ enforce_url,
43
+ data=payload,
44
+ headers={"Content-Type": "application/json"},
45
+ method="POST",
46
+ )
47
+
48
+ try:
49
+ with urllib.request.urlopen(req, timeout=5):
50
+ return VerifyResult(blocked=False)
51
+ except urllib.error.HTTPError as e:
52
+ if e.code == 403:
53
+ data = json.loads(e.read().decode())
54
+ return VerifyResult(blocked=True, reason=data.get("reason"))
55
+ return VerifyResult(blocked=False, error=True)
56
+ except Exception:
57
+ return VerifyResult(blocked=False, error=True)
58
+
59
+
60
+ async def verify_async(
61
+ token: str,
62
+ site_key: str,
63
+ path: Optional[str] = None,
64
+ api_url: str = DEFAULT_API_URL,
65
+ ) -> VerifyResult:
66
+ """Async verify using httpx. Requires: pip install xlock[fastapi]"""
67
+ import httpx
68
+
69
+ if token.startswith("v3."):
70
+ session_id = token.split(".")[1]
71
+ enforce_url = f"{api_url}/v3/session/enforce"
72
+ body = {"sessionId": session_id, "siteKey": site_key}
73
+ else:
74
+ enforce_url = f"{api_url}/v1/enforce"
75
+ body = {"token": token, "siteKey": site_key}
76
+
77
+ if path:
78
+ body["path"] = path
79
+
80
+ async with httpx.AsyncClient(timeout=5) as client:
81
+ res = await client.post(enforce_url, json=body)
82
+
83
+ if res.status_code == 403:
84
+ data = res.json()
85
+ return VerifyResult(blocked=True, reason=data.get("reason"))
86
+
87
+ if not res.is_success:
88
+ return VerifyResult(blocked=False, error=True)
89
+
90
+ return VerifyResult(blocked=False)