"""Bounded delivery to State Hub's retained memo-notification receipts.""" import hashlib import json import re import uuid from .http_transport import JSONTransport, fixed_origin class HubDelivery: def __init__(self, origin, *, transport=None): self.origin = fixed_origin(origin, allow_internal_http=True) self.transport = transport or JSONTransport(allow_internal_http=True) def _request(self, method, url, payload, key): body = json.dumps(payload, sort_keys=True, separators=(',', ':'), allow_nan=False).encode() return self.transport.request(method, url, body=body if method == 'POST' else None, headers={'Content-Type': 'application/json', 'Idempotency-Key': key, 'X-StateHub-Source-Agent': 'informed-decision'}) def notify(self, delivery_id, metadata): uuid.UUID(delivery_id) if set(metadata) != {'memo_id', 'question', 'memo_class', 'submitter'}: raise ValueError('invalid notification metadata') if any(not isinstance(v, str) or not v or len(v) > 4096 for v in metadata.values()): raise ValueError('invalid notification metadata') if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:-]{0,255}', metadata['memo_id']): raise ValueError('invalid memo id') status, receipt = self._request('POST', self.origin + '/informed-decision/notifications', metadata, delivery_id) # A queued relay receipt is pending, not successful central delivery. if status not in (200, 201) or not isinstance(receipt, dict): return False try: uuid.UUID(receipt['message_id']) except (ValueError, TypeError, KeyError, AttributeError): return False return (receipt.get('delivery_id') == delivery_id and receipt.get('status') == 'accepted' and receipt.get('authoritative_review') == 'signed-in-overview' and all(receipt.get(k) == v for k, v in metadata.items())) def resolution(self, method, url, payload): suffix = '/resolve' if method == 'POST' else '' pattern = r'/decisions/[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}' + suffix if (method not in ('POST', 'GET') or not url.startswith(self.origin + '/decisions/') or not re.fullmatch(pattern, url[len(self.origin):])): raise ValueError('invalid hub resolution route') material = json.dumps([method, url, payload], sort_keys=True, separators=(',', ':')).encode() status, receipt = self._request(method, url, payload, 'infd-resolution:' + hashlib.sha256(material).hexdigest()) if 200 <= status < 300: ident = url[len(self.origin + '/decisions/'):].split('/')[0] if (status != 200 or not isinstance(receipt, dict) or receipt.get('id') != ident or (method == 'POST' and (receipt.get('status') != 'resolved' or any(receipt.get(k) != payload[k] for k in ('rationale', 'decided_by'))))): return 503, {} # Queued or mismatched response is not a resolution receipt. return status, receipt