payfence 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,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: payfence
3
+ Version: 1.0.0
4
+ Summary: PayFence SDK for Python — API monetization middleware
5
+ License: MIT
6
+ Project-URL: Homepage, https://payfence.io
7
+ Project-URL: Documentation, https://docs.payfence.io
8
+ Keywords: payfence,api,monetization,middleware
9
+ Requires-Python: >=3.8
@@ -0,0 +1,54 @@
1
+ # payfence
2
+
3
+ Python SDK for API monetization via [PayFence](https://payfence.io).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install payfence
9
+ ```
10
+
11
+ ## ASGI Middleware (FastAPI / Starlette)
12
+
13
+ ```python
14
+ from fastapi import FastAPI
15
+ from payfence import PayFenceMiddleware
16
+
17
+ app = FastAPI()
18
+ app.add_middleware(PayFenceMiddleware, api_key="your_site_api_key")
19
+ ```
20
+
21
+ ## Direct Authorize Call
22
+
23
+ ```python
24
+ from payfence import authorize
25
+
26
+ decision = authorize(
27
+ api_key="your_site_api_key",
28
+ token="pf_live_xxx",
29
+ method="GET",
30
+ path="/api/search",
31
+ )
32
+
33
+ if decision["decision"] == "allow":
34
+ # serve the request
35
+ pass
36
+ ```
37
+
38
+ ## HMAC Verification (Proxy Mode)
39
+
40
+ ```python
41
+ from payfence import verify_signature
42
+
43
+ valid = verify_signature(
44
+ secret="your_origin_secret",
45
+ method=request.method,
46
+ path=request.url.path,
47
+ timestamp=request.headers["x-payfence-timestamp"],
48
+ request_id=request.headers["x-payfence-request-id"],
49
+ body=await request.body(),
50
+ signature=request.headers["x-payfence-signature"],
51
+ )
52
+ ```
53
+
54
+ Zero external dependencies — uses only Python stdlib.
@@ -0,0 +1,5 @@
1
+ from .client import authorize
2
+ from .middleware import PayFenceMiddleware
3
+ from .hmac_verify import verify_signature
4
+
5
+ __all__ = ["authorize", "PayFenceMiddleware", "verify_signature"]
@@ -0,0 +1,30 @@
1
+ import json
2
+ import uuid
3
+ from urllib.request import Request, urlopen
4
+ from urllib.error import URLError
5
+ from typing import Optional, Dict, Any
6
+
7
+
8
+ def authorize(
9
+ api_key: str,
10
+ token: str,
11
+ method: str,
12
+ path: str,
13
+ request_id: Optional[str] = None,
14
+ base_url: str = "https://api.payfence.io",
15
+ ) -> Dict[str, Any]:
16
+ """Call PayFence authorize endpoint. Returns decision dict."""
17
+ url = f"{base_url.rstrip('/')}/v1/authorize"
18
+ payload = json.dumps({
19
+ "token": token,
20
+ "method": method,
21
+ "path": path,
22
+ "requestId": request_id or str(uuid.uuid4()),
23
+ }).encode()
24
+
25
+ req = Request(url, data=payload, method="POST")
26
+ req.add_header("Content-Type", "application/json")
27
+ req.add_header("Authorization", f"Bearer {api_key}")
28
+
29
+ with urlopen(req, timeout=10) as resp:
30
+ return json.loads(resp.read())
@@ -0,0 +1,33 @@
1
+ import hashlib
2
+ import hmac
3
+ import time
4
+ from typing import Union
5
+
6
+
7
+ def verify_signature(
8
+ secret: str,
9
+ method: str,
10
+ path: str,
11
+ timestamp: str,
12
+ request_id: str,
13
+ body: Union[str, bytes],
14
+ signature: str,
15
+ tolerance_seconds: int = 300,
16
+ ) -> bool:
17
+ """Verify a PayFence HMAC signature from proxy mode."""
18
+ try:
19
+ ts = int(timestamp)
20
+ except (ValueError, TypeError):
21
+ return False
22
+
23
+ if abs(time.time() - ts) > tolerance_seconds:
24
+ return False
25
+
26
+ if isinstance(body, str):
27
+ body = body.encode()
28
+
29
+ body_hash = hashlib.sha256(body).hexdigest()
30
+ canonical = f"{method}\n{path}\n{timestamp}\n{request_id}\n{body_hash}"
31
+ expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
32
+
33
+ return hmac.compare_digest(signature, expected)
@@ -0,0 +1,83 @@
1
+ import json
2
+ import uuid
3
+ import logging
4
+ from typing import Optional, List, Callable
5
+ from .client import authorize
6
+
7
+ logger = logging.getLogger("payfence")
8
+
9
+
10
+ class PayFenceMiddleware:
11
+ """ASGI middleware for PayFence authorization."""
12
+
13
+ def __init__(
14
+ self,
15
+ app,
16
+ api_key: str,
17
+ base_url: str = "https://api.payfence.io",
18
+ exclude_paths: Optional[List[str]] = None,
19
+ on_deny: Optional[Callable] = None,
20
+ ):
21
+ self.app = app
22
+ self.api_key = api_key
23
+ self.base_url = base_url
24
+ self.exclude_paths = exclude_paths or []
25
+ self.on_deny = on_deny
26
+
27
+ async def __call__(self, scope, receive, send):
28
+ if scope["type"] != "http":
29
+ return await self.app(scope, receive, send)
30
+
31
+ path = scope.get("path", "/")
32
+
33
+ # Skip excluded paths
34
+ if any(path.startswith(p) for p in self.exclude_paths):
35
+ return await self.app(scope, receive, send)
36
+
37
+ # Extract bearer token from headers
38
+ headers = dict(scope.get("headers", []))
39
+ auth = headers.get(b"authorization", b"").decode()
40
+ token = auth[7:] if auth.startswith("Bearer ") else ""
41
+
42
+ if not token:
43
+ return await self._send_deny(send, {"decision": "deny", "reason": "payment_required", "message": "Missing Bearer token"})
44
+
45
+ try:
46
+ request_id = headers.get(b"x-request-id", b"").decode() or str(uuid.uuid4())
47
+ method = scope.get("method", "GET")
48
+
49
+ decision = authorize(
50
+ api_key=self.api_key,
51
+ token=token,
52
+ method=method,
53
+ path=path,
54
+ request_id=request_id,
55
+ base_url=self.base_url,
56
+ )
57
+
58
+ if decision.get("decision") == "allow":
59
+ # Attach decision to scope for downstream access
60
+ scope["payfence"] = decision
61
+ return await self.app(scope, receive, send)
62
+
63
+ # Deny
64
+ if self.on_deny:
65
+ return await self.on_deny(scope, receive, send, decision)
66
+ return await self._send_deny(send, decision)
67
+
68
+ except Exception as e:
69
+ # Fail open
70
+ logger.warning(f"PayFence authorize failed, failing open: {e}")
71
+ return await self.app(scope, receive, send)
72
+
73
+ async def _send_deny(self, send, decision):
74
+ body = json.dumps(decision).encode()
75
+ await send({
76
+ "type": "http.response.start",
77
+ "status": 402,
78
+ "headers": [[b"content-type", b"application/json"]],
79
+ })
80
+ await send({
81
+ "type": "http.response.body",
82
+ "body": body,
83
+ })
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: payfence
3
+ Version: 1.0.0
4
+ Summary: PayFence SDK for Python — API monetization middleware
5
+ License: MIT
6
+ Project-URL: Homepage, https://payfence.io
7
+ Project-URL: Documentation, https://docs.payfence.io
8
+ Keywords: payfence,api,monetization,middleware
9
+ Requires-Python: >=3.8
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ payfence/__init__.py
4
+ payfence/client.py
5
+ payfence/hmac_verify.py
6
+ payfence/middleware.py
7
+ payfence.egg-info/PKG-INFO
8
+ payfence.egg-info/SOURCES.txt
9
+ payfence.egg-info/dependency_links.txt
10
+ payfence.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ payfence
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "payfence"
7
+ version = "1.0.0"
8
+ description = "PayFence SDK for Python — API monetization middleware"
9
+ license = {text = "MIT"}
10
+ requires-python = ">=3.8"
11
+ keywords = ["payfence", "api", "monetization", "middleware"]
12
+
13
+ [project.urls]
14
+ Homepage = "https://payfence.io"
15
+ Documentation = "https://docs.payfence.io"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+