Optional Req final-attempt signing and verification before body decoding.
attach/2 adds a signing step that runs last on every attempt and, unless
verify: :none, a verification step that runs first on every response.
Every attempt removes only this adapter's previously appended values, then
regenerates parameters. Req's own redirect/retry choices still govern whether
another attempt runs. Signing must remain the last request step; a later step
rejects before transport. Only Req's Finch adapter is supported; framework
hooks that can mutate transport bytes after signing reject. No pool, server,
or background fetch starts.
When required, verification is the first response step, before retry, HTTP errors,
redirect, digest auth, decompression, or decoding. Each intermediate response
must satisfy the selected policy: unsigned 3xx/5xx responses fail closed
without redirecting or retrying. It uses the exact sent Message once per
response. A rejection returns a bounded RequestSeal.Adapter.Error with
:response_rejected, never a response body.
All verified delivery, including synchronous responses, uses a collector with
explicit max_stream_bytes: overflow stops transport before complete
buffering with :limit. Chunks are buffered within that bound and hashed when
policy requires content integrity; automatic streams refuse caller-selected
representation hashing. The original destination receives chunks only after
verification. Collectables are opened only after success. Req cache handling,
http_errors: :raise, checksum wrappers on streaming destinations, and custom
response steps before verification are refused under verification, since
these can consume or expose unverified bodies.
Summary
Functions
Attaches final-attempt signing and optional response verification to a Req request.
Returns the verification stored in a response's private metadata by an attached request.
Functions
@spec attach(Req.Request.t(), keyword()) :: {:ok, Req.Request.t()} | {:error, RequestSeal.Adapter.Error.t()}
Attaches final-attempt signing and optional response verification to a Req request.
The returned request carries three added steps: an option check prepended
to the request steps, a signing step appended as the last request step, and
a response step. With a verification map the response step runs first;
with verify: :none it runs just before Req's :decompress_body step and
makes no verification claim. Nothing is sent and no process starts. Run the
request with Req.request/1 against a caller-started Finch pool.
Parameters
request- aReq.Requestbuilt byReq.new/1that still has Req's:decompress_bodyresponse step and Req's Finch adapter. URL, method, headers, body, andintoare Req struct fields. The request options may include onlyuser_agent,compressed,range,base_url,params,path_params,path_params_style,auth(except digest),form,form_multipart,json,compress_body,checksum,raw,http_errors,decode_body,decoders,decode_json,redirect,redirect_trusted,redirect_log_level,max_redirects,retry,retry_delay,retry_log_level,max_retries,cache,cache_dir,finch,request_timeout,receive_timeout,pool_timeout,follow_redirects,location_trusted, andredact_auth. These are checked at attachment, before the request steps, and at final signing.finchaccepts only an explicit caller-owned atomnameand optionalpool_timeout,receive_timeout, andrequest_timeout; configure trusted pool connections when starting the caller-owned pool. Caller-supplied Host, connection overrides, proxies, UNIX sockets, Finch private metadata and pool options, IPv6 overrides, and Req'saws_sigv4option reject with:invalid_options.into: :selfandinto: :legacy_selfreject.opts- keyword list of the options below. Unknown or duplicate options reject.
Options
:sign(required) - the signing specification described inRequestSeal.Finch.sign/4.:signer(required) - aRequestSeal.KeyHandleor an arity-two function signer, as inRequestSeal.Finch.sign/4.:verify(required) -:none, or a map with exactly:policy(a validRequestSeal.Policy),:label(the response signature label), and:max_stream_bytes(a nonnegative integer bound on buffered response bytes; any other value, including nil, rejects with:invalid_options, from 0.5.0; 0.4.1 rejects nil with:unsupported_delivery). With a map, the request must keephttp_errors: :return(the default), must not setcache: true, and must not have a response step ahead of verification. A non-nilintodestination must be a two-arity function or aCollectable, must not be combined with Req'schecksum, and requires a policy whose content is:not_requiredor of kind:content. With:none, ordinary synchronous delivery keeps Req's semantics.:signing_timeout- 1..300,000 ms. Default 5,000.:clock- arity-zero function returning Unix seconds. Default system seconds.:request_body-:as_is(default) or{:retain, max}with a nonnegative integermax. Retention reads a Req function body or an enumerable stream once undermaxbytes and replaces it with replayable bytes before transport. With:as_isan enumerable stream is sent unread with its bytes unavailable for coverage, and a Req function body rejects at signing with:body_unavailable.:redirect-:same_origin(default) rejects any attempt whose origin differs from the first attempt's.{:allow, origins}permits up to 64 listed origins, each anhttporhttpsURL with a host and no path, query, fragment, or userinfo, at most 2,048 bytes. An origin is scheme, lowercased host, and port. Allowed cross-origin attempts stripauthorization,cookie,proxy-authorization, and:credential_headers, even with Req'sredirect_trusted. Stripping is monotonic: once removed, credential headers stay removed for later attempts, including a later redirect back to the first origin.:credential_headers- at most 64 HTTP header names of 1..256 token bytes, matched case insensitively, stripped on allowed cross-origin attempts. Default[].
Returns
{:ok, %Req.Request{}}- the request with RequestSeal steps attached.{:error, %RequestSeal.Adapter.Error{stage: :attach, attempt: 0}}- reasons::invalid_options(option, spec, signer, Req option, or Host value; a request already attached; or an unexpected exception),:invalid_request(not aReq.Request),:unsupported_component(host,tr, orreqcomponents in the spec, or a streaming destination with representation hashing),:unsupported_delivery(adapter,into, verification delivery restrictions, or a missing:decompress_bodystep), and:limit(component list bound). Meanings:RequestSeal.Adapter.Error.
Req.request/1 on the attached request returns
{:error, %RequestSeal.Adapter.Error{}} with attempt set to the final
signing attempt (starting at 1). At stage :sign: :not_final_step,
:cross_origin_redirect, :invalid_options, :invalid_request,
:unsupported_component, :unsupported_delivery, :body_unavailable,
:digest_conflict, :limit, and :signing_failed, with the meanings of
RequestSeal.Finch.sign/4 and the checks above. At stage :verify:
:limit (more than max_stream_bytes), :invalid_request (a response
RequestSeal.Message.new/1 rejects), and :response_rejected (the core
verifier rejected, with its bounded source, or an unexpected exception).
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")],
...> expires_in: 60
...> }
iex> request =
...> Req.new(url: "https://crawler.example.com/pages", finch: [name: Example.Finch], retry: false)
iex> {:ok, attached} = RequestSeal.Req.attach(request, sign: spec, signer: handle, verify: :none)
iex> {attached.request_steps |> List.first() |> elem(0), attached.request_steps |> List.last() |> elem(0)}
{:request_seal_options, :request_seal_sign}
iex> {:error, error} = RequestSeal.Req.attach(attached, sign: spec, signer: handle, verify: :none)
iex> {error.reason, error.stage, error.attempt}
{:invalid_options, :attach, 0}
iex> {:error, error} =
...> RequestSeal.Req.attach(%{request | into: :self}, sign: spec, signer: handle, verify: :none)
iex> error.reason
:unsupported_delivery
iex> RequestSeal.Custody.Local.release(handle)
:okFragment (requires a running Finch pool and an HTTPS peer that signs its responses; not executed here):
{:ok, _pid} = Finch.start_link(name: Example.Finch)
request =
Req.new(
method: :post,
url: "https://telemetry.example.com/ingest",
body: ~s({"cpu":0.42}),
finch: [name: Example.Finch]
)
{:ok, request} =
RequestSeal.Req.attach(request,
sign: spec,
signer: handle,
verify: %{policy: response_policy, label: "res", max_stream_bytes: 1_048_576}
)
{:ok, response} = Req.request(request)
{:ok, verification} = RequestSeal.Req.verification(response)
@spec verification(Req.Response.t()) :: {:ok, RequestSeal.Verification.t()} | :error
Returns the verification stored in a response's private metadata by an attached request.
Only a response that passed the verification step of attach/2 carries a
result. A rejected response is never returned by Req.request/1, so it has
no partial facts to read.
Parameters
response- aReq.Response, usually fromReq.request/1on a request attached with a verification map.
Returns
{:ok, %RequestSeal.Verification{}}- the stored verified facts.:error- no verification is stored: any other response, including responses from requests attached withverify: :none, or a value that is not aReq.Response.
Examples
iex> RequestSeal.Req.verification(Req.Response.new(status: 200))
:errorFragment (requires a running Finch pool and an HTTPS peer that signs its responses; not executed here):
{:ok, response} = Req.request(attached_request)
{:ok, verification} = RequestSeal.Req.verification(response)
verification.label
#=> "res"