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 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
+ }