X402.Extensions.Bazaar.Metadata (X402 v0.9.0)

Copy Markdown View Source

Validation rules for bazaar service metadata and routeTemplate values.

The bazaar extension lets resource servers publish provider-level metadata on the PaymentRequired.resource object (serviceName, tags, iconUrl) and, for dynamic routes, a top-level routeTemplate on the extension. Clients echo the resource block back to the facilitator, so a hostile client could try to poison a discovery catalog through it. The extension spec therefore defines soft-drop rules every SDK applies identically: a field that fails its rule is discarded and the rest of the metadata is kept.

These helpers mirror the reference SDKs' isValidServiceName, sanitizeTags, isValidIconUrl, sanitizeResourceServiceMetadata, and isValidRouteTemplate. X402.Plug.PaymentGate applies them to its own route configuration at init (rejecting invalid values outright, since a misconfigured server is a programmer error), and facilitators apply the soft-drop form through sanitize_resource/1 and extract_route_template/1 when cataloging.

One deliberate deviation: the spec asks for UTS #46 (IDNA) host normalization before the loopback / IP-literal checks on iconUrl. This library carries no IDNA dependency, so any host still containing non-ASCII characters after percent-decoding is rejected (fail closed) rather than normalized.

See the bazaar extension spec.

Summary

Types

A string-keyed ResourceInfo map as carried on the wire.

Functions

Returns the validated routeTemplate of a bazaar extension map, or nil.

Applies the soft-drop rules to a string-keyed ResourceInfo map.

Sanitizes a tags value.

Checks an iconUrl value.

Checks a routeTemplate value.

Checks a serviceName value.

Types

resource()

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

A string-keyed ResourceInfo map as carried on the wire.

Functions

extract_route_template(arg1)

(since 0.9.0)
@spec extract_route_template(term()) :: String.t() | nil

Returns the validated routeTemplate of a bazaar extension map, or nil.

A facilitator falls back to the concrete URL path when this returns nil, as the spec requires for absent or invalid templates.

Examples

iex> X402.Extensions.Bazaar.Metadata.extract_route_template(%{"routeTemplate" => "/users/:userId"})
"/users/:userId"

iex> X402.Extensions.Bazaar.Metadata.extract_route_template(%{"routeTemplate" => "../etc"})
nil

iex> X402.Extensions.Bazaar.Metadata.extract_route_template(%{"info" => %{}})
nil

sanitize_resource(resource)

(since 0.9.0)
@spec sanitize_resource(resource()) :: resource()

Applies the soft-drop rules to a string-keyed ResourceInfo map.

Drops serviceName and iconUrl when they fail their rules, replaces tags with its sanitized form (dropping the key when nothing valid is left), and leaves every other key untouched.

Examples

iex> X402.Extensions.Bazaar.Metadata.sanitize_resource(%{
...>   "url" => "https://api.example.com/weather",
...>   "serviceName" => "Example Weather",
...>   "tags" => ["weather", "WEATHER", "forecast"],
...>   "iconUrl" => "http://localhost/icon.png"
...> })
%{
  "url" => "https://api.example.com/weather",
  "serviceName" => "Example Weather",
  "tags" => ["weather", "forecast"]
}

iex> X402.Extensions.Bazaar.Metadata.sanitize_resource(%{"url" => "https://a.example", "tags" => "nope"})
%{"url" => "https://a.example"}

sanitize_tags(tags)

(since 0.9.0)
@spec sanitize_tags(term()) :: [String.t()]

Sanitizes a tags value.

Keeps the entries that pass the serviceName rules (non-empty printable ASCII, at most 32 characters), drops duplicates case-insensitively (first occurrence wins), and truncates the result to the first five entries. Anything that is not a list sanitizes to [].

Examples

iex> X402.Extensions.Bazaar.Metadata.sanitize_tags(["weather", "Weather", "", "forecast", 42])
["weather", "forecast"]

iex> X402.Extensions.Bazaar.Metadata.sanitize_tags(~w(a b c d e f g))
["a", "b", "c", "d", "e"]

iex> X402.Extensions.Bazaar.Metadata.sanitize_tags("weather")
[]

valid_icon_url?(url)

(since 0.9.0)
@spec valid_icon_url?(term()) :: boolean()

Checks an iconUrl value.

Valid values are at most 2048 characters, contain no control characters, and parse as an absolute http:// or https:// URL without userinfo. After percent-decoding, the host must not be an IP literal (v4 or v6), a loopback name (localhost, localhost.localdomain, ip6-localhost, ip6-loopback), an all-digit name (a decimal IP encoding such as 2130706433), a hex literal (0x7f000001), or contain non-ASCII characters.

Examples

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("https://api.example.com/icon.png")
true

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("data:image/png;base64,AAAA")
false

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("https://user@api.example.com/icon.png")
false

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("http://127.0.0.1/icon.png")
false

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("http://[::1]/icon.png")
false

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("http://%6cocalhost/icon.png")
false

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("http://2130706433/icon.png")
false

iex> X402.Extensions.Bazaar.Metadata.valid_icon_url?("http://0x7f000001/icon.png")
false

valid_route_template?(template)

(since 0.9.0)
@spec valid_route_template?(term()) :: boolean()

Checks a routeTemplate value.

Valid templates are non-empty, start with /, match ^/[a-zA-Z0-9_/:.\-~%]+$, and contain neither .. nor :// once percent-encoding is decoded.

Examples

iex> X402.Extensions.Bazaar.Metadata.valid_route_template?("/users/:userId")
true

iex> X402.Extensions.Bazaar.Metadata.valid_route_template?("/users/../admin")
false

iex> X402.Extensions.Bazaar.Metadata.valid_route_template?("/users/%2e%2e/admin")
false

iex> X402.Extensions.Bazaar.Metadata.valid_route_template?("/redirect/http://evil.example")
false

iex> X402.Extensions.Bazaar.Metadata.valid_route_template?("users/:userId")
false

valid_service_name?(name)

(since 0.9.0)
@spec valid_service_name?(term()) :: boolean()

Checks a serviceName value.

Valid names are non-empty, at most 32 characters long, and made of printable ASCII (U+0020–U+007E) only.

Examples

iex> X402.Extensions.Bazaar.Metadata.valid_service_name?("Example Weather")
true

iex> X402.Extensions.Bazaar.Metadata.valid_service_name?("")
false

iex> X402.Extensions.Bazaar.Metadata.valid_service_name?("Wetter für alle")
false

iex> X402.Extensions.Bazaar.Metadata.valid_service_name?(String.duplicate("a", 33))
false

iex> X402.Extensions.Bazaar.Metadata.valid_service_name?(:weather)
false