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.
Header Encoding
@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}
@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}
@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}
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
@spec header_name() :: String.t()
Returns the canonical sidechannel header name.
Examples
iex> X402.ExtensionResponses.header_name()
"EXTENSION-RESPONSES"