"""Platform Lambda — contract ingestor. Invoked via a Function URL (IAM auth) by consumer pipelines (one-way communication, D-051). Accepts { consumerRepo, contractId, contract, environment, action } and writes contracts to DynamoDB table acdl-contracts (PK consumerRepo, SK contractId#submittedAt). The report_error action (D-055) is prepared as a stub in this phase; the GitHub issue creation is implemented in Phase 25. Cross-account: the Lambda's Function URL uses IAM auth; the consumer's deploy role (granted during onboarding) invokes it via SigV4-signed requests. The invoke policy is scoped via ABAC (consumer repo identity). """ import datetime import json import os import boto3 TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts") GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token") PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl") _dynamodb = None _secrets_client = None def _get_dynamodb(): global _dynamodb if _dynamodb is None: _dynamodb = boto3.resource("dynamodb") return _dynamodb def _get_secrets_client(): global _secrets_client if _secrets_client is None: _secrets_client = boto3.client("secretsmanager") return _secrets_client def _iso8601_now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def _submit_contract(payload): consumer_repo = payload["consumerRepo"] contract_id = payload["contractId"] contract = payload["contract"] environment = payload["environment"] submitted_at = _iso8601_now() table = _get_dynamodb().Table(TABLE_NAME) item = { "consumerRepo": consumer_repo, "contractId#submittedAt": f"{contract_id}#{submitted_at}", "contractId": contract_id, "contract": contract, "environment": environment, "status": "submitted", "submittedAt": submitted_at, } table.put_item(TableName=TABLE_NAME, Item=item) return { "status": "ok", "contractId": contract_id, "action": "submit_contract", "submittedAt": submitted_at, } def _report_error(payload): # Phase 25 implements the GitHub issue creation. # This stub validates the payload and returns a prepared status. required = ["consumerRepo", "contractId", "error"] for field in required: if field not in payload: raise ValueError(f"report_error requires '{field}'") return { "status": "error_report_prepared", "contractId": payload["contractId"], "action": "report_error", } def lambda_handler(event, context): """AWS Lambda handler entry point. Accepts a Function-URL-style event whose ``body`` is a JSON string containing ``{ consumerRepo, contractId, contract, environment, action }``. """ try: body = event.get("body", "{}") if isinstance(body, str): payload = json.loads(body) else: payload = body action = payload.get("action", "submit_contract") if action == "submit_contract": # Validate required fields up front for a clean 400. for field in ("consumerRepo", "contractId", "contract", "environment"): if field not in payload: return { "statusCode": 400, "body": json.dumps({"error": f"missing field: {field}"}), } result = _submit_contract(payload) elif action == "report_error": result = _report_error(payload) else: return { "statusCode": 400, "body": json.dumps({"error": f"unknown action: {action}"}), } return {"statusCode": 200, "body": json.dumps(result)} except ValueError as e: return {"statusCode": 400, "body": json.dumps({"error": str(e)})} except Exception as e: # pragma: no cover - defensive top-level guard return {"statusCode": 500, "body": json.dumps({"error": str(e)})}