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
Functions
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
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"}
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")
[]
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
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
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