foldset 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.
- foldset-0.1.0/.gitignore +40 -0
- foldset-0.1.0/PKG-INFO +21 -0
- foldset-0.1.0/foldset/__init__.py +172 -0
- foldset-0.1.0/foldset/api.py +36 -0
- foldset-0.1.0/foldset/config.py +183 -0
- foldset-0.1.0/foldset/handler.py +125 -0
- foldset-0.1.0/foldset/health.py +18 -0
- foldset-0.1.0/foldset/mcp.py +251 -0
- foldset-0.1.0/foldset/paywall.py +106 -0
- foldset-0.1.0/foldset/routes.py +61 -0
- foldset-0.1.0/foldset/server.py +147 -0
- foldset-0.1.0/foldset/store.py +45 -0
- foldset-0.1.0/foldset/telemetry.py +109 -0
- foldset-0.1.0/foldset/types.py +147 -0
- foldset-0.1.0/foldset/web.py +17 -0
- foldset-0.1.0/pyproject.toml +34 -0
foldset-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
.pnpm-store
|
|
4
|
+
|
|
5
|
+
# Build outputs
|
|
6
|
+
dist
|
|
7
|
+
# TODO: Remove these exceptions once packages are published to npm.
|
|
8
|
+
# Currently committing dist/ because Vercel doesn't run prepare scripts
|
|
9
|
+
# for git dependencies properly.
|
|
10
|
+
!packages/typescript/nextjs/dist
|
|
11
|
+
!packages/typescript/core/dist
|
|
12
|
+
build
|
|
13
|
+
.next
|
|
14
|
+
out
|
|
15
|
+
|
|
16
|
+
# Environment files
|
|
17
|
+
.env
|
|
18
|
+
.env.local
|
|
19
|
+
.env.*.local
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.idea
|
|
23
|
+
.vscode
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Debug logs
|
|
32
|
+
npm-debug.log*
|
|
33
|
+
pnpm-debug.log*
|
|
34
|
+
|
|
35
|
+
# TypeScript
|
|
36
|
+
*.tsbuildinfo
|
|
37
|
+
|
|
38
|
+
# Misc
|
|
39
|
+
*.log
|
|
40
|
+
.cache
|
foldset-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foldset
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core types and utilities for Foldset payment protection
|
|
5
|
+
Project-URL: Homepage, https://foldset.com
|
|
6
|
+
Project-URL: Documentation, https://docs.foldset.com
|
|
7
|
+
Project-URL: Repository, https://github.com/foldset/sdks
|
|
8
|
+
Author-email: Foldset <team@foldset.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,foldset,micropayments,stablecoin
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx>=0.28.0
|
|
20
|
+
Requires-Dist: upstash-redis>=1.0.0
|
|
21
|
+
Requires-Dist: x402[mechanisms]>=2.0.0
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
BotsManager,
|
|
5
|
+
CachedConfigManager,
|
|
6
|
+
FacilitatorManager,
|
|
7
|
+
HostConfigManager,
|
|
8
|
+
PaymentMethodsManager,
|
|
9
|
+
RestrictionsManager,
|
|
10
|
+
build_request_metadata,
|
|
11
|
+
)
|
|
12
|
+
from .handler import handle_request, handle_settlement
|
|
13
|
+
from .health import HEALTH_PATH, build_health_response
|
|
14
|
+
from .mcp import handle_mcp_request
|
|
15
|
+
from .server import HttpServerManager
|
|
16
|
+
from .store import create_redis_store, fetch_redis_credentials
|
|
17
|
+
from .types import (
|
|
18
|
+
ConfigStore,
|
|
19
|
+
FoldsetOptions,
|
|
20
|
+
ProcessRequestResult,
|
|
21
|
+
RedisCredentials,
|
|
22
|
+
RequestAdapter,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_cached_core: WorkerCore | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkerCore:
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
store: ConfigStore,
|
|
32
|
+
api_key: str,
|
|
33
|
+
platform: str,
|
|
34
|
+
sdk_version: str,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.host_config = HostConfigManager(store)
|
|
37
|
+
self.restrictions = RestrictionsManager(store)
|
|
38
|
+
self.payment_methods = PaymentMethodsManager(store)
|
|
39
|
+
self.bots = BotsManager(store)
|
|
40
|
+
self.api_key = api_key
|
|
41
|
+
self.http_server = HttpServerManager(store)
|
|
42
|
+
self.platform = platform
|
|
43
|
+
self.sdk_version = sdk_version
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
async def from_options(cls, options: FoldsetOptions) -> WorkerCore:
|
|
47
|
+
global _cached_core
|
|
48
|
+
if _cached_core:
|
|
49
|
+
return _cached_core
|
|
50
|
+
|
|
51
|
+
credentials = options.redis_credentials or await fetch_redis_credentials(
|
|
52
|
+
options.api_key
|
|
53
|
+
)
|
|
54
|
+
store = create_redis_store(credentials)
|
|
55
|
+
_cached_core = cls(
|
|
56
|
+
store,
|
|
57
|
+
options.api_key,
|
|
58
|
+
options.platform or "unknown",
|
|
59
|
+
options.sdk_version or "unknown",
|
|
60
|
+
)
|
|
61
|
+
return _cached_core
|
|
62
|
+
|
|
63
|
+
async def process_request(self, adapter: RequestAdapter) -> ProcessRequestResult:
|
|
64
|
+
metadata = build_request_metadata()
|
|
65
|
+
|
|
66
|
+
if adapter.get_path() == HEALTH_PATH:
|
|
67
|
+
return ProcessRequestResult(
|
|
68
|
+
type="health-check",
|
|
69
|
+
metadata=metadata,
|
|
70
|
+
response=type(
|
|
71
|
+
"HealthResponse",
|
|
72
|
+
(),
|
|
73
|
+
{
|
|
74
|
+
"status": 200,
|
|
75
|
+
"body": build_health_response(self.platform, self.sdk_version),
|
|
76
|
+
"headers": {"Content-Type": "application/json"},
|
|
77
|
+
},
|
|
78
|
+
)(),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
host_config = await self.host_config.get()
|
|
82
|
+
mcp_endpoint = host_config.mcp_endpoint if host_config else None
|
|
83
|
+
|
|
84
|
+
if mcp_endpoint and adapter.get_path() == mcp_endpoint:
|
|
85
|
+
return await handle_mcp_request(self, adapter, mcp_endpoint, metadata)
|
|
86
|
+
|
|
87
|
+
return await handle_request(self, adapter, metadata)
|
|
88
|
+
|
|
89
|
+
async def process_settlement(
|
|
90
|
+
self,
|
|
91
|
+
adapter: RequestAdapter,
|
|
92
|
+
payment_payload,
|
|
93
|
+
payment_requirements,
|
|
94
|
+
upstream_status_code: int,
|
|
95
|
+
request_id: str,
|
|
96
|
+
):
|
|
97
|
+
return await handle_settlement(
|
|
98
|
+
self,
|
|
99
|
+
adapter,
|
|
100
|
+
payment_payload,
|
|
101
|
+
payment_requirements,
|
|
102
|
+
upstream_status_code,
|
|
103
|
+
request_id,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Re-exports
|
|
108
|
+
__all__ = [
|
|
109
|
+
"WorkerCore",
|
|
110
|
+
# Types
|
|
111
|
+
"ConfigStore",
|
|
112
|
+
"FoldsetOptions",
|
|
113
|
+
"ProcessRequestResult",
|
|
114
|
+
"RedisCredentials",
|
|
115
|
+
"RequestAdapter",
|
|
116
|
+
# Store
|
|
117
|
+
"create_redis_store",
|
|
118
|
+
"fetch_redis_credentials",
|
|
119
|
+
# Paywall
|
|
120
|
+
"generate_paywall_html",
|
|
121
|
+
# Routes
|
|
122
|
+
"build_routes_config",
|
|
123
|
+
"price_to_amount",
|
|
124
|
+
# Config managers
|
|
125
|
+
"BotsManager",
|
|
126
|
+
"CachedConfigManager",
|
|
127
|
+
"FacilitatorManager",
|
|
128
|
+
"HostConfigManager",
|
|
129
|
+
"PaymentMethodsManager",
|
|
130
|
+
"RestrictionsManager",
|
|
131
|
+
# Server
|
|
132
|
+
"HttpServerManager",
|
|
133
|
+
# MCP
|
|
134
|
+
"build_json_rpc_error",
|
|
135
|
+
"build_mcp_route_key",
|
|
136
|
+
"build_mcp_routes_config",
|
|
137
|
+
"get_mcp_list_payment_requirements",
|
|
138
|
+
"get_mcp_route_key",
|
|
139
|
+
"handle_mcp_request",
|
|
140
|
+
"is_mcp_list_method",
|
|
141
|
+
"parse_mcp_request",
|
|
142
|
+
# Telemetry
|
|
143
|
+
"build_event_payload",
|
|
144
|
+
"log_event",
|
|
145
|
+
"report_error",
|
|
146
|
+
"send_event",
|
|
147
|
+
# Handlers
|
|
148
|
+
"handle_request",
|
|
149
|
+
"handle_settlement",
|
|
150
|
+
"format_api_payment_error",
|
|
151
|
+
"format_web_payment_error",
|
|
152
|
+
# Health
|
|
153
|
+
"HEALTH_PATH",
|
|
154
|
+
"build_health_response",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Lazy imports for __all__ items
|
|
158
|
+
from .paywall import generate_paywall_html # noqa: E402
|
|
159
|
+
from .routes import build_routes_config, price_to_amount # noqa: E402
|
|
160
|
+
from .mcp import ( # noqa: E402
|
|
161
|
+
build_json_rpc_error,
|
|
162
|
+
build_mcp_route_key,
|
|
163
|
+
build_mcp_routes_config,
|
|
164
|
+
get_mcp_list_payment_requirements,
|
|
165
|
+
get_mcp_route_key,
|
|
166
|
+
is_mcp_list_method,
|
|
167
|
+
parse_mcp_request,
|
|
168
|
+
)
|
|
169
|
+
from .telemetry import build_event_payload, log_event, report_error, send_event # noqa: E402
|
|
170
|
+
from .handler import handle_payment_request # noqa: E402
|
|
171
|
+
from .api import format_api_payment_error # noqa: E402
|
|
172
|
+
from .web import format_web_payment_error # noqa: E402
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from .types import ApiRestriction, PaymentMethod, ProcessRequestResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_api_payment_error(
|
|
9
|
+
result: ProcessRequestResult,
|
|
10
|
+
restriction: ApiRestriction,
|
|
11
|
+
payment_methods: list[PaymentMethod],
|
|
12
|
+
terms_of_service_url: str | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
body: dict = {
|
|
15
|
+
"error": "payment_required",
|
|
16
|
+
"version": result.metadata.version,
|
|
17
|
+
"request_id": result.metadata.request_id,
|
|
18
|
+
"timestamp": result.metadata.timestamp,
|
|
19
|
+
"message": restriction.description,
|
|
20
|
+
"price": restriction.price,
|
|
21
|
+
}
|
|
22
|
+
if terms_of_service_url:
|
|
23
|
+
body["terms_of_service_url"] = terms_of_service_url
|
|
24
|
+
body["payment_methods"] = [
|
|
25
|
+
{
|
|
26
|
+
"network": pm.caip2_id,
|
|
27
|
+
"asset": pm.contract_address,
|
|
28
|
+
"decimals": pm.decimals,
|
|
29
|
+
"pay_to": pm.circle_wallet_address,
|
|
30
|
+
"chain": pm.chain_display_name,
|
|
31
|
+
"asset_name": pm.asset_display_name,
|
|
32
|
+
}
|
|
33
|
+
for pm in payment_methods
|
|
34
|
+
]
|
|
35
|
+
result.response.body = json.dumps(body)
|
|
36
|
+
result.response.headers["Content-Type"] = "application/json"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from x402.http import FacilitatorConfig as X402FacilitatorConfig
|
|
10
|
+
from x402.http import HTTPFacilitatorClient
|
|
11
|
+
|
|
12
|
+
from .types import (
|
|
13
|
+
Bot,
|
|
14
|
+
ConfigStore,
|
|
15
|
+
FacilitatorConfig,
|
|
16
|
+
HostConfig,
|
|
17
|
+
PaymentMethod,
|
|
18
|
+
ProcessRequestResult,
|
|
19
|
+
RequestMetadata,
|
|
20
|
+
Restriction,
|
|
21
|
+
ApiRestriction,
|
|
22
|
+
McpRestriction,
|
|
23
|
+
WebRestriction,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from importlib.metadata import version as _pkg_version
|
|
27
|
+
|
|
28
|
+
PACKAGE_VERSION = _pkg_version("foldset")
|
|
29
|
+
CACHE_TTL_MS = 30_000
|
|
30
|
+
API_BASE_URL = "https://api.foldset.com"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_request_metadata() -> RequestMetadata:
|
|
34
|
+
return RequestMetadata(
|
|
35
|
+
version=PACKAGE_VERSION,
|
|
36
|
+
request_id=str(uuid.uuid4()),
|
|
37
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def no_payment_required(metadata: RequestMetadata) -> ProcessRequestResult:
|
|
42
|
+
return ProcessRequestResult(type="no-payment-required", metadata=metadata)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CachedConfigManager[T]:
|
|
46
|
+
def __init__(self, config_store: ConfigStore, key: str, fallback: T) -> None:
|
|
47
|
+
self._config_store = config_store
|
|
48
|
+
self._key = key
|
|
49
|
+
self._fallback = fallback
|
|
50
|
+
self._cached: T = fallback
|
|
51
|
+
self._cache_timestamp: float = 0
|
|
52
|
+
|
|
53
|
+
def _is_cache_valid(self) -> bool:
|
|
54
|
+
return self._cache_timestamp > 0 and (time.time() * 1000 - self._cache_timestamp) < CACHE_TTL_MS
|
|
55
|
+
|
|
56
|
+
def _deserialize(self, raw: str) -> T:
|
|
57
|
+
return json.loads(raw)
|
|
58
|
+
|
|
59
|
+
async def get(self) -> T:
|
|
60
|
+
if self._is_cache_valid():
|
|
61
|
+
return self._cached
|
|
62
|
+
raw = await self._config_store.get(self._key)
|
|
63
|
+
self._cached = self._deserialize(raw) if raw else self._fallback
|
|
64
|
+
self._cache_timestamp = time.time() * 1000
|
|
65
|
+
return self._cached
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_restriction(data: dict[str, Any]) -> Restriction:
|
|
69
|
+
rtype = data.get("type")
|
|
70
|
+
if rtype == "web":
|
|
71
|
+
return WebRestriction(
|
|
72
|
+
description=data["description"],
|
|
73
|
+
price=data["price"],
|
|
74
|
+
scheme=data["scheme"],
|
|
75
|
+
path=data.get("path", ""),
|
|
76
|
+
)
|
|
77
|
+
elif rtype == "api":
|
|
78
|
+
return ApiRestriction(
|
|
79
|
+
description=data["description"],
|
|
80
|
+
price=data["price"],
|
|
81
|
+
scheme=data["scheme"],
|
|
82
|
+
path=data.get("path", ""),
|
|
83
|
+
http_method=data.get("httpMethod"),
|
|
84
|
+
)
|
|
85
|
+
elif rtype == "mcp":
|
|
86
|
+
return McpRestriction(
|
|
87
|
+
description=data["description"],
|
|
88
|
+
price=data["price"],
|
|
89
|
+
scheme=data["scheme"],
|
|
90
|
+
method=data.get("method", ""),
|
|
91
|
+
name=data.get("name", ""),
|
|
92
|
+
)
|
|
93
|
+
raise ValueError(f"Unknown restriction type: {rtype}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class HostConfigManager(CachedConfigManager[HostConfig | None]):
|
|
97
|
+
def __init__(self, store: ConfigStore) -> None:
|
|
98
|
+
super().__init__(store, "host-config", None)
|
|
99
|
+
|
|
100
|
+
def _deserialize(self, raw: str) -> HostConfig | None:
|
|
101
|
+
data = json.loads(raw)
|
|
102
|
+
return HostConfig(
|
|
103
|
+
host=data["host"],
|
|
104
|
+
api_protection_mode=data.get("apiProtectionMode", "bots"),
|
|
105
|
+
mcp_endpoint=data.get("mcpEndpoint"),
|
|
106
|
+
terms_of_service_url=data.get("termsOfServiceUrl"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class RestrictionsManager(CachedConfigManager[list[Restriction]]):
|
|
111
|
+
def __init__(self, store: ConfigStore) -> None:
|
|
112
|
+
super().__init__(store, "restrictions", [])
|
|
113
|
+
|
|
114
|
+
def _deserialize(self, raw: str) -> list[Restriction]:
|
|
115
|
+
data = json.loads(raw)
|
|
116
|
+
return [_parse_restriction(r) for r in data]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PaymentMethodsManager(CachedConfigManager[list[PaymentMethod]]):
|
|
120
|
+
def __init__(self, store: ConfigStore) -> None:
|
|
121
|
+
super().__init__(store, "payment-methods", [])
|
|
122
|
+
|
|
123
|
+
def _deserialize(self, raw: str) -> list[PaymentMethod]:
|
|
124
|
+
data = json.loads(raw)
|
|
125
|
+
return [
|
|
126
|
+
PaymentMethod(
|
|
127
|
+
caip2_id=pm["caip2_id"],
|
|
128
|
+
decimals=pm["decimals"],
|
|
129
|
+
contract_address=pm["contract_address"],
|
|
130
|
+
circle_wallet_address=pm["circle_wallet_address"],
|
|
131
|
+
chain_display_name=pm["chain_display_name"],
|
|
132
|
+
asset_display_name=pm["asset_display_name"],
|
|
133
|
+
extra=pm.get("extra"),
|
|
134
|
+
)
|
|
135
|
+
for pm in data
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class BotsManager(CachedConfigManager[list[Bot]]):
|
|
140
|
+
def __init__(self, store: ConfigStore) -> None:
|
|
141
|
+
super().__init__(store, "bots", [])
|
|
142
|
+
|
|
143
|
+
def _deserialize(self, raw: str) -> list[Bot]:
|
|
144
|
+
data = json.loads(raw)
|
|
145
|
+
return [
|
|
146
|
+
Bot(
|
|
147
|
+
user_agent=b["user_agent"].lower(),
|
|
148
|
+
force_200=b.get("force_200", False),
|
|
149
|
+
)
|
|
150
|
+
for b in data
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
async def match_bot(self, user_agent: str) -> Bot | None:
|
|
154
|
+
bots = await self.get()
|
|
155
|
+
ua = user_agent.lower()
|
|
156
|
+
for bot in bots:
|
|
157
|
+
if bot.user_agent in ua:
|
|
158
|
+
return bot
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class FacilitatorManager(CachedConfigManager[HTTPFacilitatorClient | None]):
|
|
163
|
+
def __init__(self, store: ConfigStore) -> None:
|
|
164
|
+
super().__init__(store, "facilitator", None)
|
|
165
|
+
|
|
166
|
+
def _deserialize(self, raw: str) -> HTTPFacilitatorClient:
|
|
167
|
+
config = json.loads(raw)
|
|
168
|
+
|
|
169
|
+
has_auth_headers = (
|
|
170
|
+
config.get("verifyHeaders")
|
|
171
|
+
or config.get("settleHeaders")
|
|
172
|
+
or config.get("supportedHeaders")
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
facilitator_config: dict[str, Any] = {"url": config["url"]}
|
|
176
|
+
if has_auth_headers:
|
|
177
|
+
facilitator_config["create_headers"] = lambda: {
|
|
178
|
+
"verify": config.get("verifyHeaders") or {},
|
|
179
|
+
"settle": config.get("settleHeaders") or {},
|
|
180
|
+
"supported": config.get("supportedHeaders") or {},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return HTTPFacilitatorClient(facilitator_config)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from x402.http import HTTPRequestContext, ProcessSettleResult
|
|
6
|
+
|
|
7
|
+
from .api import format_api_payment_error
|
|
8
|
+
from .config import no_payment_required
|
|
9
|
+
from .telemetry import log_event
|
|
10
|
+
from .types import ProcessRequestResult, RequestAdapter, RequestMetadata
|
|
11
|
+
from .web import format_web_payment_error
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from . import WorkerCore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _settlement_failure(reason: str, network: str) -> ProcessSettleResult:
|
|
18
|
+
return ProcessSettleResult(success=False, error_reason=reason)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def handle_payment_request(
|
|
22
|
+
core: WorkerCore,
|
|
23
|
+
adapter: RequestAdapter,
|
|
24
|
+
metadata: RequestMetadata,
|
|
25
|
+
path_override: str | None = None,
|
|
26
|
+
) -> ProcessRequestResult:
|
|
27
|
+
http_server = await core.http_server.get()
|
|
28
|
+
if not http_server:
|
|
29
|
+
return no_payment_required(metadata)
|
|
30
|
+
|
|
31
|
+
path = path_override or adapter.get_path()
|
|
32
|
+
|
|
33
|
+
context = HTTPRequestContext(
|
|
34
|
+
adapter=adapter,
|
|
35
|
+
path=path,
|
|
36
|
+
method=adapter.get_method(),
|
|
37
|
+
payment_header=(
|
|
38
|
+
adapter.get_header("PAYMENT-SIGNATURE")
|
|
39
|
+
or adapter.get_header("X-PAYMENT")
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not http_server.requires_payment(context):
|
|
44
|
+
return no_payment_required(metadata)
|
|
45
|
+
|
|
46
|
+
result = await http_server.process_http_request_with_restriction(context)
|
|
47
|
+
result.metadata = metadata
|
|
48
|
+
|
|
49
|
+
if result.type == "payment-error":
|
|
50
|
+
if result.restriction and result.restriction.price == 0:
|
|
51
|
+
await log_event(core, adapter, 200, metadata.request_id)
|
|
52
|
+
return no_payment_required(metadata)
|
|
53
|
+
await log_event(core, adapter, result.response.status if result.response else 402, metadata.request_id)
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def handle_request(
|
|
59
|
+
core: WorkerCore,
|
|
60
|
+
adapter: RequestAdapter,
|
|
61
|
+
metadata: RequestMetadata,
|
|
62
|
+
) -> ProcessRequestResult:
|
|
63
|
+
user_agent = adapter.get_user_agent()
|
|
64
|
+
bot = await core.bots.match_bot(user_agent) if user_agent else None
|
|
65
|
+
host_config = await core.host_config.get()
|
|
66
|
+
|
|
67
|
+
should_check = bot or (host_config and host_config.api_protection_mode == "all")
|
|
68
|
+
if not should_check:
|
|
69
|
+
return no_payment_required(metadata)
|
|
70
|
+
|
|
71
|
+
result = await handle_payment_request(core, adapter, metadata)
|
|
72
|
+
|
|
73
|
+
if result.type != "payment-error":
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
# Web restrictions are always bot-only
|
|
77
|
+
if result.restriction and result.restriction.type == "web" and not bot:
|
|
78
|
+
return no_payment_required(metadata)
|
|
79
|
+
|
|
80
|
+
payment_methods = await core.payment_methods.get()
|
|
81
|
+
|
|
82
|
+
if payment_methods and result.restriction:
|
|
83
|
+
if result.restriction.type == "api":
|
|
84
|
+
format_api_payment_error(
|
|
85
|
+
result, result.restriction, payment_methods, host_config.terms_of_service_url if host_config else None
|
|
86
|
+
)
|
|
87
|
+
elif result.restriction.type == "web":
|
|
88
|
+
format_web_payment_error(
|
|
89
|
+
result, result.restriction, payment_methods, adapter, host_config.terms_of_service_url if host_config else None
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if bot and bot.force_200 and result.response:
|
|
93
|
+
result.response.status = 200
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def handle_settlement(
|
|
99
|
+
core: WorkerCore,
|
|
100
|
+
adapter: RequestAdapter,
|
|
101
|
+
payment_payload: Any,
|
|
102
|
+
payment_requirements: Any,
|
|
103
|
+
upstream_status_code: int,
|
|
104
|
+
request_id: str,
|
|
105
|
+
) -> ProcessSettleResult:
|
|
106
|
+
http_server = await core.http_server.get()
|
|
107
|
+
if not http_server:
|
|
108
|
+
return _settlement_failure("Server not initialized", "")
|
|
109
|
+
|
|
110
|
+
if upstream_status_code >= 400:
|
|
111
|
+
await log_event(core, adapter, upstream_status_code, request_id)
|
|
112
|
+
return _settlement_failure("Upstream error", "")
|
|
113
|
+
|
|
114
|
+
result = await http_server.process_settlement(
|
|
115
|
+
payment_payload,
|
|
116
|
+
payment_requirements,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if result.success:
|
|
120
|
+
payment_response = result.headers.get("PAYMENT-RESPONSE")
|
|
121
|
+
await log_event(core, adapter, upstream_status_code, request_id, payment_response)
|
|
122
|
+
else:
|
|
123
|
+
await log_event(core, adapter, 402, request_id)
|
|
124
|
+
|
|
125
|
+
return result
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from .config import PACKAGE_VERSION
|
|
7
|
+
|
|
8
|
+
HEALTH_PATH = "/.well-known/foldset"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_health_response(platform: str, sdk_version: str) -> str:
|
|
12
|
+
return json.dumps({
|
|
13
|
+
"status": "ok",
|
|
14
|
+
"core_version": PACKAGE_VERSION,
|
|
15
|
+
"sdk_version": sdk_version,
|
|
16
|
+
"platform": platform,
|
|
17
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
18
|
+
})
|