Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions samples/go/pkg/ap2/types/mandate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
}

Expand Down
39 changes: 36 additions & 3 deletions samples/python/src/roles/shopping_agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -240,16 +241,25 @@ 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
)
# 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

Expand Down Expand Up @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions src/ap2/types/mandate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
)
Comment on lines +77 to +95
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for user_authorization states an important business rule: "If this field is None, user_cart_confirmation_required must be true". To ensure data integrity and prevent invalid model states, this rule should be enforced with a Pydantic validator.

Here is an example of how you could implement this with a model_validator:

from pydantic import model_validator

class IntentMandate(BaseModel):
    # ... existing fields ...

    @model_validator(mode='after')
    def check_user_authorization_logic(self) -> 'IntentMandate':
        if self.user_authorization is None and not self.user_cart_confirmation_required:
            raise ValueError(
                'user_cart_confirmation_required must be True if user_authorization is not provided'
            )
        return self

This will make your model more robust.



class CartContents(BaseModel):
Expand Down Expand Up @@ -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."
),
)
Comment on lines +176 to +185
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for intent_mandate_id mentions it is "Required for HNP transactions." This is a critical validation rule that should be enforced within the model to maintain data consistency. You can use a Pydantic model_validator to ensure intent_mandate_id is provided when transaction_modality is set for a Human Not Present transaction.

Here's how you could implement this check:

from pydantic import model_validator

class PaymentMandateContents(BaseModel):
    # ... existing fields ...

    @model_validator(mode='after')
    def check_hnp_requirements(self) -> 'PaymentMandateContents':
        if self.transaction_modality == 'human_not_present' and self.intent_mandate_id is None:
            raise ValueError('intent_mandate_id is required for human_not_present transactions')
        return self

(Note: This example assumes transaction_modality is a string. If you adopt the Enum suggestion from another comment, you would compare against the enum member, e.g., TransactionModality.HUMAN_NOT_PRESENT.)

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."
),
)
Comment on lines +186 to +193
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The transaction_modality field accepts a limited set of string values: 'human_present' or 'human_not_present'. Using a plain string is susceptible to errors from typos or case variations. It's a best practice to use an Enum for such fields to enforce valid values, which improves code clarity and robustness.

You could define an Enum and use it for this field:

from enum import Enum

class TransactionModality(str, Enum):
    HUMAN_PRESENT = "human_present"
    HUMAN_NOT_PRESENT = "human_not_present"

class PaymentMandateContents(BaseModel):
    # ...
    transaction_modality: Optional[TransactionModality] = Field(
        None,
        description=(
            "Transaction modality. 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."
Expand Down
Loading