"""HTTP utility functions for encoding/decoding x402 headers."""
from __future__ import annotations
import base64
import json
from typing import Any
from ..schemas import (
PaymentPayload,
PaymentRequired,
SettleResponse,
)
from ..schemas.v1 import PaymentPayloadV1, PaymentRequiredV1
from .constants import PAYMENT_REQUIRED_HEADER, X_PAYMENT_HEADER
def safe_base64_encode(data: str) -> str:
"""Base64 encode a string safely."""
return base64.b64encode(data.encode("utf-8")).decode("utf-8")
def safe_base64_decode(data: str) -> str:
"""Base64 decode a string safely."""
return base64.b64decode(data.encode("utf-8")).decode("utf-8")
def encode_payment_signature_header(payload: PaymentPayload | PaymentPayloadV1) -> str:
"""Encode a payment payload as a base64 header value."""
return safe_base64_encode(payload.model_dump_json(by_alias=True, exclude_none=True))
def decode_payment_signature_header(
header_value: str,
) -> PaymentPayload | PaymentPayloadV1:
"""Decode a base64 payment signature header into a PaymentPayload."""
json_str = safe_base64_decode(header_value)
data = json.loads(json_str)
# Detect version
version = data.get("x402Version", 2)
if version == 1:
return PaymentPayloadV1.model_validate(data)
return PaymentPayload.model_validate(data)
def encode_payment_required_header(
payment_required: PaymentRequired | PaymentRequiredV1,
) -> str:
"""Encode a PaymentRequired object as a base64 header value."""
return safe_base64_encode(payment_required.model_dump_json(by_alias=True, exclude_none=True))
def decode_payment_required_header(
header_value: str,
) -> PaymentRequired | PaymentRequiredV1:
"""Decode a base64 payment required header into a PaymentRequired object."""
json_str = safe_base64_decode(header_value)
data = json.loads(json_str)
# Detect version
version = data.get("x402Version", 2)
if version == 1:
return PaymentRequiredV1.model_validate(data)
return PaymentRequired.model_validate(data)
def encode_payment_response_header(settle_response: SettleResponse) -> str:
"""Encode a SettleResponse object as a base64 header value."""
return safe_base64_encode(settle_response.model_dump_json(by_alias=True, exclude_none=True))
def decode_payment_response_header(header_value: str) -> SettleResponse:
"""Decode a base64 payment response header into a SettleResponse object."""
json_str = safe_base64_decode(header_value)
return SettleResponse.model_validate_json(json_str)
def detect_payment_required_version(
headers: dict[str, str],
body: bytes | None = None,
) -> int:
"""Detect the x402 protocol version from HTTP response headers and body.
Prioritizes V2 header, then V1 body.
Args:
headers: Response headers (case-insensitive).
body: Optional response body bytes.
Returns:
Protocol version (1 or 2).
Raises:
ValueError: If version cannot be detected.
"""
normalized_headers = {k.upper(): v for k, v in headers.items()}
if PAYMENT_REQUIRED_HEADER in normalized_headers:
return 2
if X_PAYMENT_HEADER in normalized_headers:
return 1
if body:
try:
data = json.loads(body.decode("utf-8"))
version = data.get("x402Version")
if version in [1, 2]:
return version
except (json.JSONDecodeError, UnicodeDecodeError):
pass
raise ValueError("Could not detect x402 version from response")
def htmlsafe_json_dumps(obj: Any) -> str:
"""Serialize object to JSON with HTML-safe escaping.
Escapes <, >, and & characters to prevent XSS attacks when
embedding JSON in HTML script tags.
Args:
obj: Object to serialize to JSON.
Returns:
HTML-safe JSON string.
"""
_json_script_escapes = {
ord(">"): "\\u003E",
ord("<"): "\\u003C",
ord("&"): "\\u0026",
}
return json.dumps(obj).translate(_json_script_escapes)