ash_hooks HTTP adapter signing final method, URL, headers, and body bytes.
Configure http: RequestSeal.AshHooks.Http and
http_opts: [request_seal: [spec: signing_spec, signer: custody_handle]];
request/5 lists every option. Each delivery attempt signs once with fresh
metadata and hands the signed request to ash_hooks' bounded transport.
ash_hooks continues to own SSRF resolve-and-pin, durable attempts, retries,
secret references, rotation, and response classification.
ash_hooks still requires its Standard Webhooks secret reference and emits
those headers alongside RFC 9421 fields. Select the RFC label explicitly.
Adapter options receive no endpoint or tenant identity; choose custody in
static http_opts or its ash_hooks MFA resolver. No client or pool starts here.
Summary
Functions
Signs a final webhook request and delegates it to ash_hooks' bounded transport.
Functions
@spec request(atom() | binary(), binary(), map(), binary() | nil, keyword()) :: {:ok, %{status: integer(), headers: list(), body: binary() | nil}} | {:error, term()}
Signs a final webhook request and delegates it to ash_hooks' bounded transport.
ash_hooks calls this as its AshHooks.Http adapter. Header names are
lowercased, the request is signed with RequestSeal.sign/4, and the signed
headers go to AshHooks.Http.Bounded with the remaining total
timeout and otherwise unchanged transport options.
Parameters
method-:connect,:delete,:get,:head,:options,:patch,:post,:put,:trace, or a binary method (uppercased).url- the absolute target URL.headers- a map of request headers. Names are lowercased; names that collide after lowercasing reject, as dohost,connection,content-length,transfer-encoding,signature, andsignature-input.body- the exact body bytes, or nil.opts- keyword list of the options below.
Options
:timeout- total milliseconds for signing and transport, 1..300,000. Default 15,000.:request_seal(required) - keyword list of these keys; others reject::spec(required) - aRequestSeal.signing_spec/0.:digestdefaults to["sha-256"]and must not be nil. The components must include"@method","@authority","@path","content-digest","content-type", and"webhook-id"without component parameters, plus"@query"when the URL has a query.:signer(required) - aRequestSeal.KeyHandle; function signers reject.:clock- arity-zero function returning Unix seconds.:signing_timeout- milliseconds, default 5,000, capped at the time left before the total deadline.
- any other option - passed unchanged to
AshHooks.Http.Bounded, with:timeoutreplaced by the remaining time and:request_sealremoved.
Every attempt regenerates created, expires, and a random nonce. From 0.5.0,
a nonce is required on every signed attempt: a spec with nonce: nil is
refused as a signing failure before any request is sent.
Returns
{:ok, %{status: status, headers: headers, body: body}}- the response fromAshHooks.Http.Bounded.{:error, :timeout}-:timeoutis outside 1..300,000, or signing used up the whole deadline.{:error, {:terminal, :request_seal_signing_failed}}- any signing failure: invalid:request_sealoptions or spec, missing coverage, a rejected header or method, or aRequestSeal.sign/4error. The terminal tag is for caller classification; ash_hooks 2.0 retries returned adapter errors other than:unsafe_destination.{:error, term}- any other error returned byAshHooks.Http.Bounded, such as:unsafe_destination. Transport exceptions propagate unchanged.
Examples
iex> {_public, seed} = :crypto.generate_key(:eddsa, :ed25519)
iex> {:ok, handle} = RequestSeal.Custody.Local.new("ed25519", {:ed25519, seed})
iex> spec = %{
...> label: "sig",
...> algorithm: "ed25519",
...> components:
...> ~s[("@method" "@authority" "@path" "content-digest" "content-type" "webhook-id")],
...> expires_in: 300
...> }
iex> url = "https://hooks.example.com/builds"
iex> headers = %{"content-type" => "application/json", "webhook-id" => "msg-1"}
iex> RequestSeal.AshHooks.Http.request(:post, url, headers, "{}", timeout: 0)
{:error, :timeout}
iex> RequestSeal.AshHooks.Http.request(:post, url <> "?page=2", headers, "{}",
...> request_seal: [spec: spec, signer: handle]
...> )
{:error, {:terminal, :request_seal_signing_failed}}
iex> RequestSeal.AshHooks.Http.request(:post, url, Map.put(headers, "host", "other.example.com"), "{}",
...> request_seal: [spec: spec, signer: handle]
...> )
{:error, {:terminal, :request_seal_signing_failed}}
iex> RequestSeal.Custody.Local.release(handle)
:okFragment (requires an ash_hooks delivery resource with a PostgreSQL data layer and a reachable HTTPS receiver; not executed here):
AshHooks.Delivery.run(%{"endpoint_id" => endpoint_id, "event_uuid" => event_id},
deliveries: MyApp.Hooks.Delivery,
endpoints: MyApp.Hooks.Endpoint,
secret_resolver: secret_resolver,
http: RequestSeal.AshHooks.Http,
http_opts: [request_seal: [spec: spec, signer: handle]]
)