# `Sigra.APIToken`
[🔗](https://github.com/sztheory/sigra/blob/v1.20.0/lib/sigra/api_token.ex#L1)

Core API token operations: creation, verification, revocation, and scope checks.

API tokens (also called personal access tokens or secret keys) allow users to
authenticate API requests without session cookies. Tokens use a prefix format
(e.g., `my_app_sk_...`) for easy identification and are stored as SHA-256 hashes
in the database.

## Token Lifecycle

1. **Create** -- `create/3` generates a prefixed token, validates scopes, and
   stores the SHA-256 hash. The raw token is returned once and never stored.

2. **Verify** -- `verify/2` hashes the submitted token and looks up the hash.
   Revoked and expired tokens are rejected. Successful verification does not
   write a durable audit row (D-27); `maybe_update_last_used/2` bumps
   `last_used_at` asynchronously via `Task.start/1` so the hot path stays
   low-latency while telemetry covers observability. When `:audit_schema` is
   configured, **`api.token_verify.failure`** rows are written inside
   **`Repo.transaction/1`** via **`Ecto.Multi`** + **`Audit.log_multi_safe/3`**
   (audit-only transaction; success remains unaudited per D-27).

3. **Revoke** -- `revoke/2` soft-deletes a token by setting `revoked_at`.
   `revoke_all/2` revokes all active tokens for a user. With `:audit_schema`
   configured, both operations append `api.token_revoke` /
   `api.token_revoke_all` on the same `Ecto.Multi` as the DB write (AUD-07).

4. **JWT refresh auditing** -- `audit_jwt_refresh/2` and `audit_jwt_refresh_reuse/2`
   emit `api.jwt_refresh` / `api.jwt_refresh_reuse` rows when `:audit_schema` is
   set, using **`Repo.transaction/1`** + **`Ecto.Multi`** + **`Audit.log_multi_safe/3`**
   (audit-only transaction; AUD-18). When `:audit_schema` is set, **`Sigra.JWT.refresh/3`**
   also performs **persistence + audit co-fate** in one transaction — do not call
   **`audit_jwt_refresh/2`** afterward or you risk double-audit rows.

## Scope System

Tokens carry a list of scopes in `resource:action` format (e.g., `"profile:read"`).
The `can?/2` function checks whether a token's scopes satisfy requirements.
The special `"*"` wildcard scope grants access to all resources.

## Security

- Raw tokens are never stored; only SHA-256 hashes are persisted
- Token prefix is validated to prevent JWT collision (`eyJ` prefix blocked)
- All operations emit telemetry events for observability

# `append_api_token_jwt_audit_to_multi`
*since 0.9.1* 

Appends a JWT audit insert step to the given `Ecto.Multi`.

Delegates to `Sigra.Audit.log_multi_safe/3` (no-op when `:audit_schema` is `nil`).

For internal composition from `Sigra.JWT.refresh/3` when `:audit_schema` is set.
Host applications should not compose arbitrary multis unless they own the
outer `Repo.transaction/1`.

# `audit_jwt_refresh`
*since 0.9.0* 

```elixir
@spec audit_jwt_refresh(Sigra.Config.t(), term()) :: :ok
```

Emit **api.jwt_refresh** audit row (called from Sigra.JWT refresh flow).

When **`:audit_schema`** is configured, the row is written inside
**`Repo.transaction/1`** via audit-only **`Ecto.Multi`** + **`Audit.log_multi_safe/3`**
(same durability posture as verify-failure auditing).

Returns **`:ok`** even when auditing is disabled or when the audit insert is
rejected or rolled back after the host transaction has already committed elsewhere.
**`:ok` does not prove** the audit row exists; monitor **`[:sigra, :audit, :log_safe_error]`**
for **`reason: :invalid_changeset`** or **`:constraint_violation`**.

This is exposed so the JWT refresh implementation (potentially a separate module)
can write a consistent audit row through this module's helpers.

# `audit_jwt_refresh_reuse`
*since 0.9.0* 

```elixir
@spec audit_jwt_refresh_reuse(Sigra.Config.t(), term()) :: :ok
```

Emit **api.jwt_refresh_reuse** audit row (detected refresh-token reuse).

When **`:audit_schema`** is configured, uses the same transactional **`Multi`** +
**`log_multi_safe`** path as **`audit_jwt_refresh/2`**.

Returns **`:ok`** regardless of whether a durable audit row was persisted; see
**`audit_jwt_refresh/2`** and **`[:sigra, :audit, :log_safe_error]`** for operational honesty.

# `can?`
*since 0.7.0* 

```elixir
@spec can?(map(), [String.t()], keyword()) :: boolean()
```

Checks whether a token or scope struct has the required scopes.

Accepts either a map with `:scopes` (token struct) or `:token_scopes`
(scope struct from conn.assigns).

## Options

- `:match` - `:all` (default) requires all scopes, `:any` requires at least one

## Examples

    Sigra.APIToken.can?(token, ["profile:read"])
    #=> true

    Sigra.APIToken.can?(token, ["admin:write"], match: :any)
    #=> false

# `create`
*since 0.7.0* 

```elixir
@spec create(Sigra.Config.t(), map(), map()) ::
  {:ok, String.t(), map()} | {:error, term()}
```

Creates a new API token for the given user.

Returns `{:ok, raw_key, token_record}` on success. The `raw_key` includes
the configured prefix and should be shown to the user exactly once.

## Parameters

- `config` - A `%Sigra.Config{}` struct
- `user` - The user struct (must have an `:id` field)
- `attrs` - A map with:
  - `:name` (required) - Human-readable token name, max 255 chars
  - `:scopes` (required) - List of scope strings
  - `:expires_at` (optional) - Expiration datetime

## Examples

    {:ok, raw_key, token} = Sigra.APIToken.create(config, user, %{
      name: "CI Deploy Key",
      scopes: ["profile:read", "api_tokens:read"]
    })

# `decode_cursor`
*since 0.7.0* 

```elixir
@spec decode_cursor(String.t()) :: {DateTime.t(), integer()}
```

Decodes an opaque cursor string into `{inserted_at, id}`.

# `encode_cursor`
*since 0.7.0* 

```elixir
@spec encode_cursor(DateTime.t(), term()) :: String.t()
```

Encodes an inserted_at timestamp and ID into an opaque cursor string.

# `list_active`
*since 0.7.0* 

```elixir
@spec list_active(Sigra.Config.t(), term(), keyword()) :: {[map()], String.t() | nil}
```

Lists active (non-revoked, non-expired) API tokens for a user with cursor pagination.

Returns `{tokens, next_cursor}` where `next_cursor` is `nil` on the last page.

## Options

- `:limit` - Page size (default from config, max from config)
- `:cursor` - Opaque cursor string from a previous call

## Examples

    {tokens, cursor} = Sigra.APIToken.list_active(config, user_id)
    {more_tokens, nil} = Sigra.APIToken.list_active(config, user_id, cursor: cursor)

# `list_scopes`
*since 0.7.0* 

```elixir
@spec list_scopes(Sigra.Config.t()) :: [String.t()]
```

Returns all registered scopes (built-in + custom).

Delegates to `Sigra.APIToken.ScopeRegistry.all_scopes/1`.

## Examples

    scopes = Sigra.APIToken.list_scopes(config)
    #=> ["profile:read", "profile:write", ...]

# `revoke`
*since 0.7.0* 

```elixir
@spec revoke(Sigra.Config.t(), term()) ::
  {:ok, map()} | {:error, :not_found} | {:error, Ecto.Changeset.t()}
```

Revokes a single API token by ID.

Sets `revoked_at` to the current UTC time. Returns `{:ok, token}` on
success or `{:error, :not_found}` if the token does not exist.

## Examples

    {:ok, revoked_token} = Sigra.APIToken.revoke(config, token_id)

# `revoke_all`
*since 0.7.0* 

```elixir
@spec revoke_all(Sigra.Config.t(), map()) :: {:ok, non_neg_integer()}
```

Revokes all active API tokens for a user.

Sets `revoked_at` on all tokens where `revoked_at IS NULL` for the given user.
Returns `{:ok, count}` with the number of tokens revoked.

## Examples

    {:ok, 3} = Sigra.APIToken.revoke_all(config, user)

# `verify`
*since 0.7.0* 

```elixir
@spec verify(Sigra.Config.t(), String.t()) ::
  {:ok, map()} | {:error, :invalid_token | :token_revoked | :token_expired}
```

Verifies a raw API token string.

Hashes the token and looks up the hash in the database. Returns
`{:ok, token}` for valid active tokens, or an error tuple.

## Error Returns

- `{:error, :invalid_token}` - Token not found
- `{:error, :token_revoked}` - Token has been revoked
- `{:error, :token_expired}` - Token has expired

## Examples

    case Sigra.APIToken.verify(config, raw_token) do
      {:ok, token} -> # authenticated
      {:error, reason} -> # rejected
    end

---

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