# `RequestSeal.Quorum`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/quorum.ex#L1)

Explicit composite RFC 9421 verification policy.

`new/1` builds the policy that `RequestSeal.verify_quorum/3` evaluates:
a mode, a counting unit, 1 to 16 signer slots (each with its own
`RequestSeal.Policy`), and the handling of unexpected and invalid labels.
Labels never count. Every mode requires all configured required slots and
at least one qualifying signature. `:all` requires at least one required
slot; `:any` requires one eligible signer; thresholds count distinct
qualifying units. Every signature must meet its own slot's complete policy.
Each slot receives at most one signature; each label and each identity
class occupies at most one slot across every counting unit. Observations of
one label merge all its known identities and any unknown observation into
one class. Unknown observations share a pool. A label is known if any
eligible verification supplies a known identity. Required slots must be
filled in every mode; an unfilled bound slot fails its binding. No coverage
union is performed.

`verify_quorum/3` requires `replay: :not_required` in every slot policy.
A required replay policy returns `:invalid_policy`, layer `:input`, before
any callback or store claim, including in optional or unmatched slots.
Composite replay requires one claim after the complete quorum, bindings,
and negotiation pass; per-label claims can consume nonqualifying candidates.
Required replay is rejected until that composite replay design exists.

Slot content checks use the message's retained body only. Quorum
verification accepts no `:representation` or `:digest_state` option;
streamed or unavailable content cannot qualify when a digest is required.

Assignment maximizes counting units, then filled slots, in canonical slot-id
and candidate-label order, preferring challenge labels. Declaration and wire
order cannot change the chosen set; output lists retain configured slot order.
Within the configured input limits and with existing covered bytes preserved,
adding an unrelated optional signature preserves a feasible quorum except:

* `unexpected: :reject` rejects a new label with no selector-eligible slot.
* `invalid: :reject` rejects a new eligible label that fails every slot policy.
* A newly verified label spanning identity classes merges them and can reduce
  the count or reject the quorum. This includes a label observed as unknown
  under one slot and known under another: it bridges the shared unknown pool
  into the known class, reducing capacity for unknown-only labels as well.

An unfillable required slot returns `:quorum_not_met` before binding checks,
even when a binding also fails. Once required slots can be filled, an unfillable
bound slot or inconsistent binding returns `:binding_unsatisfied`. After a valid
assignment is found, an unmet mode or threshold returns `:quorum_not_met`.

Principal and role counting bindings establish no attribution. Key
identities stay internal. Unknown key equivalence cannot count in key-unit
quorums; optional unknown identities are ignored, while a required slot with
only unknown candidates rejects with `:ambiguous_key_identity`.
Principal/role policies conservatively assign the shared unknown pool to at
most one slot.

Bindings search the complete eligible candidate pool, including alternatives
with the same key. Nested bindings require both inner dictionary members;
same-parameter bindings require equal, non-nil values. Negotiation searches
the verified, selector-eligible pool after key-unit filtering. A challenge
label can fulfill negotiation without counting as a unit; it appears in
`qualifying` and `signatures` only when assigned. A tampered challenge fails.

Verification performs at most 64 × 16 label-slot attempts. Without bindings,
key, principal, and shared-principal role units use polynomial matching.
Other assignments use exact search with a 131,072-node budget. On exhaustion,
a best-so-far assignment is returned only if it satisfies the mode, required
slots, and bindings; otherwise the result is `:limit`, layer `:input`.
Selector-free bound slots populated by trusted signers can exhaust that budget;
the bound prevents unbounded search and never produces partial success.

# `t`

```elixir
@type t() :: %RequestSeal.Quorum{
  bindings: [term()],
  invalid: :reject | :ignore,
  max_signatures: 1..64,
  mode: :all | :any | {:threshold, 1..64},
  slots: [map()],
  unexpected: :reject | :ignore,
  unit: :key | :principal | {:role, keyword()}
}
```

# `new`

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

Construct a bounded composite policy without invoking a callback.

## Parameters

  * `attrs` - map with all five required keys and any of the two optional
    keys. Unknown keys reject.
    * `:mode` - `:all`, `:any`, or `{:threshold, n}` with `n` from 1 to
      `:max_signatures`. `:all` requires at least one required slot.
    * `:unit` - `:key`, `:principal`, or `{:role, shared_principal: boolean}`.
    * `:slots` - list of 1 to 16 slot maps, described below.
    * `:unexpected` - `:reject` or `:ignore`, for a label with no
      selector-eligible slot.
    * `:invalid` - `:reject` or `:ignore`, for an eligible label that fails
      every slot policy.
    * `:bindings` - optional, default `[]`, at most 64 unique entries:
      `{:nested, outer: id, inner: id}` requires the outer to cover both the
      inner's actual `"signature-input";key="<inner-label>"` and
      `"signature";key="<inner-label>"` dictionary members;
      `{:same_parameter, name, ids}` (`name` is `"nonce"`, `"tag"`, or
      `"created"`; 1 to 16 unique slot ids) requires identical, present
      values across the selected slots. Bindings must reference configured
      slots and cannot bind a nested signature to itself.
    * `:max_signatures` - optional, default 16 (1 to 64); thresholds cannot
      exceed this bound.

Each slot requires a unique atom `:id` (not `nil`, `true`, or `false`), a
valid `RequestSeal.Policy` as `:policy`, and Boolean `:required`. Optional
`:label` (a valid signature label) and `:tag` (1 to 256 bytes) selectors
default to nil (any); `:principal` (1 to 256 bytes) and `:role` (an atom)
default to nil. Principal counting requires a principal in every slot. Role
counting additionally requires a role in every slot. With
`shared_principal: false`, one principal counts toward only one role.
Slot policy `:content` must be `:not_required` or a Content-Digest policy
(`kind: :content`); representation digest policies reject, including in
optional or unmatched slots.

## Returns

  * `{:ok, %RequestSeal.Quorum{}}` - slots normalized with nil `:label`,
    `:tag`, `:principal`, and `:role` defaults. Inspection shows only
    `mode`, `unit`, and `max_signatures`.
  * `{:error, %RequestSeal.Error{}}` - always reason `:invalid_quorum`,
    layer `:input`, for unknown units, malformed choices, duplicate or
    invalid bindings, or a non-map argument. Meanings: `RequestSeal.Error`.

## Examples

    iex> {public, _secret} = :crypto.generate_key(:eddsa, :ed25519)
    iex> {:ok, key} = RequestSeal.PublicKey.import({:ed25519, public}, :raw)
    iex> {:ok, policy} =
    ...>   RequestSeal.Policy.new(%{
    ...>     algorithms: ["ed25519"],
    ...>     components: ~s[("@method" "@path")],
    ...>     key_resolver: fn _ -> {:ok, %{algorithm: "ed25519", key: key}} end,
    ...>     freshness: %{clock: fn -> System.system_time(:second) end, max_age: 300, skew: 5, require_expires: false},
    ...>     content: :not_required,
    ...>     replay: :not_required
    ...>   })
    iex> slots = [
    ...>   %{id: :author, policy: policy, required: true, label: "author", principal: "writer@example.org", role: :author},
    ...>   %{id: :editor, policy: policy, required: true, label: "editor", principal: "editor@example.org", role: :editor}
    ...> ]
    iex> {:ok, quorum} =
    ...>   RequestSeal.Quorum.new(%{
    ...>     mode: :all,
    ...>     unit: {:role, shared_principal: false},
    ...>     slots: slots,
    ...>     unexpected: :ignore,
    ...>     invalid: :reject,
    ...>     bindings: [{:nested, outer: :editor, inner: :author}]
    ...>   })
    iex> {quorum.mode, quorum.max_signatures, Enum.map(quorum.slots, & &1.tag)}
    {:all, 16, [nil, nil]}
    iex> {:error, error} =
    ...>   RequestSeal.Quorum.new(%{
    ...>     mode: :all,
    ...>     unit: :key,
    ...>     slots: [%{id: :author, policy: policy, required: false}],
    ...>     unexpected: :ignore,
    ...>     invalid: :ignore
    ...>   })
    iex> {error.reason, error.layer}
    {:invalid_quorum, :input}

---

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