diff --git a/samples/go/pkg/ap2/types/mandate.go b/samples/go/pkg/ap2/types/mandate.go index 4aa6701c..6cfab83e 100644 --- a/samples/go/pkg/ap2/types/mandate.go +++ b/samples/go/pkg/ap2/types/mandate.go @@ -29,6 +29,7 @@ type IntentMandate struct { SKUs []string `json:"skus,omitempty"` RequiresRefundability *bool `json:"requires_refundability,omitempty"` IntentExpiry string `json:"intent_expiry"` + UserAuthorization *string `json:"user_authorization,omitempty"` } func NewIntentMandate() *IntentMandate { @@ -75,6 +76,8 @@ type PaymentMandateContents struct { PaymentDetailsTotal PaymentItem `json:"payment_details_total"` PaymentResponse PaymentResponse `json:"payment_response"` MerchantAgent string `json:"merchant_agent"` + IntentMandateID *string `json:"intent_mandate_id,omitempty"` + TransactionModality *string `json:"transaction_modality,omitempty"` Timestamp string `json:"timestamp,omitempty"` } diff --git a/samples/python/src/roles/shopping_agent/tools.py b/samples/python/src/roles/shopping_agent/tools.py index 1d8b7bf1..139aa7a4 100644 --- a/samples/python/src/roles/shopping_agent/tools.py +++ b/samples/python/src/roles/shopping_agent/tools.py @@ -31,6 +31,7 @@ from ap2.types.contact_picker import ContactAddress from ap2.types.mandate import CART_MANDATE_DATA_KEY from ap2.types.mandate import CartMandate +from ap2.types.mandate import IntentMandate from ap2.types.mandate import PAYMENT_MANDATE_DATA_KEY from ap2.types.mandate import PaymentMandate from ap2.types.mandate import PaymentMandateContents @@ -240,6 +241,9 @@ def sign_mandates_on_user_device(tool_context: ToolContext) -> str: """ payment_mandate: PaymentMandate = tool_context.state["payment_mandate"] cart_mandate: CartMandate = tool_context.state["cart_mandate"] + intent_mandate: IntentMandate | None = tool_context.state.get( + "intent_mandate" + ) cart_mandate_hash = _generate_cart_mandate_hash(cart_mandate) payment_mandate_hash = _generate_payment_mandate_hash( payment_mandate.payment_mandate_contents @@ -247,9 +251,15 @@ def sign_mandates_on_user_device(tool_context: ToolContext) -> str: # A JWT containing the user's digital signature to authorize the transaction. # The payload uses hashes to bind the signature to the specific cart and # payment details, and includes a nonce to prevent replay attacks. - payment_mandate.user_authorization = ( - cart_mandate_hash + "_" + payment_mandate_hash - ) + hashes = [cart_mandate_hash, payment_mandate_hash] + + # In Human Not Present scenarios, include the intent mandate hash to bind + # the payment authorization to the user-signed intent mandate. + if intent_mandate: + intent_mandate_hash = _generate_intent_mandate_hash(intent_mandate) + hashes.append(intent_mandate_hash) + + payment_mandate.user_authorization = "_".join(hashes) tool_context.state["signed_payment_mandate"] = payment_mandate return payment_mandate.user_authorization @@ -302,6 +312,29 @@ def _generate_cart_mandate_hash(cart_mandate: CartMandate) -> str: return "fake_cart_mandate_hash_" + cart_mandate.contents.id +def _generate_intent_mandate_hash(intent_mandate: IntentMandate) -> str: + """Generates a cryptographic hash of the IntentMandate. + + This hash binds the user's payment authorization to the specific + user-signed Intent Mandate, ensuring that Human Not Present transactions + can be traced back to a verified user intent. + + Note: This is a placeholder implementation for development. A real + implementation must use a secure hashing algorithm (e.g., SHA-256) on the + canonical representation of the IntentMandate object. + + Args: + intent_mandate: The IntentMandate object to hash. + + Returns: + A string representing the hash of the intent mandate. + """ + return ( + "fake_intent_mandate_hash_" + + intent_mandate.natural_language_description[:20] + ) + + def _generate_payment_mandate_hash( payment_mandate_contents: PaymentMandateContents, ) -> str: diff --git a/src/ap2/types/mandate.py b/src/ap2/types/mandate.py index c5506689..42f653b8 100644 --- a/src/ap2/types/mandate.py +++ b/src/ap2/types/mandate.py @@ -74,6 +74,25 @@ class IntentMandate(BaseModel): ..., description="When the intent mandate expires, in ISO 8601 format.", ) + user_authorization: Optional[str] = Field( + None, + description=( + """ + A base64url-encoded JSON Web Token (JWT) that digitally signs the + intent mandate contents by the user's private key. This provides + non-repudiable proof of the user's intent and prevents tampering + by the shopping agent. + + If this field is present, user_cart_confirmation_required can be + set to false, allowing the agent to execute purchases in the + user's absence. + + If this field is None, user_cart_confirmation_required must be true, + requiring the user to confirm each specific purchase. + """ + ), + example="eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXhhbXBsZ...", + ) class CartContents(BaseModel): @@ -154,6 +173,24 @@ class PaymentMandateContents(BaseModel): ), ) merchant_agent: str = Field(..., description="Identifier for the merchant.") + intent_mandate_id: Optional[str] = Field( + None, + description=( + "Reference to the user-signed Intent Mandate that authorizes " + "this transaction in Human Not Present scenarios. This allows " + "the payment network to verify that the 'human not present' " + "transaction has pre-authorization support from a 'human present' " + "intent mandate. Required for HNP transactions." + ), + ) + transaction_modality: Optional[str] = Field( + None, + description=( + "Transaction modality: 'human_present' or 'human_not_present'. " + "This signals to the payment network whether the user was present " + "at the time of payment authorization." + ), + ) timestamp: str = Field( description=( "The date and time the mandate was created, in ISO 8601 format."