"""aere-cloud: clientul Python oficial al Aere Cloud (chain 2800). Oglinda fidela a @aere/cloud (sdk/index.mjs): fiecare ruta a gateway-ului e o metoda, erorile se ridica drept AereCloudError (cod HTTP + corpul serverului), si verify_webhook e inclus ca primitorul de webhook-uri sa nu-si scrie singur verificarea HMAC. Zero dependinte in afara bibliotecii standard (urllib, hmac, hashlib, json); Python 3.9+. """ from __future__ import annotations import hmac import hashlib import json as _json import urllib.request import urllib.error from typing import Any, Optional __version__ = "1.6.0" __all__ = ["AereCloud", "AereCloudError", "verify_webhook"] _BAZA = "https://cloud.aere.network/v1" class _FaraRedirectari(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None _FARA_REDIRECTARI = urllib.request.build_opener(_FaraRedirectari) class AereCloudError(Exception): """Raspuns non-2xx (sau eroare JSON-RPC) de la Aere Cloud. Poarta .status (int), .body (dict sau None, corpul serverului) si .path, ca integratorul sa poata actiona pe eroarea exacta, nu pe un mesaj generic. """ def __init__(self, status: int, body: Any, path: str): mesaj = body.get("error") if isinstance(body, dict) else None super().__init__(f"Aere Cloud {status} on {path}: {mesaj or 'request failed'}") self.status = status self.body = body self.path = path class AereCloud: """Client pentru https://cloud.aere.network/v1. Cheia: ak2800...""" def __init__(self, api_key: str, base_url: str = _BAZA, timeout: float = 30.0, opener=None): if not api_key: raise ValueError("AereCloud: api_key is required (ak2800.
.)") self.api_key = api_key self.base_url = base_url.rstrip("/") self.timeout = timeout # opener injectabil pentru teste: primeste (urllib.request.Request, timeout) si # intoarce (status:int, corp:bytes). Implicit, urllib. self._opener = opener or self._urllib_opener @staticmethod def _urllib_opener(cerere: urllib.request.Request, timeout: float): try: # 1.5.1: fara redirectari urmate (urllib le urmeaza implicit si copiaza x-api-key spre orice gazda din Location); # un 3xx ajunge aici ca HTTPError si se intoarce drept raspuns, deci ca AereCloudError cu statusul lui with _FARA_REDIRECTARI.open(cerere, timeout=timeout) as r: return r.status, r.read() except urllib.error.HTTPError as e: return e.code, e.read() def _req(self, metoda: str, cale: str, body: Any = None, auth: bool = True) -> Any: # User-Agent-ul explicit nu e cosmetic: marginea (Cloudflare) raspunde 403 la # "Python-urllib/3.x" pentru ORICE ruta, inclusiv /health fara cheie. Masurat # 2026-08-27 chiar la prima rulare vie a acestui SDK. anteturi = {"content-type": "application/json", "user-agent": f"aere-cloud-python/{__version__}"} if auth: anteturi["x-api-key"] = self.api_key date = None if body is None else _json.dumps(body).encode("utf-8") cerere = urllib.request.Request(self.base_url + cale, data=date, headers=anteturi, method=metoda) status, corp = self._opener(cerere, self.timeout) try: d = _json.loads(corp.decode("utf-8")) if corp else None except ValueError: d = None if not (200 <= status < 300): raise AereCloudError(status, d, cale) return d # ---- health & account ---- def health(self) -> dict: return self._req("GET", "/health", auth=False) def account(self) -> dict: return self._req("GET", "/account") # ---- JSON-RPC ---- def rpc(self, method: str, params: Optional[list] = None) -> Any: d = self._req("POST", "/rpc", {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []}) if isinstance(d, dict) and d.get("error"): raise AereCloudError(200, d, "/rpc") return d.get("result") if isinstance(d, dict) else None def block_number(self) -> int: return int(self.rpc("eth_blockNumber"), 16) # ---- post-quantum verification ---- def pq_verify(self, payload: dict) -> dict: """{scheme, publicKey, signature?, message?, signedMessage?} (0x-hex) -> {valid, ...}""" return self._req("POST", "/pq/verify", payload) def verify(self, envelope: dict, chain: "int | None" = None) -> dict: """the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify. chain: 2800 or 28001; None = the chain the envelope declares.""" return self._req("POST", "/verify" + (f"?chain={int(chain)}" if chain else ""), envelope) # ---- chain data ---- def data_head(self) -> dict: return self._req("GET", "/data/head") def validators(self) -> dict: return self._req("GET", "/data/validators") def anchors(self, limit: int = 10) -> dict: return self._req("GET", f"/data/anchors?limit={int(limit)}") def anchor(self, height: int) -> dict: return self._req("GET", f"/data/anchors/{int(height)}") def finality(self) -> dict: """2026-09-26: the post-quantum finality point (finalized/safe = the parent of the last certified anchor).""" return self._req("GET", "/data/finality") def transfers(self, address: str, limit: int = 50) -> dict: return self._req("GET", f"/data/transfers?address={address}&limit={int(limit)}") # ---- notarization ---- def notarize(self, digest_hex: str) -> dict: """Notarizeaza un digest de 32 de octeti (0x + 64 hex); Aere plateste gazul.""" return self._req("POST", "/notarize", {"hash": digest_hex}) def proof_of(self, digest_hex: str) -> dict: return self._req("GET", f"/notarize/{digest_hex}") def proof(self, digest_hex: str) -> dict: """2026-09-17: dovada cu finalitatea ei post-cuantica (prima aparitie, ancora care o acopera, verdict, pasii de verificare).""" return self._req("GET", f"/proof/{digest_hex}") # ---- quantum readiness ---- def readiness(self, domain: str, attest: bool = False, fresh: bool = False) -> dict: """Scanarea marginii TLS publice a unui domeniu: libera, fara cheie; attest=True notarizeaza digestul raportului pe lant (cere cheia).""" body: dict = {"domain": domain} if fresh: body["fresh"] = True if attest: body["attest"] = True return self._req("POST", "/pq/readiness", body, auth=attest) def readiness_report(self, domain: str) -> dict: return self._req("GET", f"/pq/readiness/{domain}", auth=False) def readiness_perimeter(self, domain: str, hosts: list | None = None, discover: bool = True, attest: bool = False) -> dict: """Tot perimetrul public al unui domeniu intr-un raport (cu cheie): gazdele numite (etichete sau nume de sub domeniu) plus prefixele obisnuite gasite prin DNS, fiecare masurata cu aceleasi strangeri de mana. Cat summary['complete'] e False se cere din nou (scanarile terminate stau in cache); attest=True notarizeaza digestul unui raport COMPLET pe lantul 2800.""" body: dict = {"domain": domain} if hosts: body["hosts"] = list(hosts) if discover is False: body["discover"] = False if attest: body["attest"] = True return self._req("POST", "/pq/readiness/perimeter", body) def pq_finality(self, block: str = "latest", network: str = "testnet", client: str = "besu") -> dict: """AIP-21: finalitatea post-cuantica a unui bloc (sigiliile auzite de un validator, re-verificate, verdict de cvorum). Libera. client: "besu" (implicit), "nethermind" sau "both" (doua implementari independente, cu `agreement` calculat de gateway).""" return self._req("GET", f"/pq/finality/{block}?network={network}&client={client}", auth=False) # ---- audit log ---- def audit_log(self, day: Optional[str] = None, offset: int = 0, limit: int = 1000) -> dict: """Jurnalul cererilor cu cheie ale contului pentru o zi UTC (paginat), cu sha256-ul fisierului zilei.""" q = f"offset={int(offset)}&limit={int(limit)}" + (f"&day={day}" if day else "") return self._req("GET", f"/account/audit?{q}") def audit_export(self, day: Optional[str] = None, fmt: str = "jsonl") -> dict: """Ziua ca FISIER (1.4.0): fmt='jsonl' = octetii exacti ai fisierului zilei, al caror sha256 e digestul pe care il notarizezi (calculat AICI, din ce ai primit, nu luat pe cuvant); fmt='cef' = un rand ArcSight CEF pe cerere, pentru un SIEM.""" import hashlib q = f"format={fmt}" + (f"&day={day}" if day else "") anteturi = {"user-agent": f"aere-cloud-python/{__version__}", "x-api-key": self.api_key} cerere = urllib.request.Request(self.base_url + f"/account/audit?{q}", headers=anteturi, method="GET") status, corp = self._opener(cerere, self.timeout) if not (200 <= status < 300): try: d = _json.loads(corp.decode("utf-8")) if corp else None except ValueError: d = None raise AereCloudError(status, d, "/account/audit") corp = corp or b"" return {"format": fmt, "bytes": corp, "text": corp.decode("utf-8"), "entries": sum(1 for l in corp.split(b"\n") if l), "sha256": "0x" + hashlib.sha256(corp).hexdigest()} def audit_retention(self) -> dict: """Retentia jurnalului de audit (1.5.0), DOAR-CITIRE cu cheia API: ce e in vigoare, ce asteapta si pana cand, ce tinem, si registrul fiecarei schimbari si al fiecarei zile sterse (cu digestul ei). Se SETEAZA cu semnatura portofelului, in consola.""" return self._req("GET", "/account/audit/retention") def audit_verify_kept(self, day: str, data) -> dict: """Exportul pe care l-ai PASTRAT se mai potriveste cu ziua de la noi, chiar dupa ce am sters-o prin retentia ta? sha256 al octetilor TAI, calculat aici, fata de digestul zilei tinute ('held') sau de piatra lasata de ziua stearsa ('expired', 410).""" import hashlib octeti = data.encode("utf-8") if isinstance(data, str) else bytes(data) local = "0x" + hashlib.sha256(octeti).hexdigest() anteturi = {"user-agent": f"aere-cloud-python/{__version__}", "x-api-key": self.api_key} cerere = urllib.request.Request(self.base_url + f"/account/audit?format=jsonl&day={day}", headers=anteturi, method="GET") status, corp = self._opener(cerere, self.timeout) corp = corp or b"" if not (200 <= status < 300): try: d = _json.loads(corp.decode("utf-8")) if corp else None except ValueError: d = None if status == 410 and isinstance(d, dict) and isinstance(d.get("expired"), dict): e = d["expired"] return {"day": day, "state": "expired", "digest": e.get("digest"), "local": local, "matches": local == e.get("digest"), "deleted_at": e.get("deletedAt")} raise AereCloudError(status, d, "/account/audit") digest = "0x" + hashlib.sha256(corp).hexdigest() return {"day": day, "state": "held", "digest": digest, "local": local, "matches": local == digest, "deleted_at": None} # ---- webhooks ---- def list_webhooks(self) -> dict: return self._req("GET", "/webhooks") def create_webhook(self, spec: dict) -> dict: return self._req("POST", "/webhooks", spec) def delete_webhook(self, webhook_id: str) -> dict: return self._req("DELETE", f"/webhooks/{webhook_id}") # ---- gas sponsorship ---- def sponsor_health(self) -> dict: return self._req("GET", "/sponsor/health") def sponsor_create_account(self, body: dict) -> dict: return self._req("POST", "/sponsor/createAccount", body) def sponsor_create_pq_account(self, falcon_pub_key: str, salt: int = 0) -> dict: """Cont inteligent cu proprietar POST-CUANTIC (Falcon-512), gazul platit de releu. falcon_pub_key: 0x-hex, 897 octeti, antetul 0x09 (cheia publica Falcon-512). Idempotent pe (cheie, salt): a doua chemare intoarce contul existent, fara tx. """ return self._req("POST", "/sponsor/createPqAccount", {"falconPubKey": falcon_pub_key, "salt": salt}) def sponsor_execute(self, body: dict) -> dict: return self._req("POST", "/sponsor/execute", body) def verify_webhook(raw_body, signature_header: str, secret: str) -> bool: """Verifica semnatura unei livrari de webhook. A NU crede un webhook fara asta. raw_body: octetii EXACTI ai cererii (nu re-serializati). Comparatia e in timp constant. """ if isinstance(raw_body, str): raw_body = raw_body.encode("utf-8") asteptat = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(asteptat, str(signature_header or ""))