# `RequestSeal.AshHooks.Http`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/ash_hooks.ex#L135)

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.

# `request`

```elixir
@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 do `host`, `connection`,
    `content-length`, `transfer-encoding`, `signature`, and
    `signature-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) - a `t:RequestSeal.signing_spec/0`. `:digest`
      defaults 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) - a `RequestSeal.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
    `:timeout` replaced by the remaining time and `:request_seal` removed.

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
    from `AshHooks.Http.Bounded`.
  * `{:error, :timeout}` - `:timeout` is outside 1..300,000, or signing
    used up the whole deadline.
  * `{:error, {:terminal, :request_seal_signing_failed}}` - any signing
    failure: invalid `:request_seal` options or spec, missing coverage,
    a rejected header or method, or a `RequestSeal.sign/4` error. 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 by
    `AshHooks.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)
    :ok

Fragment (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]]
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
