# `AttestoPhoenix.Event`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/event.ex#L1)

Neutral event struct and dispatcher for the optional `:on_event` callback.

An OAuth 2.0 / OIDC authorization server takes many decisions that an
operator may wish to record: a token was issued, a token request was denied,
a token was revoked (RFC 7009), a refresh token was rotated (RFC 6749 §6),
presented refresh-token reuse was detected (RFC 6819 §5.2.2.3), a bearer
token authenticated a request (RFC 6750), a request was rejected, or a client
was registered (RFC 7591). *Recording* those decisions (to a log, a database,
a SIEM) is host policy, not a concern of this library. This module therefore
does two things and nothing more:

  1. Defines a closed set of event names and a generic payload struct that
     carries only OAuth/OIDC vocabulary (subject, client id, scope, grant
     type, result, request metadata).
  2. Dispatches each event to the host's optional `:on_event` callback read
     from `AttestoPhoenix.Config`. When the callback is unset the dispatch is
     a no-op: the library emits, the host stores.

The dispatcher never raises on a missing callback (emission is optional),
never inspects or persists the event itself, and discards the callback's
return value so a storage decision can never alter the authorization-server
control flow that emitted the event.

## Configuration

The callback is the `:on_event` field of `AttestoPhoenix.Config`. It accepts
any of the `t:AttestoPhoenix.Config.callback/0` forms - an anonymous
function, a `{module, function}` pair, or a full `{module, function, args}`
tuple - and is invoked with the `%AttestoPhoenix.Event{}` struct. For the
`{module, function, args}` form the event is prepended to `args`:

    config :my_app, AttestoPhoenix,
      on_event: &MyApp.OAuth.handle_event/1

    config :my_app, AttestoPhoenix,
      on_event: {MyApp.OAuth, :handle_event}

    config :my_app, AttestoPhoenix,
      on_event: {MyApp.OAuth, :handle_event, [extra_context]}

# `name`

```elixir
@type name() ::
  :token_issued
  | :token_denied
  | :code_issued
  | :authorization_denied
  | :authorization_failed
  | :token_revoked
  | :refresh_issued
  | :refresh_rotated
  | :refresh_reuse_detected
  | :auth_succeeded
  | :auth_denied
  | :client_registered
```

The closed set of authorization-server lifecycle events.

* `:token_issued` - an access (and optionally refresh) token was issued in
  response to a successful grant (RFC 6749 §5.1). Its `:metadata` includes
  `:token_type`, `:sender_constraint`, and `:cnf` so a host can audit whether
  the issued access token is plain Bearer, DPoP-bound, or mTLS-bound.
* `:token_denied` - a token request was rejected (RFC 6749 §5.2). Its
  `:metadata` includes `:client_id` when known, `:reason` as the structured
  OAuth error atom, and the sender-constraint audit fields `:token_type`,
  `:sender_constraint`, and `:cnf`.
* `:code_issued` - an authorization code was issued at the authorization
  endpoint in response to a successful authorization request (RFC 6749
  §4.1.2).
* `:authorization_denied` - the resource owner refused an authorization
  request, reported to the client as `access_denied` (RFC 6749 §4.1.2.1).
* `:authorization_failed` - an authorization request was rejected before a
  code was issued (RFC 6749 §4.1.2.1), whether reported as a direct error
  page or by redirect.
* `:token_revoked` - a previously issued token was revoked (RFC 7009).
* `:refresh_issued` - an initial refresh token was issued alongside an
  access token at the token endpoint (RFC 6749 §5.1, §6). Distinct from
  `:refresh_rotated`: no predecessor was consumed, this is the first token
  in a new rotation family.
* `:refresh_rotated` - a refresh token was exchanged and a new refresh token
  issued, invalidating the presented one (RFC 6749 §6, RFC 6819 §5.2.2.3).
* `:refresh_reuse_detected` - an already-rotated refresh token was presented
  again, indicating possible theft (RFC 6819 §5.2.2.3).
* `:auth_succeeded` - a presented access token authenticated a protected
  resource request (RFC 6750 §2.1).
* `:auth_denied` - a protected resource request was rejected (RFC 6750 §3.1).
* `:client_registered` - a client was registered (RFC 7591).

# `t`

```elixir
@type t() :: %AttestoPhoenix.Event{
  client_id: String.t() | nil,
  grant_type: String.t() | nil,
  metadata: map(),
  name: name(),
  result: term() | nil,
  scope: String.t() | nil,
  subject: String.t() | nil
}
```

A neutral authorization-server event.

Every field is optional because the populated subset depends on the event:
a `:token_denied` before client authentication has no `:subject`, a
`client_credentials` grant has no resource-owner `:subject`, and so on. The
library never fabricates a value it does not have.

* `:name` - the event name (one of `t:name/0`).
* `:subject` - the resource owner identifier, the `sub` claim (RFC 7519
  §4.1.2) when one is present.
* `:client_id` - the OAuth client identifier (RFC 6749 §2.2).
* `:scope` - the granted or requested scope (RFC 6749 §3.3).
* `:grant_type` - the grant type of the request (RFC 6749 §1.3).
* `:result` - for denial events, a machine-readable reason term (typically an
  RFC 6749 §5.2 error code such as `:invalid_client` or `:invalid_grant`).
* `:metadata` - a host-opaque map of request metadata (for example client IP
  or request identifiers). For token issuance-like events (`:token_issued`,
  `:refresh_issued`, and `:refresh_rotated`) and token denials
  (`:token_denied`), this map also carries sender-constraint audit fields:
  `:token_type` (`"Bearer"` or `"DPoP"`), `:sender_constraint` (`:none`,
  `:dpop`, or `:mtls`), and `:cnf` (`%{"jkt" => thumbprint}`,
  `%{"x5t#S256" => thumbprint}`, or `nil`). Denials additionally carry
  `:reason` as the OAuth error atom and `:client_id` when known.

# `dispatch`

```elixir
@spec dispatch(AttestoPhoenix.Config.callback() | nil, t()) :: :ok
```

Dispatches a pre-built event struct to a resolved `:on_event` callback.

`callback` is any `t:AttestoPhoenix.Config.callback/0` form, or `nil` for the
unconfigured case (a no-op returning `:ok`). Exposed for callers that already
hold a built struct and the resolved callback.

# `emit`

```elixir
@spec emit(AttestoPhoenix.Config.t(), name(), map() | keyword()) :: :ok
```

Emits an event to the host's `:on_event` callback, if one is configured.

`config` is the `AttestoPhoenix.Config` for the request; the callback is read
from its `:on_event` field. `name` plus `fields` are passed to `new/2` (so an
unrecognized name raises). When `:on_event` is unset this is a no-op that
returns `:ok`.

Emission is observational: the callback's return value is discarded and `:ok`
is always returned, so a host's storage decision can never alter the
authorization-server control flow that emitted the event.

# `names`

```elixir
@spec names() :: [name(), ...]
```

Returns the closed set of recognized event names.

# `new`

```elixir
@spec new(name(), map() | keyword()) :: t()
```

Builds an event struct for `name` from a payload of OAuth/OIDC fields.

`name` must be a recognized event name (`t:name/0`); an unrecognized name
raises `ArgumentError` so a typo fails closed instead of silently emitting an
uninterpretable event. `fields` is a map or keyword list whose recognized
keys (`:subject`, `:client_id`, `:scope`, `:grant_type`, `:result`,
`:metadata`) populate the struct. An unknown key raises `KeyError` via
`struct!/2` rather than being silently dropped.

## Examples

    iex> AttestoPhoenix.Event.new(:token_issued, client_id: "abc", scope: "openid")
    %AttestoPhoenix.Event{
      name: :token_issued,
      client_id: "abc",
      scope: "openid",
      metadata: %{}
    }

---

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