X402.ExtensionResponses (X402 v0.9.0)

Copy Markdown View Source

Encodes and decodes the EXTENSION-RESPONSES facilitator sidechannel.

Facilitators may report extension-specific processing outcomes on /verify and /settle responses through a transport sidechannel that is not part of the JSON body (x402 v2 §7.2.1). On HTTP that sidechannel is the EXTENSION-RESPONSES header: Base64-encoded JSON keyed by extension name, each value being that extension's outcome object — for example the bazaar extension reports {"bazaar": {"status": "success"}}.

The sidechannel is for the resource server only and is never forwarded to the buyer: X402.Facilitator surfaces it as the :extension_responses key of verify/settle results, and X402.Plug.PaymentGate keeps it out of the PAYMENT-RESPONSE header.

Decoding is lenient by design at the call sites: the sidechannel is advisory, so a malformed header is logged and dropped rather than failing the payment. The functions here still report structured errors so callers can make that choice.

Summary

Header Encoding

Decodes a Base64 EXTENSION-RESPONSES value to a map of outcomes.

Encodes extension outcomes to a Base64 header value.

Extracts and decodes the sidechannel from a list of response headers.

Decodes the sidechannel from response headers, dropping malformed values.

Returns the canonical sidechannel header name.

Types

t()

Extension outcomes keyed by extension name.

Header Encoding

decode(value)

(since 0.9.0)
@spec decode(String.t()) :: {:ok, t()} | {:error, decode_error()}

Decodes a Base64 EXTENSION-RESPONSES value to a map of outcomes.

Returns {:error, :payload_too_large} above 8 KB, {:error, :invalid_base64} or {:error, :invalid_json} for undecodable values, and {:error, :invalid_responses} when the JSON is not an object of objects.

Examples

iex> X402.ExtensionResponses.decode("eyJiYXphYXIiOnsic3RhdHVzIjoic3VjY2VzcyJ9fQ==")
{:ok, %{"bazaar" => %{"status" => "success"}}}

iex> X402.ExtensionResponses.decode(Base.encode64(~s({"bazaar":"success"})))
{:error, :invalid_responses}

iex> X402.ExtensionResponses.decode("%%%")
{:error, :invalid_base64}

encode(responses)

(since 0.9.0)
@spec encode(t()) :: {:ok, String.t()} | {:error, encode_error()}

Encodes extension outcomes to a Base64 header value.

The argument must be a map from extension name to an outcome map.

Examples

iex> {:ok, value} = X402.ExtensionResponses.encode(%{"bazaar" => %{"status" => "success"}})
iex> value
"eyJiYXphYXIiOnsic3RhdHVzIjoic3VjY2VzcyJ9fQ=="

iex> X402.ExtensionResponses.encode(%{"bazaar" => "success"})
{:error, :invalid_responses}

from_headers(headers)

(since 0.9.0)
@spec from_headers([{String.t(), String.t()}]) ::
  {:ok, t() | nil} | {:error, decode_error()}

Extracts and decodes the sidechannel from a list of response headers.

Header names are matched case-insensitively. Returns {:ok, nil} when the header is absent.

Examples

iex> X402.ExtensionResponses.from_headers([{"content-type", "application/json"}])
{:ok, nil}

iex> X402.ExtensionResponses.from_headers([
...>   {"extension-responses", "eyJiYXphYXIiOnsic3RhdHVzIjoic3VjY2VzcyJ9fQ=="}
...> ])
{:ok, %{"bazaar" => %{"status" => "success"}}}

iex> X402.ExtensionResponses.from_headers([{"EXTENSION-RESPONSES", "%%%"}])
{:error, :invalid_base64}

from_headers_lenient(headers)

(since 0.9.0)
@spec from_headers_lenient([{String.t(), String.t()}]) :: t() | nil

Decodes the sidechannel from response headers, dropping malformed values.

The advisory nature of the sidechannel means a malformed header must not fail the payment: this returns the decoded outcomes, or nil when the header is absent or invalid, emitting a [:x402, :extension_responses, :decode] telemetry event with status: :error and the :reason in the latter case.

Examples

iex> X402.ExtensionResponses.from_headers_lenient([{"extension-responses", "%%%"}])
nil

header_name()

(since 0.9.0)
@spec header_name() :: String.t()

Returns the canonical sidechannel header name.

Examples

iex> X402.ExtensionResponses.header_name()
"EXTENSION-RESPONSES"

Types

decode_error()

@type decode_error() ::
  :invalid_base64 | :invalid_json | :invalid_responses | :payload_too_large

encode_error()

@type encode_error() :: :invalid_responses | :invalid_json

t()

@type t() :: %{optional(String.t()) => map()}

Extension outcomes keyed by extension name.