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.
- payfence-1.0.0/PKG-INFO +9 -0
- payfence-1.0.0/README.md +54 -0
- payfence-1.0.0/payfence/__init__.py +5 -0
- payfence-1.0.0/payfence/client.py +30 -0
- payfence-1.0.0/payfence/hmac_verify.py +33 -0
- payfence-1.0.0/payfence/middleware.py +83 -0
- payfence-1.0.0/payfence.egg-info/PKG-INFO +9 -0
- payfence-1.0.0/payfence.egg-info/SOURCES.txt +10 -0
- payfence-1.0.0/payfence.egg-info/dependency_links.txt +1 -0
- payfence-1.0.0/payfence.egg-info/top_level.txt +1 -0
- payfence-1.0.0/pyproject.toml +15 -0
- payfence-1.0.0/setup.cfg +4 -0
payfence-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
payfence-1.0.0/README.md
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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"
|
payfence-1.0.0/setup.cfg
ADDED