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:
- the route's platform is configured to allow everyone, or
- the user is paired, or
- the user is in the platform's allowlist, or
- the user is in the global allowlist, otherwise
- 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.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_info/2.
Whether a user is paired.
Issue a pairing code for a user, or {:error, :rate_limited}.
Remove a user's pairing.
Functions
@spec allow(GenServer.server(), String.t(), :global | {:platform, atom()}) :: :ok
Add a user to the global allowlist, or a platform allowlist with {:platform, atom}.
@spec allow_platform_all(GenServer.server(), atom()) :: :ok
Configure a platform to allow every user.
@spec approve(GenServer.server(), String.t()) :: :ok
Mark a user paired directly (admin path).
@spec authorize(GenServer.server(), Raxol.Gateway.Route.t()) :: :allow | :deny
Decide whether a route is authorized.
Returns a specification to start this module under a supervisor.
See Supervisor.
@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.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_info/2.
@spec paired?(GenServer.server(), String.t()) :: boolean()
Whether a user is paired.
@spec request_code(GenServer.server(), String.t()) :: {:ok, String.t()} | {:error, :rate_limited}
Issue a pairing code for a user, or {:error, :rate_limited}.
@spec revoke(GenServer.server(), String.t()) :: :ok
Remove a user's pairing.
@spec start_link(keyword()) :: GenServer.on_start()