# `Snodo.Authorization`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.2.0/lib/snodo/authorization.ex#L1)

Optional application-owned authorization at the component boundary.

The application decides whether a request context may discover or invoke a
registered component. `snodo` applies that decision inside the router, below
every transport and protocol dialect, and before argument validation or any
application callback. The library supplies no identity, role, token, or
policy vocabulary and never interprets the returned error.

Configure a runtime with `authorization: MyApp.Policy` or
`authorization: {MyApp.Policy, options}`; the second element is passed back
unchanged on every call. An unconfigured runtime performs no extra work.

    defmodule MyApp.Policy do
      @behaviour Snodo.Authorization

      alias Snodo.Authorization.Component

      @impl true
      def authorize(phase, %Component{} = component, context, _options) do
        if allowed?(context.auth, component) do
          :ok
        else
          if phase == :invocation, do: MyApp.Audit.refused(context.auth, component)
          {:error, Snodo.Error.authorization(-32_003, "Not authorized")}
        end
      end
    end

## Phases

`:discovery` covers `tools/list`, `prompts/list`, `resources/list`, and
`resources/templates/list`. A refusal removes the component from that
response, so the client never learns the name exists.

`:invocation` covers `tools/call`, `prompts/get`, `resources/read`, and
`completion/complete`. A refusal is returned to the client as the
application's own `Snodo.Error`, which keeps a boundary violation distinct from
an unknown name and gives the policy the one place to record an audit event.
`Snodo.Context.request_method` names the exact operation being refused.

The policy runs once per listed component during discovery and once per
invocation, so keep it allocation-light: pass a precomputed catalog through
the options rather than querying a store inside the callback.

Only the enforcement seam lives here. Server capability advertisement stays
catalog-wide, because it describes the server rather than one request.
Application-owned subscription sources and negotiated extension routes
receive the same `Snodo.Context` and own their policy, because neither
dispatches through the router catalog.

A policy that raises, exits, throws, or returns something else is a fault
rather than a decision: the operation fails with an internal error in both
phases instead of silently emptying a catalog.

# `config`

```elixir
@type config() :: nil | {module(), term()}
```

# `decision`

```elixir
@type decision() :: :ok | {:refused, Snodo.Error.t()} | {:fault, Snodo.Error.t()}
```

# `phase`

```elixir
@type phase() :: :discovery | :invocation
```

# `authorize`

```elixir
@callback authorize(phase(), Snodo.Authorization.Component.t(), Snodo.Context.t(), term()) ::
  :ok | {:error, Snodo.Error.t()}
```

Decides whether `context` may discover or invoke `component`.

Return `:ok` to allow, or `{:error, %Snodo.Error{}}` to refuse. The error is
returned verbatim during `:invocation` and only hides the component during
`:discovery`.

---

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