# `RequestSeal.Discovery.Source`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/discovery/source.ex#L1)

Caller-configured discovery trust root, built with `new/1`.

A directory origin expands to `/.well-known/http-message-signatures-directory`.
For CIMD, pass `:location` with exactly the spelling of the document's
`client_id`, including scheme/host case, any trailing host dot and explicit
port. The client ID comparison is exact; origin normalization for fetching
and same-origin checks never changes that comparison.

## Key ID modes

Thumbprint mode (`key_id: :thumbprint`, the default) requires any supplied JWK
`kid` to equal its RFC thumbprint. Directory mode (`key_id: :directory`)
requires a unique `kid` per JWK (1..256 printable ASCII bytes), preserves its
spelling, and keeps thumbprints as the revocation identity.
Directory proof sources (`type: :directory`) require thumbprint mode; their
signed proof identifies its verification key by thumbprint. Duplicate IDs
reject among eligible entries only; expired, not-yet-valid, revoked, or
incompatible entries do not reserve an ID during rotation.

Modified structs are revalidated at every fetch. Inspection excludes
locations, addresses, trust roots and revoked IDs. No networking occurs during
construction. Starting SSL remains the caller's responsibility.

# `t`

```elixir
@type t() :: %RequestSeal.Discovery.Source{
  cacerts: :os | [binary()],
  key_id: :thumbprint | :directory,
  location: binary(),
  max_bytes: pos_integer(),
  max_decoded_bytes: pos_integer(),
  max_keys: pos_integer(),
  max_redirects: non_neg_integer(),
  max_ttl: non_neg_integer(),
  min_ttl: non_neg_integer(),
  negative_ttl: non_neg_integer(),
  permitted_addresses: [tuple()],
  redirect_scope: :same_origin | :any_https,
  require_signed_directory: boolean(),
  revoked: [binary()],
  timeout: pos_integer(),
  type: :directory | :jwks_uri | :cimd
}
```

# `directory_id?`

```elixir
@spec directory_id?(term()) :: boolean()
```

Check whether a value is a valid directory-assigned key ID.

Directory IDs are compared with their exact spelling; this check does not
normalize them.

## Parameters

  * `value` - any term.

## Returns

  * `true` - a binary of 1–256 bytes, each printable ASCII (0x20 through 0x7E).
  * `false` - anything else.

## Examples

    iex> RequestSeal.Discovery.Source.directory_id?("build-signer 2026")
    true
    iex> RequestSeal.Discovery.Source.directory_id?("tab	separated")
    false
    iex> RequestSeal.Discovery.Source.directory_id?("")
    false

# `new`

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

Construct a bounded explicit source from trusted caller configuration.

## Parameters

  * `attrs` - a plain map (not a struct) with the required keys `:type` and
    `:location` plus any options below. Unknown keys reject.
    * `:type` - `:directory`, `:jwks_uri` or `:cimd`.
    * `:location` - an `https` URL of 1–2,048 printable ASCII bytes with a host,
      without userinfo, fragment, backslash, malformed percent-escape or dot
      segment. A `:directory` location is either an origin (expanded to the
      well-known path) or exactly that path, without a query. A `:cimd`
      location needs a nonempty path.

## Options

  * `:key_id` - `:thumbprint` (default) or `:directory`; see "Key ID modes"
    above. A `:directory` type requires `:thumbprint`.
  * `:cacerts` - `:os` (default) or a nonempty list of DER certificates, each
    1–16,384 bytes.
  * `:permitted_addresses` - at most 16 exact IPv4 or IPv6 address tuples that
    are allowed even though they are private or special-purpose. Default `[]`.
  * `:revoked` - at most 256 RFC thumbprints. Default `[]`.
  * `:require_signed_directory` - boolean, default `true`.
  * `:max_redirects` - 0 (default) through 3.
  * `:redirect_scope` - `:same_origin` (default) or explicit `:any_https`.
  * `:max_bytes` - response body bytes before content decoding, 1 through
    1,048,576; default 65,536.
  * `:max_decoded_bytes` - response body bytes after content decoding, same
    bounds and default.
    CIMD response content additionally has a 5,120-byte cap.
  * `:max_keys` - 1 through 256; default 32.
  * `:timeout` - milliseconds, 1 through 300,000; default 5,000.
  * `:min_ttl` - seconds, 0 through 604,800 and at most `:max_ttl`; default 300.
    It supplies fallback freshness only when neither `max-age` nor `Expires` is
    present; it never extends explicit freshness or a key/signature expiry.
  * `:max_ttl` - seconds, 0 through 604,800; default 86,400.
  * `:negative_ttl` - seconds, 0 through 300; default 30.

## Returns

  * `{:ok, %RequestSeal.Discovery.Source{}}` - the validated source, with a
    directory origin expanded to the well-known path.
  * `{:error, %RequestSeal.Discovery.Error{}}` - reason `:invalid_options` for an
    invalid `:negative_ttl`, otherwise `:invalid_source`. Meanings:
    `RequestSeal.Discovery.Error`.

## Examples

    iex> {:ok, source} =
    ...>   RequestSeal.Discovery.Source.new(%{type: :directory, location: "https://signer.example"})
    iex> source.location
    "https://signer.example/.well-known/http-message-signatures-directory"
    iex> RequestSeal.Discovery.Source.new(%{type: :jwks_uri, location: "http://keys.example/jwks.json"})
    {:error, %RequestSeal.Discovery.Error{reason: :invalid_source, retryable: false}}
    iex> RequestSeal.Discovery.Source.new(%{
    ...>   type: :jwks_uri,
    ...>   location: "https://keys.example/jwks.json",
    ...>   negative_ttl: 301
    ...> })
    {:error, %RequestSeal.Discovery.Error{reason: :invalid_options, retryable: false}}

---

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