Raxol.Gateway.Pairing (RaxolGateway v0.2.0)

Copy Markdown View Source

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]}

Summary

Functions

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

Configure a platform to allow every user.

Mark a user paired directly (admin path).

Decide whether a route is authorized.

Returns a specification to start this module under a supervisor.

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

Whether a user is paired.

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

Remove a user's pairing.

Functions

allow(server, user_id, scope \\ :global)

@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(server, platform)

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

Configure a platform to allow every user.

approve(server, user_id)

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

Mark a user paired directly (admin path).

authorize(server, route)

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

Decide whether a route is authorized.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

confirm(server, code)

@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(msg, state)

Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.

handle_manager_info(msg, state)

Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_info/2.

paired?(server, user_id)

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

Whether a user is paired.

request_code(server, user_id)

@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(server, user_id)

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

Remove a user's pairing.

start_link(init_opts \\ [])

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