"""Nova Outbox Writer — write an evidence event to the DynamoDB outbox. ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before ack). The event is hash-chained (SHA-256 over canonical JSON); the first event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE CONFIDENCE_COMPUTED event. The outbox table (Phase 08): nova-outbox, PAY_PER_REQUEST, PK contractId, SK eventType#eventTs, TTL expire_at = now + 365d (D-044). CLI: outbox_writer.py (uses AWS creds from env) """ import datetime import hashlib import json import os import sys import boto3 OUTBOX_TABLE = "nova-outbox" REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") def _canonical_hash(event): """SHA-256 over canonical JSON (sort_keys, compact separators).""" canonical = json.dumps(event, sort_keys=True, separators=(",", ":")) return hashlib.sha256(canonical.encode("utf-8")).hexdigest() def write_event(event, outbox_table=OUTBOX_TABLE, region=REGION): """Write an evidence event to the DynamoDB outbox. Returns the item dict.""" contract_id = event["contractId"] event_type = event.get("eventType", "CONFIDENCE_COMPUTED") event_ts = event.get("ts") or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") sk = f"{event_type}#{event_ts}" # Chain: first event = GENESIS (D-P10-3 spike writes one event). prev_hash = event.get("prev_event_hash", "GENESIS") event_hash = _canonical_hash(event) item = { "contractId": {"S": contract_id}, "eventType#eventTs": {"S": sk}, "payload": {"S": json.dumps(event, sort_keys=True)}, "prev_event_hash": {"S": prev_hash}, "hash": {"S": event_hash}, "environment": {"S": str(event.get("environment", ""))}, "stack": {"S": str(event.get("stack", ""))}, "score": {"N": str(event.get("score", 0))}, "band": {"S": str(event.get("band", ""))}, "expire_at": {"N": str(int((datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365)).timestamp()))}, } session = boto3.Session(region_name=region) dyn = session.client("dynamodb") dyn.put_item(TableName=outbox_table, Item=item) return item if __name__ == "__main__": if len(sys.argv) != 2: print("usage: outbox_writer.py ", file=sys.stderr) sys.exit(2) with open(sys.argv[1], "r") as fh: event = json.load(fh) item = write_event(event) print(json.dumps({k: list(v.values())[0] for k, v in item.items()}, indent=2))