fix: add user_authorization to IntentMandate and intent binding to PaymentMandate#189
Conversation
…ymentMandate - Add user_authorization (JWT signature) field to IntentMandate for non-repudiable proof of user intent in HNP scenarios - Add intent_mandate_id and transaction_modality to PaymentMandateContents for payment network verification - Update sign_mandates_on_user_device to include intent_mandate_hash when intent mandate is present Fixes google-agentic-commerce#150 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the system's capability to handle Human Not Present (HNP) transactions securely. It establishes a robust framework for users to pre-authorize transactions through digitally signed intent mandates, allowing the payment network to verify these pre-authorizations for HNP payments. This ensures non-repudiable proof of user intent and tightly binds payment authorizations to specific user-signed intents, thereby increasing trust and security in automated transaction flows. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces fields to support Human Not Present (HNP) transaction flows. The changes add user_authorization to IntentMandate and intent_mandate_id and transaction_modality to PaymentMandateContents. The logic for signing mandates is updated to include an intent mandate hash when available. The corresponding Go type definitions are also updated. My review focuses on improving data integrity and validation in the Pydantic models by suggesting the use of validators and enums to enforce business rules described in the field descriptions.
| 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...", | ||
| ) |
There was a problem hiding this comment.
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 selfThis will make your model more robust.
| 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." | ||
| ), | ||
| ) |
There was a problem hiding this comment.
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." | ||
| ), | ||
| ) |
There was a problem hiding this comment.
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."
),
)
# ...
Summary
user_authorization(JWT signature) field toIntentMandatefor non-repudiable proof of user intent in Human Not Present (HNP) scenariosintent_mandate_idandtransaction_modalityfields toPaymentMandateContentsso the payment network can verify HNP transactions have proper pre-authorizationsign_mandates_on_user_deviceto includeintent_mandate_hashwhen an intent mandate is present, binding the payment authorization to the user-signed intentsamples/go/pkg/ap2/types/mandate.goto matchTest plan
IntentMandatenow accepts optionaluser_authorizationfieldPaymentMandateContentsnow accepts optionalintent_mandate_idandtransaction_modalityfieldssign_mandates_on_user_deviceappends intent mandate hash whenintent_mandateis present in stateFixes #150
🤖 Generated with Claude Code