# `RequestSeal.WebBotAuth.Policy`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/web_bot_auth/policy.ex#L1)

Explicit policy for `draft-ietf-webbotauth-httpsig-protocol-00`.

`new/1` validates every choice without invoking trust, clock, or storage.
Content and replay use the generic `RequestSeal.Policy` contracts; HMAC
algorithms are forbidden. The `:agents` trust function selects
caller-configured trust, never an arbitrary fetch URL. Attribution requires
this trusted association. Held-key verification attributes only the
thumbprint, never an unresolved agent URL. Test keys default to rejection
under Section 6.8.

Within the caller's chosen replay namespace, commitments should hash each
signature's nonce, keyid, and agent identifier only (`identifier`, `keyid`, and
`agent.identifier` in the supplied facts; held keys have no agent identifier).
Replay facts retain the agent's stable `kind`, `identifier`, `type`, `origin`,
and `thumbprint` fields. They exclude provenance and cache-refresh fields such as
`fetched_at` and `revision`, so hashing the supplied facts is stable across directory
refreshes. Full provenance remains available in the verification result.
Replay scope remains caller-owned,
as described in [RFC 9421 Section 7.2.2](https://www.rfc-editor.org/rfc/rfc9421.html#section-7.2.2).
Inspection omits callback environments and source configuration.

# `agents`

```elixir
@type agents() :: (map() -&gt;
               {:ok,
                RequestSeal.Discovery.Source.t()
                | RequestSeal.Discovery.KeySet.t()}
               | :error)
```

# `signer`

```elixir
@type signer() :: RequestSeal.Policy.signer()
```

# `t`

```elixir
@type t() :: %RequestSeal.WebBotAuth.Policy{
  agents: agents(),
  algorithms: list(),
  cache: pid() | nil,
  components: binary(),
  content: term(),
  field_schemas: map(),
  freshness: map(),
  max_lifetime: pos_integer(),
  max_signatures: pos_integer(),
  replay: term(),
  test_keys: :reject | :allow,
  unresolved: term(),
  untagged: :ignore | :reject
}
```

# `new`

```elixir
@spec new(map()) :: {:ok, t()} | {:error, RequestSeal.Error.t()}
```

Construct an explicit policy without invoking trust, clock, or storage.

## Parameters

  * `attrs` - a plain map (not a struct) with the required keys below and any
    of the optional keys. Unknown keys reject.

Required keys:

  * `:algorithms` - nonempty unique list of `RequestSeal.Crypto` HTTP tokens or
    `{:jws, name}` selections, as in `RequestSeal.Policy`; `"hmac-sha256"` and
    `{:jws, "HS256"}` reject.
  * `:agents` - arity-one trust function receiving
    `%{identifier: id, location: url, type: type}`, where `type` is
    `:directory`, `:jwks_uri`, or `:cimd`, `location` is the received member
    URL, and `identifier` is the directory URL
    (`origin/.well-known/http-message-signatures-directory`) or the normalized
    location. It returns `{:ok, %RequestSeal.Discovery.Source{}}`,
    `{:ok, %RequestSeal.Discovery.KeySet{}}`, or `:error`. A Source requires a
    caller-started `:cache`. A KeySet must be fresh and associated with the
    same URL and type. Sources must allow no redirects.
  * `:cache` - a `RequestSeal.Discovery.Cache` PID, or nil.
  * `:freshness` - map with `:clock` (arity zero, integer Unix seconds),
    `:skew` (integer 0 to 86,400 seconds), and `:max_age` (nil or integer 1 to
    253,402,300,799 seconds). An optional `:require_expires` key is accepted,
    but expiration is always required.
  * `:content` - `:not_required` or a `RequestSeal.Policy` content map.
  * `:replay` - `:not_required` or a `RequestSeal.Policy` replay map.

## Options

  * `:components` - serialized Inner List of extra required component
    identifiers, without Inner List parameters. Default `"()"`.
  * `:max_lifetime` - integer seconds, 1 to 604,800. Default 86,400.
  * `:unresolved` - `:reject` (default) or `{:held_keys, key_resolver}`. The
    arity-one resolver receives `%{label: label, keyid: keyid, tag: tag}` and
    returns `{:ok, %{algorithm: algorithm, key: %RequestSeal.PublicKey{}}}` or
    `:error`; the algorithm must be in `:algorithms` and the key's thumbprint
    must equal `keyid`. It applies when no trusted association resolves,
    except for revoked keys, source mismatches, and non-origin directory
    locations. It waives the draft's `Signature-Agent` presence rule only when
    the field is absent.
  * `:test_keys` - `:reject` (default) or `:allow`.
  * `:untagged` - `:ignore` (default; listed in `ignored`) or `:reject`.
  * `:max_signatures` - integer 1 to 64. Default 16.
  * `:field_schemas` - Structured Fields schemas, as in `RequestSeal.Policy`.
    Default `%{}`.

## Returns

  * `{:ok, %RequestSeal.WebBotAuth.Policy{}}`.
  * `{:error, %RequestSeal.Error{}}` - always reason `:invalid_policy`, layer
    `:input`. Meanings: `RequestSeal.Error`.

## Examples

    iex> attrs = %{algorithms: ["ed25519"], agents: fn _ -> :error end, cache: nil, freshness: %{clock: fn -> 1_760_000_000 end, skew: 5, max_age: 60}, content: :not_required, replay: :not_required}
    iex> {:ok, policy} = RequestSeal.WebBotAuth.Policy.new(attrs)
    iex> {policy.max_lifetime, policy.unresolved, policy.test_keys, policy.untagged}
    {86400, :reject, :reject, :ignore}
    iex> {:error, error} = RequestSeal.WebBotAuth.Policy.new(%{attrs | algorithms: ["hmac-sha256"]})
    iex> error.reason
    :invalid_policy
    iex> {:error, error} = RequestSeal.WebBotAuth.Policy.new(Map.put(attrs, :max_lifetime, 604_801))
    iex> error.reason
    :invalid_policy

---

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