sester 0.5.0__py3-none-any.whl
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.
- sester/__init__.py +14 -0
- sester/adapters.py +596 -0
- sester/bridges.py +167 -0
- sester/compat.py +8 -0
- sester/demo_api.py +288 -0
- sester/escalation.py +227 -0
- sester/evidence.py +112 -0
- sester/facilitator.py +105 -0
- sester/facilitator_svc/__init__.py +19 -0
- sester/facilitator_svc/app.py +106 -0
- sester/facilitator_svc/service.py +302 -0
- sester/ledger.py +285 -0
- sester/middleware.py +321 -0
- sester/migrate_pg.py +137 -0
- sester/panel.py +210 -0
- sester/pg_ledger.py +300 -0
- sester/policy.py +178 -0
- sester/policy_signed.py +277 -0
- sester/schemes.py +218 -0
- sester/settlement.py +304 -0
- sester/watchfeed.py +150 -0
- sester-0.5.0.dist-info/METADATA +180 -0
- sester-0.5.0.dist-info/RECORD +25 -0
- sester-0.5.0.dist-info/WHEEL +4 -0
- sester-0.5.0.dist-info/licenses/LICENSE +201 -0
sester/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""SESTER — Ajan ticaret yığını, B-katmanı (AgentMeter) v0.
|
|
2
|
+
|
|
3
|
+
Katmanlar:
|
|
4
|
+
policy — Policy-DSL-v0 v0 alt-kümesi (fail-closed cüzdan politikası)
|
|
5
|
+
ledger — hash-chain'li SQLite usage ledger (tamga-uyumlu olay şeması)
|
|
6
|
+
middleware — x402 el sıkışması + sayaç + kota (ASGI/Starlette)
|
|
7
|
+
panel — basit HTML tablo (kim, kaç çağrı, ne kadar)
|
|
8
|
+
|
|
9
|
+
Kimlik-tarihi: Pugio → Sikke → Sester (bkz. identity-migration record). Donuk
|
|
10
|
+
kablo-alanları (pugio0, pugio_bundle_version, pugio_evidence_bundle,
|
|
11
|
+
source:sikke) v2'ye kadar alıcı-uyumu için korunur.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.5.0"
|
sester/adapters.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""SESTER adapters — K4: AP2 mandate + ACP checkout + UCP web-monetization
|
|
2
|
+
→ ChargeIntent/Receipt.
|
|
3
|
+
|
|
4
|
+
63-A mimari tezi (concept paper §TUR): "Protokol-galibi riskine karşı tek çekirdek".
|
|
5
|
+
ChargeIntent (fiyat+kimlik+kanıt-istemi) ve ChargeReceipt (ödeme-kanıtı) K0
|
|
6
|
+
ortak-zarfı soyutlamasıdır; her protokol bunlara çevrilir. Bu modül üç
|
|
7
|
+
protokolün çekirdek-akışını *yerel* taklit eder:
|
|
8
|
+
|
|
9
|
+
AP2 (Agent Payments Protocol — Google → FIDO Alliance, Nis 2026):
|
|
10
|
+
"mandate" = kullanıcı, ajanın bir harcama-yetkisini imzalamıştır.
|
|
11
|
+
Auth: "AP2-Mandate <b64(mandate)>"
|
|
12
|
+
Mandate: {mandate_id, agent (cüzdan-adresi), principal (kullanıcı),
|
|
13
|
+
scope: {resource_prefixes, per_request_max_minor, currency,
|
|
14
|
+
valid_from, valid_to}, signature: <JWS-compact>}
|
|
15
|
+
Doğrulama: verify_ap2_mandate — scope, pencere, para-birimi, tutar;
|
|
16
|
+
imza: `signature` alanı JWS-compact (RFC 7515). Üretimde `key_resolver`
|
|
17
|
+
ŞART: HS256 (stdlib) veya ES256 (opsiyonel `cryptography`) ile
|
|
18
|
+
doğrulanır; resolver'sız kurulumda imza yalnız form-bağlamı yapılır
|
|
19
|
+
(v0.2 uyumu — architecture decision record: bilinçli v0.3 geçişi).
|
|
20
|
+
|
|
21
|
+
ACP (Agentic Commerce Protocol — Stripe+OpenAI, Eyl 2025):
|
|
22
|
+
"checkout_session" = satıcı-oturumu, ajan bakiyesinden tek-çekim.
|
|
23
|
+
Auth: "ACP-Session <b64(session)>"
|
|
24
|
+
Session: {session_id, buyer (cüzdan-adresi), line_item: {resource,
|
|
25
|
+
amount_minor, currency}, valid_to}
|
|
26
|
+
|
|
27
|
+
UCP (Universal Commerce Protocol — Google+Shopify, 2026; protocol-delta register v3):
|
|
28
|
+
"web_monetization " = satıcının /.well-known/ucp manifestinde ilan ettiği
|
|
29
|
+
satıcı-mühürlü tek-çekim satın-alma niyeti (402-akışına paralel biçim).
|
|
30
|
+
Auth: "UCP-Checkout <b64(intent)>"
|
|
31
|
+
Intent: {intent_id, buyer (cüzdan-adresi), merchant (satıcı-kimliği),
|
|
32
|
+
line_item: {resource, amount_minor, currency}, valid_to,
|
|
33
|
+
signature?}
|
|
34
|
+
SESTER katkısı: manifest-teşhiri *imzalı-zarfla* bağlanır — satıcı
|
|
35
|
+
issue_ucp_checkout ile üretir; alıcı verify_ucp_checkout ile
|
|
36
|
+
ChargeIntent'e çevirir (aynı nonce/quota/kanıt-çekirdeği).
|
|
37
|
+
|
|
38
|
+
Her üç adaptör de şema-Registry'sine takılır: başlık-önekinden
|
|
39
|
+
ChargeIntent üretir. Ortak kural: ajan-kimliği = cüzdan-adresi
|
|
40
|
+
(non-custodial tez), nonce = mandate_id / session_id / intent_id (kalıcı
|
|
41
|
+
replay-koruması), kanıt = hash-chain'li charge_receipt olayı.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import base64
|
|
47
|
+
import binascii
|
|
48
|
+
import hashlib
|
|
49
|
+
import hmac
|
|
50
|
+
import json
|
|
51
|
+
import time
|
|
52
|
+
from dataclasses import dataclass, field
|
|
53
|
+
from typing import Any, Callable
|
|
54
|
+
|
|
55
|
+
# key_resolver(kid, alg) -> anahtar (HS256: bytes-secret; ES256: PEM-public-bytes)
|
|
56
|
+
KeyResolver = Callable[[str | None, str], Any]
|
|
57
|
+
|
|
58
|
+
JWS_SUPPORTED_ALGS = ("HS256", "ES256")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AdapterError(Exception):
|
|
62
|
+
"""AP2/ACP zarfı bozuk, pencere-dışı ya da scope-dışı — ret."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SignatureRequiredError(AdapterError):
|
|
66
|
+
"""Zarf form-geçerli ama imzası doğrulanmadı (key_resolver yok/başarısız) —
|
|
67
|
+
production bayrağıyla zorunlu tutulur (fail-closed)."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------ b64url (JWS)
|
|
71
|
+
|
|
72
|
+
def _b64u_encode(raw: bytes) -> str:
|
|
73
|
+
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _b64u_decode(seg: str) -> bytes:
|
|
77
|
+
padded = seg + "=" * (-len(seg) % 4)
|
|
78
|
+
return base64.urlsafe_b64decode(padded)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------ K0 çekirdek
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class ChargeIntent:
|
|
85
|
+
"""K0 §2: fiyat + kimlik + kanıt-istemi — protokol-nötr harcama-niyeti."""
|
|
86
|
+
agent: str # cüzdan-adresi (non-custodial tez)
|
|
87
|
+
resource: str
|
|
88
|
+
amount_minor: int # integer minor-unit (v0.2 aritmetik-kuralı)
|
|
89
|
+
currency: str
|
|
90
|
+
nonce: str # mandate_id / session_id / imza-nonce
|
|
91
|
+
protocol: str # "ap2" | "acp" | "pugio0" | "exact-sester" | "exact"
|
|
92
|
+
principal: str = "" # AP2: imzalayan kullanıcı (boşsa ajan kendisi)
|
|
93
|
+
raw: dict[str, Any] = field(default_factory=dict, compare=False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ChargeReceipt:
|
|
98
|
+
"""K0 §2: ödeme-kanıtı — ledger'a charge_receipt olarak yazılır."""
|
|
99
|
+
intent: ChargeIntent
|
|
100
|
+
paid_minor: int
|
|
101
|
+
currency: str
|
|
102
|
+
receipt_hash: str # ledger hash-chain halkası (tamga-köprüsü)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _sign_object(obj: dict[str, Any], *, alg: str, key: Any,
|
|
106
|
+
kid: str | None = None) -> str:
|
|
107
|
+
"""Nesnenin 'signature' ALANI DIŞINDAKİ her alanını JWS ile mühürler
|
|
108
|
+
(imza-yuvası çıkarılır → deterministik, tekrar-üretilebilir payload)."""
|
|
109
|
+
body = {k: v for k, v in obj.items() if k != "signature"}
|
|
110
|
+
return sign_mandate_jws(body, alg=alg, key=key, kid=kid)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _b64_json(header: str, prefix: str) -> dict[str, Any]:
|
|
114
|
+
tok = header.strip()
|
|
115
|
+
if not tok.startswith(prefix + " "):
|
|
116
|
+
raise AdapterError(f"başlık {prefix} önekiyle değil")
|
|
117
|
+
raw = tok[len(prefix) + 1:]
|
|
118
|
+
try:
|
|
119
|
+
padded = raw + "=" * (-len(raw) % 4)
|
|
120
|
+
return json.loads(base64.urlsafe_b64decode(padded))
|
|
121
|
+
except (binascii.Error, ValueError, json.JSONDecodeError) as e:
|
|
122
|
+
raise AdapterError(f"zarf base64/JSON bozuk: {e}") from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_window(obj: dict[str, Any], now: float | None) -> None:
|
|
126
|
+
now = time.time() if now is None else now
|
|
127
|
+
vf = obj.get("valid_from")
|
|
128
|
+
vt = obj.get("valid_to")
|
|
129
|
+
if vf is not None and now < float(vf):
|
|
130
|
+
raise AdapterError("yetki henüz başlamadı (valid_from)")
|
|
131
|
+
if vt is not None and now > float(vt):
|
|
132
|
+
raise AdapterError("yetki süresi doldu (valid_to)")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------ JWS (RFC 7515)
|
|
136
|
+
|
|
137
|
+
def sign_mandate_jws(mandate_body: dict[str, Any], *, alg: str, key: Any,
|
|
138
|
+
kid: str | None = None) -> str:
|
|
139
|
+
"""JWS-compact üretimi (test/üretici tarafı): payload = mandate-gövdesi.
|
|
140
|
+
HS256: key = bytes; ES256: key = PEM-private-bytes (cryptography gerekir)."""
|
|
141
|
+
if alg not in JWS_SUPPORTED_ALGS:
|
|
142
|
+
raise AdapterError(f"desteklenmeyen alg: {alg}")
|
|
143
|
+
hdr = {"alg": alg, "typ": "JWS"}
|
|
144
|
+
if kid:
|
|
145
|
+
hdr["kid"] = kid
|
|
146
|
+
signing_input = (_b64u_encode(json.dumps(hdr, sort_keys=True).encode())
|
|
147
|
+
+ "." + _b64u_encode(json.dumps(mandate_body, sort_keys=True).encode()))
|
|
148
|
+
if alg == "HS256":
|
|
149
|
+
mac = hmac.new(key if isinstance(key, bytes) else str(key).encode(),
|
|
150
|
+
signing_input.encode(), hashlib.sha256).digest()
|
|
151
|
+
return signing_input + "." + _b64u_encode(mac)
|
|
152
|
+
# ES256 — opsiyonel cryptography
|
|
153
|
+
try:
|
|
154
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
155
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
156
|
+
from cryptography.hazmat.primitives.asymmetric.utils import (
|
|
157
|
+
decode_dss_signature,
|
|
158
|
+
encode_dss_signature,
|
|
159
|
+
)
|
|
160
|
+
except ImportError as e: # pragma: no cover - kurulum-bağımlı
|
|
161
|
+
raise AdapterError("ES256 için 'cryptography' gerekir: pip install 'sester[jws]'") from e
|
|
162
|
+
try:
|
|
163
|
+
priv = serialization.load_pem_private_key(key, password=None)
|
|
164
|
+
der = priv.sign(signing_input.encode(), ec.ECDSA(hashes.SHA256()))
|
|
165
|
+
r, s = decode_dss_signature(der)
|
|
166
|
+
return signing_input + "." + _b64u_encode(r.to_bytes(32, "big") + s.to_bytes(32, "big"))
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise AdapterError(f"ES256 imza-üretimi başarısız: {e}") from e
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def verify_mandate_jws(jws_compact: str, *, resolve_key: KeyResolver,
|
|
172
|
+
expected_body: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
173
|
+
"""JWS-compact doğrulaması — fail-closed: her hata AdapterError.
|
|
174
|
+
expected_body verilirse JWS-payload ile zarf-gövdesinin bağlandığı
|
|
175
|
+
alanlar (mandate_id, agent, principal, scope) birebir aynı olmalı."""
|
|
176
|
+
try:
|
|
177
|
+
hdr_seg, pay_seg, sig_seg = jws_compact.strip().split(".")
|
|
178
|
+
except ValueError as e:
|
|
179
|
+
raise AdapterError("JWS compact 3-segmentli değil") from e
|
|
180
|
+
try:
|
|
181
|
+
hdr = json.loads(_b64u_decode(hdr_seg))
|
|
182
|
+
payload = json.loads(_b64u_decode(pay_seg))
|
|
183
|
+
except (binascii.Error, ValueError, json.JSONDecodeError) as e:
|
|
184
|
+
raise AdapterError(f"JWS header/payload bozuk: {e}") from e
|
|
185
|
+
alg = hdr.get("alg")
|
|
186
|
+
if alg not in JWS_SUPPORTED_ALGS:
|
|
187
|
+
raise AdapterError(f"JWS alg reddedildi: {alg!r} (desteklenen: {JWS_SUPPORTED_ALGS})")
|
|
188
|
+
signing_input = f"{hdr_seg}.{pay_seg}".encode()
|
|
189
|
+
sig = _b64u_decode(sig_seg)
|
|
190
|
+
key = resolve_key(hdr.get("kid"), alg)
|
|
191
|
+
if key is None:
|
|
192
|
+
raise AdapterError(f"anahtar çözülemedi: kid={hdr.get('kid')!r}")
|
|
193
|
+
|
|
194
|
+
if alg == "HS256":
|
|
195
|
+
secret = key if isinstance(key, bytes) else str(key).encode()
|
|
196
|
+
expect = hmac.new(secret, signing_input, hashlib.sha256).digest()
|
|
197
|
+
if not hmac.compare_digest(expect, sig):
|
|
198
|
+
raise AdapterError("JWS imzası uyuşmuyor (HS256)")
|
|
199
|
+
else: # ES256
|
|
200
|
+
try:
|
|
201
|
+
from cryptography.exceptions import InvalidSignature
|
|
202
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
203
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
204
|
+
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
|
|
205
|
+
except ImportError as e: # pragma: no cover
|
|
206
|
+
raise AdapterError("ES256 için 'cryptography' gerekir") from e
|
|
207
|
+
if len(sig) != 64:
|
|
208
|
+
raise AdapterError("ES256 imzası 64-byte r||s değil")
|
|
209
|
+
r, s = int.from_bytes(sig[:32], "big"), int.from_bytes(sig[32:], "big")
|
|
210
|
+
try:
|
|
211
|
+
pub = serialization.load_pem_public_key(key)
|
|
212
|
+
pub.verify(encode_dss_signature(r, s), signing_input,
|
|
213
|
+
ec.ECDSA(hashes.SHA256()))
|
|
214
|
+
except (InvalidSignature, ValueError, TypeError) as e:
|
|
215
|
+
raise AdapterError("JWS imzası uyuşmuyor (ES256)") from e
|
|
216
|
+
|
|
217
|
+
if expected_body is not None:
|
|
218
|
+
# tam gövde-bağı: zarfta bulunan HER alan JWS-payload ile birebir
|
|
219
|
+
# (AP2: mandate_id/agent/principal/scope; ACP: session_id/buyer/
|
|
220
|
+
# line_item/pencereler) — kazıma + geçerli-imza ret edilir
|
|
221
|
+
for k, v_expect in expected_body.items():
|
|
222
|
+
if payload.get(k) != v_expect:
|
|
223
|
+
raise AdapterError(f"JWS-payload zarf-gövdesiyle uyuşmuyor: {k}")
|
|
224
|
+
return payload
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------ AP2 mandate
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class Ap2Mandate:
|
|
231
|
+
"""AP2 mandate zarfı — kullanıcının ajana verdiği imzalı harcama-yetkisi."""
|
|
232
|
+
mandate_id: str
|
|
233
|
+
agent: str
|
|
234
|
+
principal: str
|
|
235
|
+
scope: dict[str, Any]
|
|
236
|
+
signature: str # JWS-compact (RFC 7515)
|
|
237
|
+
raw: dict[str, Any]
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def parse(cls, header: str) -> "Ap2Mandate":
|
|
241
|
+
obj = _b64_json(header, "AP2-Mandate")
|
|
242
|
+
for k in ("mandate_id", "agent", "principal", "scope", "signature"):
|
|
243
|
+
if not obj.get(k):
|
|
244
|
+
raise AdapterError(f"mandate.{k} eksik")
|
|
245
|
+
scope = obj["scope"]
|
|
246
|
+
if not isinstance(scope, dict):
|
|
247
|
+
raise AdapterError("mandate.scope nesne değil")
|
|
248
|
+
return cls(
|
|
249
|
+
mandate_id=str(obj["mandate_id"]),
|
|
250
|
+
agent=str(obj["agent"]).lower(),
|
|
251
|
+
principal=str(obj["principal"]),
|
|
252
|
+
scope=scope,
|
|
253
|
+
signature=str(obj["signature"]),
|
|
254
|
+
raw=obj,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def authorize(self, resource: str, amount_minor: int, currency: str,
|
|
258
|
+
*, now: float | None = None) -> ChargeIntent:
|
|
259
|
+
"""Mandate scope'una göre ChargeIntent üretir; scope-dışı → AdapterError."""
|
|
260
|
+
_check_window(self.raw, now)
|
|
261
|
+
prefixes = self.scope.get("resource_prefixes", [])
|
|
262
|
+
res = (resource or "").lower()
|
|
263
|
+
if not any(res.startswith(str(p).lower()) for p in prefixes):
|
|
264
|
+
raise AdapterError(f"kaynak mandate-kapsamı dışı: {resource}")
|
|
265
|
+
prm = int(self.scope.get("per_request_max_minor", 0))
|
|
266
|
+
if amount_minor > prm:
|
|
267
|
+
raise AdapterError(f"tutar mandate üst-sınırı aşıyor: {amount_minor} > {prm}")
|
|
268
|
+
cur = str(self.scope.get("currency", "USDC"))
|
|
269
|
+
if currency and currency != cur:
|
|
270
|
+
raise AdapterError(f"para-birimi uyuşmuyor: {currency} ≠ {cur}")
|
|
271
|
+
return ChargeIntent(
|
|
272
|
+
agent=self.agent, resource=resource, amount_minor=amount_minor,
|
|
273
|
+
currency=cur, nonce=self.mandate_id, protocol="ap2",
|
|
274
|
+
principal=self.principal, raw=self.raw,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def verify(self, *, resolve_key: KeyResolver | None = None) -> None:
|
|
278
|
+
"""JWS-dogrulama. resolve_key verilirse tam-kripto (HS256/ES256);
|
|
279
|
+
verilmezse yalnız form-bağlamı (v0.2 uyumu — üretimde resolver ŞART)."""
|
|
280
|
+
if resolve_key is None:
|
|
281
|
+
if len(self.signature) < 16:
|
|
282
|
+
raise AdapterError("mandate imzası formda değil")
|
|
283
|
+
return
|
|
284
|
+
verify_mandate_jws(
|
|
285
|
+
self.signature, resolve_key=resolve_key,
|
|
286
|
+
expected_body={k: v for k, v in self.raw.items() if k != "signature"},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def verify_ap2_mandate(header: str, resource: str, amount_minor: int,
|
|
291
|
+
currency: str = "USDC", *, now: float | None = None,
|
|
292
|
+
resolve_key: KeyResolver | None = None
|
|
293
|
+
) -> ChargeIntent:
|
|
294
|
+
"""AP2 başlığı → ChargeIntent (yetki-kapsamı + imza doğrulanmış)."""
|
|
295
|
+
m = Ap2Mandate.parse(header)
|
|
296
|
+
m.verify(resolve_key=resolve_key)
|
|
297
|
+
return m.authorize(resource, amount_minor, currency, now=now)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ------------------------------------------------------------------ ACP session
|
|
301
|
+
|
|
302
|
+
@dataclass(frozen=True)
|
|
303
|
+
class AcpSession:
|
|
304
|
+
"""ACP checkout_session zarfı — satıcı-oturumu, tek-çekim bakiye-çekimi.
|
|
305
|
+
v0.3.1: satıcı-tarafı imza — `signature` alanı JWS-compact; tam zarfın
|
|
306
|
+
(imza-yuvası hariç) mührü. Üretici: `issue_acp_session` (satıcı-sırası)."""
|
|
307
|
+
session_id: str
|
|
308
|
+
buyer: str
|
|
309
|
+
line_item: dict[str, Any]
|
|
310
|
+
signature: str = "" # satıcı-JWS (boş = imzasız eski-zarf)
|
|
311
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def parse(cls, header: str) -> "AcpSession":
|
|
315
|
+
obj = _b64_json(header, "ACP-Session")
|
|
316
|
+
for k in ("session_id", "buyer", "line_item"):
|
|
317
|
+
if not obj.get(k):
|
|
318
|
+
raise AdapterError(f"session.{k} eksik")
|
|
319
|
+
if not isinstance(obj["line_item"], dict):
|
|
320
|
+
raise AdapterError("session.line_item nesne değil")
|
|
321
|
+
return cls(
|
|
322
|
+
session_id=str(obj["session_id"]),
|
|
323
|
+
buyer=str(obj["buyer"]).lower(),
|
|
324
|
+
line_item=obj["line_item"],
|
|
325
|
+
signature=str(obj.get("signature", "")),
|
|
326
|
+
raw=obj,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def authorize(self, resource: str, *, now: float | None = None) -> ChargeIntent:
|
|
330
|
+
"""Oturumun tek line_item'ı bu kaynağa değilse ret."""
|
|
331
|
+
_check_window(self.raw, now)
|
|
332
|
+
li = self.line_item
|
|
333
|
+
if str(li.get("resource", "")).lower() != (resource or "").lower():
|
|
334
|
+
raise AdapterError("line_item kaynağı istenen kaynak değil")
|
|
335
|
+
try:
|
|
336
|
+
amount_minor = int(li["amount_minor"])
|
|
337
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
338
|
+
raise AdapterError("line_item.amount_minor bozuk") from e
|
|
339
|
+
if amount_minor <= 0:
|
|
340
|
+
raise AdapterError("line_item.amount_minor pozitif olmalı")
|
|
341
|
+
return ChargeIntent(
|
|
342
|
+
agent=self.buyer, resource=resource, amount_minor=amount_minor,
|
|
343
|
+
currency=str(li.get("currency", "USDC")), nonce=self.session_id,
|
|
344
|
+
protocol="acp", raw=self.raw,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def verify(self, *, resolve_key: KeyResolver | None = None,
|
|
348
|
+
require_signature: bool = False) -> None:
|
|
349
|
+
"""Satıcı-imzası doğrulaması. İmzasız eski-zarf: yalnız
|
|
350
|
+
require_signature=False'ta kabul (v0.2 geri-uyum); imzalıysa JWS
|
|
351
|
+
tam-gövde-bağı (expected_body=imza-yuvası-hariç zarf)."""
|
|
352
|
+
if not self.signature:
|
|
353
|
+
if require_signature:
|
|
354
|
+
raise SignatureRequiredError("ACP session imzasız — production reddi")
|
|
355
|
+
return
|
|
356
|
+
if resolve_key is None:
|
|
357
|
+
if require_signature:
|
|
358
|
+
raise SignatureRequiredError("key_resolver yok — imza doğrulanamadı")
|
|
359
|
+
return
|
|
360
|
+
verify_mandate_jws(
|
|
361
|
+
self.signature, resolve_key=resolve_key,
|
|
362
|
+
expected_body={k: v for k, v in self.raw.items() if k != "signature"},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def issue_acp_session(*, session_id: str, buyer: str, resource: str,
|
|
367
|
+
amount_minor: int, currency: str = "USDC",
|
|
368
|
+
ttl_seconds: float = 3600.0, alg: str = "HS256",
|
|
369
|
+
key: Any = None, kid: str | None = None,
|
|
370
|
+
valid_from: float | None = None) -> str:
|
|
371
|
+
"""Satıcı-tarafı üretici: imzalı ACP-Session başlığı döndürür.
|
|
372
|
+
key=None → imzasız (v0.2 uyum / iç-dogfood)."""
|
|
373
|
+
t = time.time() if valid_from is None else valid_from
|
|
374
|
+
obj: dict[str, Any] = {
|
|
375
|
+
"session_id": session_id,
|
|
376
|
+
"buyer": buyer.lower(),
|
|
377
|
+
"valid_from": t,
|
|
378
|
+
"valid_to": t + ttl_seconds,
|
|
379
|
+
"line_item": {"resource": resource, "amount_minor": int(amount_minor),
|
|
380
|
+
"currency": currency},
|
|
381
|
+
}
|
|
382
|
+
if key is not None:
|
|
383
|
+
obj["signature"] = _sign_object(obj, alg=alg, key=key, kid=kid)
|
|
384
|
+
raw = base64.urlsafe_b64encode(json.dumps(obj, sort_keys=True).encode()
|
|
385
|
+
).decode().rstrip("=")
|
|
386
|
+
return f"ACP-Session {raw}"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def verify_acp_session(header: str, resource: str, *, now: float | None = None,
|
|
390
|
+
resolve_key: KeyResolver | None = None,
|
|
391
|
+
require_signature: bool = False) -> ChargeIntent:
|
|
392
|
+
"""ACP başlığı → ChargeIntent (kaynak/tutar + imza-politikası doğrulanmış)."""
|
|
393
|
+
s = AcpSession.parse(header)
|
|
394
|
+
s.verify(resolve_key=resolve_key, require_signature=require_signature)
|
|
395
|
+
return s.authorize(resource, now=now)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ------------------------------------------------------------------ UCP checkout
|
|
399
|
+
|
|
400
|
+
@dataclass(frozen=True)
|
|
401
|
+
class UcpCheckout:
|
|
402
|
+
"""UCP web-monetization zarfı — satıcı-mühürlü tek-çekim satın-alma.
|
|
403
|
+
Zarf satıcı anahtarıyla JWS-mühürlü gelir; buyer alanı ajan-kimliğidir
|
|
404
|
+
(non-custodial tez). nonce = intent_id → kalıcı replay-koruması."""
|
|
405
|
+
intent_id: str
|
|
406
|
+
buyer: str
|
|
407
|
+
merchant: str
|
|
408
|
+
line_item: dict[str, Any]
|
|
409
|
+
signature: str = ""
|
|
410
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
411
|
+
|
|
412
|
+
@classmethod
|
|
413
|
+
def parse(cls, header: str) -> "UcpCheckout":
|
|
414
|
+
obj = _b64_json(header, "UCP-Checkout")
|
|
415
|
+
for k in ("intent_id", "buyer", "merchant", "line_item"):
|
|
416
|
+
if not obj.get(k):
|
|
417
|
+
raise AdapterError(f"ucp.{k} eksik")
|
|
418
|
+
if not isinstance(obj["line_item"], dict):
|
|
419
|
+
raise AdapterError("ucp.line_item nesne değil")
|
|
420
|
+
return cls(
|
|
421
|
+
intent_id=str(obj["intent_id"]),
|
|
422
|
+
buyer=str(obj["buyer"]).lower(),
|
|
423
|
+
merchant=str(obj["merchant"]),
|
|
424
|
+
line_item=obj["line_item"],
|
|
425
|
+
signature=str(obj.get("signature", "")),
|
|
426
|
+
raw=obj,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def authorize(self, resource: str, *, now: float | None = None) -> ChargeIntent:
|
|
430
|
+
"""Satın-alma niyeti bu kaynağa ait değilse ret (kazıma-kapanı)."""
|
|
431
|
+
_check_window(self.raw, now)
|
|
432
|
+
li = self.line_item
|
|
433
|
+
if str(li.get("resource", "")).lower() != (resource or "").lower():
|
|
434
|
+
raise AdapterError("UCP line_item kaynağı istenen kaynak değil")
|
|
435
|
+
try:
|
|
436
|
+
amount_minor = int(li["amount_minor"])
|
|
437
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
438
|
+
raise AdapterError("UCP line_item.amount_minor bozuk") from e
|
|
439
|
+
if amount_minor <= 0:
|
|
440
|
+
raise AdapterError("UCP line_item.amount_minor pozitif olmalı")
|
|
441
|
+
return ChargeIntent(
|
|
442
|
+
agent=self.buyer, resource=resource, amount_minor=amount_minor,
|
|
443
|
+
currency=str(li.get("currency", "USDC")), nonce=self.intent_id,
|
|
444
|
+
protocol="ucp", raw=self.raw,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def verify(self, *, resolve_key: KeyResolver | None = None,
|
|
448
|
+
require_signature: bool = False,
|
|
449
|
+
expected_merchant: str | None = None) -> None:
|
|
450
|
+
"""Satıcı-mührü doğrulaması. expected_merchant verilirse zarfın
|
|
451
|
+
merchant alanı ile bağlanır (manifest-teşhir sahteliği kapanır).
|
|
452
|
+
İmzasız zarf yalnız require_signature=False'ta kabul edilir."""
|
|
453
|
+
if expected_merchant is not None and self.merchant != expected_merchant:
|
|
454
|
+
raise AdapterError("UCP merchant beklenen satıcı değil")
|
|
455
|
+
if not self.signature:
|
|
456
|
+
if require_signature:
|
|
457
|
+
raise SignatureRequiredError("UCP checkout imzasız — production reddi")
|
|
458
|
+
return
|
|
459
|
+
if resolve_key is None:
|
|
460
|
+
if require_signature:
|
|
461
|
+
raise SignatureRequiredError("key_resolver yok — UCP imzası doğrulanamadı")
|
|
462
|
+
return
|
|
463
|
+
verify_mandate_jws(
|
|
464
|
+
self.signature, resolve_key=resolve_key,
|
|
465
|
+
expected_body={k: v for k, v in self.raw.items() if k != "signature"},
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def issue_ucp_checkout(*, intent_id: str, buyer: str, merchant: str,
|
|
470
|
+
resource: str, amount_minor: int,
|
|
471
|
+
currency: str = "USDC", ttl_seconds: float = 3600.0,
|
|
472
|
+
alg: str = "HS256", key: Any = None,
|
|
473
|
+
kid: str | None = None,
|
|
474
|
+
valid_from: float | None = None) -> str:
|
|
475
|
+
"""Satıcı-tarafı üretici: imzalı UCP-Checkout başlığı döndürür
|
|
476
|
+
(/.well-known/ucp manifestinin 'payment methods' hanesine denk gelir).
|
|
477
|
+
key=None → imzasız (iç-dogfood)."""
|
|
478
|
+
t = time.time() if valid_from is None else valid_from
|
|
479
|
+
obj: dict[str, Any] = {
|
|
480
|
+
"intent_id": intent_id,
|
|
481
|
+
"buyer": buyer.lower(),
|
|
482
|
+
"merchant": merchant,
|
|
483
|
+
"valid_from": t,
|
|
484
|
+
"valid_to": t + ttl_seconds,
|
|
485
|
+
"line_item": {"resource": resource, "amount_minor": int(amount_minor),
|
|
486
|
+
"currency": currency},
|
|
487
|
+
}
|
|
488
|
+
if key is not None:
|
|
489
|
+
obj["signature"] = _sign_object(obj, alg=alg, key=key, kid=kid)
|
|
490
|
+
raw = base64.urlsafe_b64encode(json.dumps(obj, sort_keys=True).encode()
|
|
491
|
+
).decode().rstrip("=")
|
|
492
|
+
return f"UCP-Checkout {raw}"
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def verify_ucp_checkout(header: str, resource: str, *, now: float | None = None,
|
|
496
|
+
resolve_key: KeyResolver | None = None,
|
|
497
|
+
require_signature: bool = False,
|
|
498
|
+
expected_merchant: str | None = None) -> ChargeIntent:
|
|
499
|
+
"""UCP başlığı → ChargeIntent (kaynak/tutar + merchant-bağı + imza)."""
|
|
500
|
+
u = UcpCheckout.parse(header)
|
|
501
|
+
u.verify(resolve_key=resolve_key, require_signature=require_signature,
|
|
502
|
+
expected_merchant=expected_merchant)
|
|
503
|
+
return u.authorize(resource, now=now)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ------------------------------------------------------------------ registry
|
|
507
|
+
|
|
508
|
+
def install_adapters(register_scheme: Any, *, price_minor: int,
|
|
509
|
+
on_intent: Any | None = None,
|
|
510
|
+
currency: str = "USDC",
|
|
511
|
+
key_resolver: KeyResolver | None = None,
|
|
512
|
+
require_acp_signature: bool = False,
|
|
513
|
+
require_ucp_signature: bool = False,
|
|
514
|
+
ucp_merchant: str | None = None) -> None:
|
|
515
|
+
"""Üç protokolü middleware şema-Registry'sine takar.
|
|
516
|
+
|
|
517
|
+
register_scheme(prefix, parser) — middleware'in mevcut kayıt-noktası;
|
|
518
|
+
parser(header, resource) → parsed-dict (middleware sözleşmesi:
|
|
519
|
+
agent/nonce/amount_minor/scheme). price_minor = satıcı-fiyatı
|
|
520
|
+
(middleware.price_minor) — tutar-benchmark tek-kaynak.
|
|
521
|
+
on_intent(intent) — protokol-niyeti olayı; SESTER'da
|
|
522
|
+
`ledger.append("protocol_intent", ...)`.
|
|
523
|
+
key_resolver — AP2 JWS doğrulaması (üretimde ŞART; None → form-bağlamı).
|
|
524
|
+
require_acp_signature — ACP oturum-imzasını zorunlu tutar (üretim: True);
|
|
525
|
+
satıcı-ucu üretici: issue_acp_session.
|
|
526
|
+
require_ucp_signature — UCP satıcı-mührünü zorunlu tutar (üretim: True);
|
|
527
|
+
ucp_merchant — verilirse zarf-merchant'ı buna bağlanır (manifest-bağı).
|
|
528
|
+
|
|
529
|
+
Yerel-değerlendirme: AP2 scope'u zaten 'mandate-cüzdan-kuralı'dır
|
|
530
|
+
(63-A: "AP2 müşteride mandate birincil"); harici çağrı YOK — fail-closed
|
|
531
|
+
ağ-bağımlılığı yalnız x402 exact/facilitator hattında kalır.
|
|
532
|
+
"""
|
|
533
|
+
from .middleware import PaymentErr
|
|
534
|
+
|
|
535
|
+
def _ap2(header: str, resource: str) -> dict[str, Any]:
|
|
536
|
+
try:
|
|
537
|
+
m = Ap2Mandate.parse(header)
|
|
538
|
+
m.verify(resolve_key=key_resolver)
|
|
539
|
+
intent = m.authorize(resource, price_minor, currency)
|
|
540
|
+
except AdapterError as e:
|
|
541
|
+
raise PaymentErr(f"AP2 mandate reddedildi: {e}") from e
|
|
542
|
+
if on_intent:
|
|
543
|
+
on_intent(intent)
|
|
544
|
+
return {"agent": intent.agent, "nonce": intent.nonce,
|
|
545
|
+
"amount_minor": intent.amount_minor, "scheme": "ap2",
|
|
546
|
+
"intent": intent}
|
|
547
|
+
|
|
548
|
+
def _acp(header: str, resource: str) -> dict[str, Any]:
|
|
549
|
+
try:
|
|
550
|
+
s = AcpSession.parse(header)
|
|
551
|
+
s.verify(resolve_key=key_resolver,
|
|
552
|
+
require_signature=require_acp_signature)
|
|
553
|
+
intent = s.authorize(resource)
|
|
554
|
+
except AdapterError as e:
|
|
555
|
+
raise PaymentErr(f"ACP session reddedildi: {e}") from e
|
|
556
|
+
if intent.amount_minor < price_minor:
|
|
557
|
+
raise PaymentErr("ACP line_item tutar satıcı-fiyatının altında")
|
|
558
|
+
if on_intent:
|
|
559
|
+
on_intent(intent)
|
|
560
|
+
return {"agent": intent.agent, "nonce": intent.nonce,
|
|
561
|
+
"amount_minor": intent.amount_minor, "scheme": "acp",
|
|
562
|
+
"intent": intent}
|
|
563
|
+
|
|
564
|
+
def _ucp(header: str, resource: str) -> dict[str, Any]:
|
|
565
|
+
try:
|
|
566
|
+
u = UcpCheckout.parse(header)
|
|
567
|
+
u.verify(resolve_key=key_resolver,
|
|
568
|
+
require_signature=require_ucp_signature,
|
|
569
|
+
expected_merchant=ucp_merchant)
|
|
570
|
+
intent = u.authorize(resource)
|
|
571
|
+
except AdapterError as e:
|
|
572
|
+
raise PaymentErr(f"UCP checkout reddedildi: {e}") from e
|
|
573
|
+
if intent.amount_minor < price_minor:
|
|
574
|
+
raise PaymentErr("UCP line_item tutar satıcı-fiyatının altında")
|
|
575
|
+
if on_intent:
|
|
576
|
+
on_intent(intent)
|
|
577
|
+
return {"agent": intent.agent, "nonce": intent.nonce,
|
|
578
|
+
"amount_minor": intent.amount_minor, "scheme": "ucp",
|
|
579
|
+
"intent": intent}
|
|
580
|
+
|
|
581
|
+
register_scheme("AP2-Mandate", _ap2)
|
|
582
|
+
register_scheme("ACP-Session", _acp)
|
|
583
|
+
register_scheme("UCP-Checkout", _ucp)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def protocol_intent_event(intent: ChargeIntent) -> dict[str, Any]:
|
|
587
|
+
"""ChargeIntent → ledger olay-payload'ı (tamga-uyumlu şema)."""
|
|
588
|
+
return {
|
|
589
|
+
"protocol": intent.protocol,
|
|
590
|
+
"intent_id": intent.nonce,
|
|
591
|
+
"agent": intent.agent,
|
|
592
|
+
"resource": intent.resource,
|
|
593
|
+
"amount_minor": intent.amount_minor,
|
|
594
|
+
"currency": intent.currency,
|
|
595
|
+
"principal": intent.principal,
|
|
596
|
+
}
|