X402.Extension behaviour (X402 v0.9.0)

Copy Markdown View Source

Behaviour for resource-server extension adapters run by X402.Plug.PaymentGate.

An adapter packages one protocol extension's server-side lifecycle so the gate can run it from a single option instead of the user wiring each step by hand:

plug X402.Plug.PaymentGate,
  routes: [...],
  extensions: [
    {X402.Extensions.PaymentIdentifier.Adapter, required: true},
    {X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}
  ]

Every callback but key/0 is optional:

  • init/1 validates the adapter's options once, when the gate's own options are validated; the returned options are what the remaining callbacks receive.
  • advertise/2 returns the value to advertise under the extension key in every PaymentRequired.extensions (merged over the route's static extensions map), or nil to advertise nothing for this request.
  • validate/3 runs after the generic extension echo check on every payment, with the value the client echoed (or nil) and the value that was advertised (or nil). An error rejects the payment with 400 invalid_payload and a {:extension_invalid, key, reason} telemetry reason.
  • after_verify/4 and after_settle/4 are notified with the facilitator's result once verification succeeded and once settlement succeeded. Their return values are ignored and exceptions are logged.

advertise/2 and validate/3 are part of the request's control flow, so an exception there propagates like any other programmer error.

The sign-in-with-x extension keeps its dedicated :siwx gate option: its challenge is regenerated per response and it changes the request flow (a proof can skip payment entirely), which the generic advertise/validate shape does not express. Bazaar discovery metadata likewise has its own :bazaar route option because it depends on the matched route pattern.

Summary

Types

A normalized adapter entry.

An adapter entry as accepted by the gate's :extensions option.

Callbacks

Builds the value advertised under the extension key, or nil.

Notified after a successful facilitator settlement.

Notified after a successful facilitator verification.

Validates the adapter options at gate initialization.

Returns the extension key on the wire (for example "payment-identifier").

Validates the client's echoed value against the advertised one.

Functions

Merges every adapter's advertisement over a base extensions map.

Runs every adapter's validate/3 over the echoed and advertised maps.

Validates one :extensions entry and normalizes it to {module, opts}.

Types

entry()

@type entry() :: {module(), keyword()}

A normalized adapter entry.

spec()

@type spec() :: module() | {module(), keyword()}

An adapter entry as accepted by the gate's :extensions option.

Callbacks

advertise(keyword, t)

(optional)
@callback advertise(
  keyword(),
  X402.Hooks.RequestContext.t()
) :: map() | nil

Builds the value advertised under the extension key, or nil.

after_settle(payload, requirements, result, keyword)

(optional)
@callback after_settle(
  payload :: map(),
  requirements :: map(),
  result :: map(),
  keyword()
) :: term()

Notified after a successful facilitator settlement.

after_verify(payload, requirements, result, keyword)

(optional)
@callback after_verify(
  payload :: map(),
  requirements :: map(),
  result :: map(),
  keyword()
) :: term()

Notified after a successful facilitator verification.

init(keyword)

(optional)
@callback init(keyword()) :: {:ok, keyword()} | {:error, String.t()}

Validates the adapter options at gate initialization.

key()

@callback key() :: String.t()

Returns the extension key on the wire (for example "payment-identifier").

validate(echoed, advertised, keyword)

(optional)
@callback validate(echoed :: term(), advertised :: term(), keyword()) ::
  :ok | {:error, term()}

Validates the client's echoed value against the advertised one.

Functions

after_settle_all(entries, payload, requirements, result)

(since 0.9.0)
@spec after_settle_all([entry()], map(), map(), map()) :: :ok

Notifies every adapter defining after_settle/4.

Return values are ignored; an exception is logged and the remaining adapters still run.

after_verify_all(entries, payload, requirements, result)

(since 0.9.0)
@spec after_verify_all([entry()], map(), map(), map()) :: :ok

Notifies every adapter defining after_verify/4.

Return values are ignored; an exception is logged and the remaining adapters still run.

validate_all(entries, echoed, advertised)

(since 0.9.0)
@spec validate_all([entry()], term(), map()) ::
  :ok | {:error, {:extension_invalid, String.t(), term()}}

Runs every adapter's validate/3 over the echoed and advertised maps.

Stops at the first error, tagging it with the adapter's key.

Examples

iex> entries = [{X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}]
iex> advertised = %{"builder-code" => X402.Extensions.BuilderCode.extension("my_app")}
iex> X402.Extension.validate_all(entries, %{"builder-code" => %{"a" => "my_app"}}, advertised)
:ok

iex> entries = [{X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}]
iex> advertised = %{"builder-code" => X402.Extensions.BuilderCode.extension("my_app")}
iex> X402.Extension.validate_all(entries, %{"builder-code" => %{"a" => "other"}}, advertised)
{:error, {:extension_invalid, "builder-code", :builder_code_mismatch}}

validate_spec(module)

(since 0.9.0)
@spec validate_spec(term()) :: {:ok, entry()} | {:error, String.t()}

Validates one :extensions entry and normalizes it to {module, opts}.

Designed for NimbleOptions custom validation. The module must define key/0; when it defines init/1 the options are passed through it.

Examples

iex> X402.Extension.validate_spec(X402.Extensions.PaymentIdentifier.Adapter)
{:ok, {X402.Extensions.PaymentIdentifier.Adapter, [required: false]}}

iex> X402.Extension.validate_spec({X402.Extensions.PaymentIdentifier.Adapter, required: true})
{:ok, {X402.Extensions.PaymentIdentifier.Adapter, [required: true]}}

iex> X402.Extension.validate_spec(:not_an_adapter)
{:error, "expected a module implementing X402.Extension (key/0), got: :not_an_adapter"}