# `Raxol.Gateway.Pairing`
[🔗](https://github.com/DROOdotFOO/raxol/blob/raxol_gateway-v0.2.0/lib/raxol/gateway/pairing.ex#L1)

DM pairing codes and the gateway's authorization decision.

A user requests a pairing code (rate-limited per user); confirming a valid,
unexpired code marks that user paired. `authorize/2` then decides whether a
route is allowed, in this order:

  1. the route's platform is configured to allow everyone, or
  2. the user is paired, or
  3. the user is in the platform's allowlist, or
  4. the user is in the global allowlist, otherwise
  5. deny.

Codes are 8 characters from an unambiguous alphabet (no `0/O/1/I/L`), expire
after `:code_ttl_ms`, and repeated invalid confirms lock confirmation for
`:lockout_ms` after `:max_failures`. The lockout is global for this slice.

## Pairings and the global allowlist are NOT platform-scoped

Steps 2 and 4 above match on the user id alone. A pairing confirmed over
Telegram, or an id in `:allowed_users`, admits that same id STRING on every
connected platform. Platform id namespaces are unrelated -- a Telegram integer
id, a Discord snowflake and an email address are drawn from different spaces --
so this is a deliberate convenience, not an identity guarantee, and it is only
safe while those spaces do not collide.

Use `:platform_users` (step 3) or `allow/3` with `{:platform, atom}` when the
grant should mean "this id, on this platform". A deployment connecting two
platforms whose ids could collide should prefer them for everything.

## Config keys (all optional)

`:code_ttl_ms` (1h), `:request_cooldown_ms` (30s), `:max_failures` (5),
`:lockout_ms` (5m), `:code_length` (8).

## Seeds, and why the configured posture is not runtime state

`:allow_platforms`, `:allowed_users` and `:platform_users` are applied at
`init_manager/1` on every start, including a supervisor RESTART. The
runtime-mutable calls (`allow/3`, `allow_platform_all/2`, `approve/2`) exist
alongside them and are lost on restart, which is what in-memory pairing means.

The split matters because the two failure modes are not symmetric. Losing a
DM pairing costs one user one re-pair. Losing the configured posture is total
and silent in BOTH directions: an `:open` deployment starts denying every
message forever, and an enforcing one revokes every allowlist entry it was
given. Neither re-announces itself, because the boot that announced it already
happened. Seeding from opts means a crash restores the deployment's intent
rather than an empty server that no caller can tell from a configured one.

    {Raxol.Gateway.Pairing, name: :gw_pairing, allow_platforms: [:telegram]}

# `allow`

```elixir
@spec allow(GenServer.server(), String.t(), :global | {:platform, atom()}) :: :ok
```

Add a user to the global allowlist, or a platform allowlist with `{:platform, atom}`.

# `allow_platform_all`

```elixir
@spec allow_platform_all(GenServer.server(), atom()) :: :ok
```

Configure a platform to allow every user.

# `approve`

```elixir
@spec approve(GenServer.server(), String.t()) :: :ok
```

Mark a user paired directly (admin path).

# `authorize`

```elixir
@spec authorize(GenServer.server(), Raxol.Gateway.Route.t()) :: :allow | :deny
```

Decide whether a route is authorized.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `confirm`

```elixir
@spec confirm(GenServer.server(), String.t()) ::
  {:ok, String.t()} | {:error, :invalid | :expired | :locked_out}
```

Confirm a code, pairing its user. Errors: `:invalid`, `:expired`, `:locked_out`.

# `handle_manager_cast`

# `handle_manager_info`

# `paired?`

```elixir
@spec paired?(GenServer.server(), String.t()) :: boolean()
```

Whether a user is paired.

# `request_code`

```elixir
@spec request_code(GenServer.server(), String.t()) ::
  {:ok, String.t()} | {:error, :rate_limited}
```

Issue a pairing code for a user, or `{:error, :rate_limited}`.

# `revoke`

```elixir
@spec revoke(GenServer.server(), String.t()) :: :ok
```

Remove a user's pairing.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

---

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