# `AttestoPhoenix.ClientIdMetadata.Resolver`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/client_id_metadata/resolver.ex#L1)

Resolves a Client ID Metadata Document URL into a client - CIMD
(`draft-ietf-oauth-client-id-metadata-document-01`, IETF OAuth WG).

CIMD lets a client identify itself with no prior registration by using an
HTTPS URL as its `client_id`; the authorization server dereferences that URL
to a JSON client metadata document and uses it as the client. This module is
the orchestrator that turns a presented CIMD `client_id` into the normalized
client map attesto's resolution expects, gluing together the four collaborators
the rest of the feature provides:

  * `Attesto.ClientIdMetadata` - the pure, network-free URL grammar
    (`validate_client_id/1`) and document validation (`validate_document/2`);
  * `AttestoPhoenix.ClientIdMetadata.Cache` - remembers a *validated* document
    so not every authorization request reaches the network;
  * `AttestoPhoenix.ClientIdMetadata.Fetcher` - the single SSRF-guarded
    outbound `GET`;
  * `AttestoPhoenix.Config` - the host's `:client_id_metadata` options
    (fetcher, cache, size/timeout caps, cache-TTL bounds, host allow/block
    lists).

## Algorithm (`resolve/2`)

Each step errors closed; later steps run only when every earlier one passed:

  1. **Grammar (fail fast, no network).** `validate_client_id/1` rejects a
     non-CIMD `client_id` before any host check or socket - a request whose
     `client_id` is not a well-formed CIMD URL never reaches the resolver in
     normal operation, but the check is repeated here so a direct caller is
     never trusted. Failure -> `{:error, {:invalid_client_id, reason}}`.
  2. **Host policy.** The URL's host is screened against the configured
     `:blocked_hosts` (always refused) and, when set, `:allowed_hosts` (only
     these are permitted). A blocked or non-allowlisted host is refused
     *before* the cache or the network is consulted, so a policy change takes
     effect immediately. Failure -> `{:error, {:blocked_host, host}}`.
  3. **Cache.** `Cache.get/1` is consulted; a live (unexpired) entry is
     returned as the client without any fetch. The cache only ever holds a
     previously validated document, so a hit needs no re-validation.
  4. **Fetch.** On a miss the configured `:fetcher` performs the SSRF-guarded
     `GET`, honoring `:max_document_bytes`, `:request_timeout_ms`, and
     `:allow_loopback`. Any transport failure, non-`200`, redirect, non-JSON
     content type, or oversize body is an `{:error, _}` and is **never**
     cached (draft §6 / RFC 9111).
  5. **Decode + validate.** The body is JSON-decoded and handed to
     `validate_document/2`, which enforces the `client_id` match and the
     no-symmetric-secret rules and normalizes the document into the client
     shape. A malformed JSON body or an invalid document is an `{:error, _}`
     and is **never** cached.
  6. **Cache + return.** Only after validation succeeds is the document stored
     via `Cache.put/3`, with an `expires_at` derived from the response's
     `Cache-Control: max-age` / `Expires` freshness directives clamped to the
     configured `:cache_ttl_bounds` (RFC 9111). The normalized client map is
     returned.

The returned client is shaped identically to a host `:load_client` result, so
downstream resolution (scopes, redirect-URI match, JARM, DPoP) needs no
CIMD-specific handling.

# `error`

```elixir
@type error() ::
  {:invalid_client_id, Attesto.ClientIdMetadata.url_error()}
  | {:blocked_host, String.t()}
  | {:fetch, term()}
  | :invalid_json
  | Attesto.ClientIdMetadata.document_error()
```

A reason `resolve/2` refused to produce a client. `:invalid_client_id` and
`:blocked_host` are local policy failures (no network); `{:fetch, reason}`
wraps a fetcher error; `:invalid_json` is an undecodable body; and a bare
`t:Attesto.ClientIdMetadata.document_error/0` is a validation failure.

# `resolve`

```elixir
@spec resolve(String.t(), AttestoPhoenix.Config.t()) ::
  {:ok, map()} | {:error, error()}
```

Resolve a CIMD `client_id` URL into a normalized client map.

Runs the algorithm documented on this module against the host's
`:client_id_metadata` configuration in `config`. Returns `{:ok, client}` for a
freshly fetched-and-validated document (now cached) or a live cache hit, or
`{:error, reason}` for any local-policy, fetch, decode, or validation failure -
none of which are ever cached.

---

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