# SPDX-License-Identifier: MIT """Record a manual Clark route-offer send without sending or storing an address.""" from __future__ import annotations import argparse from datetime import date from hashlib import sha256 import json from pathlib import Path REQUEST="ab2a8887110cb058d496e0afe1563fe7e42ee260a63608efa2de03a62e58a534";HASH=set("0123456789abcdef") OFFER_FIELDS=["schema_version","status","request_sha256","queue_sha256","role","worksheet_sha256","issued_on","expires_on","route_type","allowed_files","max_total_bytes","forbidden_content","reply_subject_marker","automatic_contact","message_sent","attachment_received","submission_created","identity_verified","independence_verified","scientific_validation_claimed","offer_sha256"] DISPATCH_FIELDS=["schema_version","offer_sha256","sent_on","subject_marker","attachment_count","provider_message_id_sha256","operator_confirmed_manual_send"] ALLOWED=["worksheet.csv","validation-draft.json","reviewer-self-check.json","handoff-manifest.json","validator-output.txt"] FORBIDDEN=["publisher_pdf_or_scan","credentials_or_secrets","other_people_personal_data","executable_or_archive"] def _hash(v):return isinstance(v,str) and len(v)==64 and not(set(v)-HASH) def _offer(v): if list(v)!=OFFER_FIELDS or v["schema_version"]!="openlab-clark-route-offer-v1" or v["status"]!="manual_reply_draft_authorized_not_sent" or v["request_sha256"]!=REQUEST:raise ValueError("route offer identity or fields drift") if not _hash(v["queue_sha256"]) or not _hash(v["worksheet_sha256"]) or not _hash(v["offer_sha256"]):raise ValueError("route offer hash invalid") body={k:v[k] for k in OFFER_FIELDS[:-1]};expected=sha256(json.dumps(body,ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest() if v["offer_sha256"]!=expected:raise ValueError("route offer seal mismatch") if v["route_type"]!="managed_mailbox_reply" or v["allowed_files"]!=ALLOWED or v["max_total_bytes"]!=1048576 or v["forbidden_content"]!=FORBIDDEN or v["reply_subject_marker"]!="[KIWERB-CLARK-RETURN]":raise ValueError("route offer boundary drift") for f in ["automatic_contact","message_sent","attachment_received","submission_created","identity_verified","independence_verified","scientific_validation_claimed"]: if v[f] is not False:raise ValueError(f"{f} cannot be promoted in offer") return v def record(offer,dispatch): offer=_offer(offer) if list(dispatch)!=DISPATCH_FIELDS or dispatch["schema_version"]!="openlab-clark-route-dispatch-v1":raise ValueError("dispatch fields drift") if dispatch["offer_sha256"]!=offer["offer_sha256"] or not _hash(dispatch["provider_message_id_sha256"]):raise ValueError("dispatch binding invalid") try:sent=date.fromisoformat(dispatch["sent_on"]);issued=date.fromisoformat(offer["issued_on"]);expires=date.fromisoformat(offer["expires_on"]) except (TypeError,ValueError) as e:raise ValueError("dispatch date invalid") from e if sentexpires:raise ValueError("send outside offer validity") if dispatch["subject_marker"]!="[KIWERB-CLARK-RETURN]" or dispatch["attachment_count"]!=0 or dispatch["operator_confirmed_manual_send"] is not True:raise ValueError("manual zero-attachment dispatch required") result={"schema_version":"openlab-clark-route-dispatch-receipt-v1","status":"manual_route_offer_sent_not_submission","request_sha256":REQUEST,"offer_sha256":offer["offer_sha256"],"queue_sha256":offer["queue_sha256"],"role":offer["role"],"worksheet_sha256":offer["worksheet_sha256"],"sent_on":dispatch["sent_on"],"expires_on":offer["expires_on"],"provider_message_id_sha256":dispatch["provider_message_id_sha256"],"recipient_address_stored":False,"automatic_contact":False,"message_sent":True,"attachments_sent":0,"attachment_received":False,"submission_created":False,"identity_verified":False,"independence_verified":False,"scientific_validation_claimed":False} result["dispatch_sha256"]=sha256(json.dumps(result,ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest();return result def main(): p=argparse.ArgumentParser();p.add_argument("offer");p.add_argument("dispatch");p.add_argument("--out",required=True);a=p.parse_args();r=record(json.loads(Path(a.offer).read_text(encoding="utf-8")),json.loads(Path(a.dispatch).read_text(encoding="utf-8")));Path(a.out).write_text(json.dumps(r,ensure_ascii=False,indent=2)+"\n",encoding="utf-8");print(json.dumps(r,ensure_ascii=False)) if __name__=="__main__":main()