# `PhoenixKit.API.Approvals`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/approvals.ex#L1)

The approval queue of API write operations (the approval path of the confirmation flow):
every write call of a key marked "requires approval", dangerous or not, waits here for a
person. A read or a `dry_run` of such a key is never held.

## Life of an approval

    pending -> approved -> executing -> executed | failed
    pending -> rejected | stale | expired

`request/2` creates the `pending` row (the danger gate calls it with the preview the
handler produced in dry-run mode). An agent follows its approval with
`system.get_approval` (`get_for_key/2` and `status_view/1`).

## Rules

  * An identical pending request (same key, tool, arguments) returns the existing
    approval.
  * A key holds at most `api_approval_pending_max` pending approvals; one more is
    `{:error, :pending_cap}`. Both rules run under a per-key advisory lock.
  * `input` is the arguments the approval will run with; at most 1 MB; cleared when
    the approval is decided or expires.
  * Who may decide: an Owner, or a person who holds `api`, created the key and holds
    the tool's `permission` (`may_decide?/2`).
  * An approved operation is run once: only an `approved` row starts, and it moves to
    `executing` first. If it runs for more than 15 minutes without calling `ctx.progress`,
    the stuck-operation sweep marks it `failed` (`interrupted`) although the process is
    still working; when the handler then succeeds, the row is corrected to `executed`
    (with a `note`). Until then, and if the process really died, the row stays `failed`
    and part of the operation may have been applied: an agent must check the data and
    not queue the operation again before it has.

Changes are announced on the PubSub topic `"phoenix_kit:api:approvals"` as
`{:api_approval_changed, approval_uuid}`, which the approvals page listens to.

# `approve`

```elixir
@spec approve(term(), PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) ::
  {:ok, PhoenixKit.API.Confirm.Confirmation.t()}
  | {:error, :not_found | :forbidden | :not_pending | :expired}
```

Approves `approval_id` and runs it now, in the calling process: the decision, the checks
of the request path repeated against the key as it stands, the values fingerprint, then
the operation. On `{:ok, row}` the row is final: `executed`, `failed` or `stale`.

# `deciders`

```elixir
@spec deciders(PhoenixKit.API.Confirm.Confirmation.t()) :: [
  PhoenixKit.Users.Auth.User.t()
]
```

The active people who may decide `approval`: the Owners and the key's creator, if they qualify.

# `get_for_key`

```elixir
@spec get_for_key(term(), term()) :: PhoenixKit.API.Confirm.Confirmation.t() | nil
```

The approval `approval_id` if it belongs to the key `key_uuid`, else `nil` (an unknown
id, a malformed id, another key's approval and a confirmation code all read the same).
A pending approval past its time reads as `expired`.

# `in_use?`

```elixir
@spec in_use?() :: boolean()
```

Whether the site uses approvals at all: a key that still works (`Keys.dead_reason/1` is
`nil`: a revocation or expiry in the future has not been reached) is marked "requires
approval", or an approval of any status is still kept (a pending one outlives the flag
being switched off, and the decided ones are the page's history). The admin menu offers
the Approvals entry only then; the page itself answers either way.

# `list_pending`

```elixir
@spec list_pending(PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) :: [
  %{
    approval: PhoenixKit.API.Confirm.Confirmation.t(),
    key: PhoenixKit.API.Keys.Key.t()
  }
]
```

The pending approvals `scope` may decide, oldest first, with their keys.

# `list_recent`

```elixir
@spec list_recent(PhoenixKit.Users.Auth.Scope.t(), pos_integer()) :: [
  %{
    approval: PhoenixKit.API.Confirm.Confirmation.t(),
    key: PhoenixKit.API.Keys.Key.t()
  }
]
```

The latest decided approvals `scope` may see, newest change first.

# `may_decide?`

```elixir
@spec may_decide?(
  PhoenixKit.Users.Auth.Scope.t(),
  PhoenixKit.API.Confirm.Confirmation.t()
) :: boolean()
```

Whether the person behind `scope` may approve or reject `approval`: an Owner, or a
holder of `api` who created the key and holds the tool's `permission`.

# `pending_count`

```elixir
@spec pending_count(PhoenixKit.Users.Auth.Scope.t()) :: non_neg_integer()
```

How many pending approvals `scope` may decide: what `list_pending/2` would list, counted
without reading the rows (the menu draws it on every render). An Owner's is one count;
anyone else's is a count per tool of the approvals of their keys, kept for the tools they
hold the permission of.

# `reject`

```elixir
@spec reject(term(), PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) ::
  {:ok, PhoenixKit.API.Confirm.Confirmation.t()}
  | {:error, :not_found | :forbidden | :not_pending | :expired}
```

Rejects `approval_id`: nothing runs and its input is discarded.

# `request`

```elixir
@spec request(map(), DateTime.t()) ::
  {:ok, PhoenixKit.API.Confirm.Confirmation.t(), :created | :existing}
  | {:error, :pending_cap | :input_too_large | Ecto.Changeset.t()}
```

Queues an approval. `args` is `%{actor, tool, input, input_hash, preview, fingerprint,
context}`: the key's actor, the tool name, the validated arguments without the control
fields, their canonical hash, the preview and fingerprint of the dry run, and the
request context (`language currency hosts base_url request_id`) the operation will
run with.

# `status_view`

```elixir
@spec status_view(PhoenixKit.API.Confirm.Confirmation.t()) :: map()
```

The JSON-able view `system.get_approval` answers: id, status, tool, preview, times,
and by status the result, or the error and the partial results. Never the input.

# `topic`

```elixir
@spec topic() :: String.t()
```

The PubSub topic of approval changes.

---

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