defmodule CCXT.WS.Auth do @moduledoc """ WebSocket authentication pattern dispatcher. Parallel of `CCXT.Signing`: a function-head dispatcher that routes a pattern atom to the matching per-pattern module. Each pattern module implements the `CCXT.WS.Auth.Behaviour` callbacks. ## Supported Patterns | Pattern | Exchanges | Notes | |---------|-----------|-------| | `:direct_hmac_expiry` | bybit, bitmex, htx family | `GET/realtime{expires}` HMAC-SHA256 | | `:iso_passphrase` | okx family, kucoin family | Requires `credentials.password` | | `:jsonrpc_linebreak` | deribit | JSON-RPC `public/auth`, returns `{:ok, %{ttl_ms: _}}` | | `:sha384_nonce` | bitfinex | `AUTH{nonce}` HMAC-SHA384 | | `:sha512_newline` | gate, gateio | Gate `spot.login` HMAC-SHA512 | | `:listen_key` | binance family, aster | REST pre-auth, no WS auth frame | | `:rest_token` | kraken | REST pre-auth, token injected into subscribe frames | | `:inline_subscribe` | coinbaseexchange | Auth fields inlined in each subscribe frame | ## Usage Typical flow from the adapter layer (T94/T95): {:ok, pre_auth_data} = CCXT.WS.Auth.pre_auth(pattern, credentials, config, market_type: :spot) # caller performs any REST round-trip implied by pre_auth_data case CCXT.WS.Auth.build_auth_message(pattern, credentials, config) do {:ok, frame} -> ZenWebsocket.Client.send_message(client, Jason.encode!(frame)) :no_message -> :ok # listen_key / rest_token / inline_subscribe {:error, why} -> {:error, why} end # After receiving the auth response frame: case CCXT.WS.Auth.handle_auth_response(pattern, response, state) do :ok -> :authenticated {:ok, %{ttl_ms: _}} = meta -> schedule_re_auth(meta) {:error, reason} -> {:auth_failed, reason} end Per-exchange configuration (which pattern + which config options + which channels need inline token injection) lives in `CCXT.WS.Config` entries added by Task 94. """ alias CCXT.Credentials alias CCXT.WS.Auth.DirectHmacExpiry alias CCXT.WS.Auth.InlineSubscribe alias CCXT.WS.Auth.IsoPassphrase alias CCXT.WS.Auth.JsonrpcLinebreak alias CCXT.WS.Auth.ListenKey alias CCXT.WS.Auth.RestToken alias CCXT.WS.Auth.Sha384Nonce alias CCXT.WS.Auth.Sha512Newline @type pattern :: :direct_hmac_expiry | :iso_passphrase | :jsonrpc_linebreak | :sha384_nonce | :sha512_newline | :listen_key | :rest_token | :inline_subscribe @type config :: map() @type auth_message :: map() @type pre_auth_result :: {:ok, map()} | {:error, term()} @type build_result :: {:ok, auth_message()} | :no_message | {:error, term()} @patterns [ :direct_hmac_expiry, :iso_passphrase, :jsonrpc_linebreak, :sha384_nonce, :sha512_newline, :listen_key, :rest_token, :inline_subscribe ] @doc "Lists every supported auth pattern atom." @spec patterns() :: [pattern()] def patterns, do: @patterns @doc """ Performs pattern-specific pre-auth. Patterns that don't require a REST round-trip return `{:ok, %{}}`. """ @spec pre_auth(pattern(), Credentials.t(), config(), keyword()) :: pre_auth_result() def pre_auth(:listen_key, credentials, config, opts), do: ListenKey.pre_auth(credentials, config, opts) def pre_auth(:rest_token, credentials, config, opts), do: RestToken.pre_auth(credentials, config, opts) def pre_auth(:direct_hmac_expiry, credentials, config, opts), do: DirectHmacExpiry.pre_auth(credentials, config, opts) def pre_auth(:iso_passphrase, credentials, config, opts), do: IsoPassphrase.pre_auth(credentials, config, opts) def pre_auth(:jsonrpc_linebreak, credentials, config, opts), do: JsonrpcLinebreak.pre_auth(credentials, config, opts) def pre_auth(:sha384_nonce, credentials, config, opts), do: Sha384Nonce.pre_auth(credentials, config, opts) def pre_auth(:sha512_newline, credentials, config, opts), do: Sha512Newline.pre_auth(credentials, config, opts) def pre_auth(:inline_subscribe, credentials, config, opts), do: InlineSubscribe.pre_auth(credentials, config, opts) def pre_auth(pattern, _credentials, _config, _opts), do: {:error, {:unknown_pattern, pattern}} @doc """ Builds the auth frame for the pattern. Patterns without a standalone auth frame return `:no_message` — the caller should skip the send step. """ @spec build_auth_message(pattern(), Credentials.t(), config(), keyword()) :: build_result() def build_auth_message(:direct_hmac_expiry, credentials, config, opts), do: DirectHmacExpiry.build_auth_message(credentials, config, opts) def build_auth_message(:iso_passphrase, credentials, config, opts), do: IsoPassphrase.build_auth_message(credentials, config, opts) def build_auth_message(:jsonrpc_linebreak, credentials, config, opts), do: JsonrpcLinebreak.build_auth_message(credentials, config, opts) def build_auth_message(:sha384_nonce, credentials, config, opts), do: Sha384Nonce.build_auth_message(credentials, config, opts) def build_auth_message(:sha512_newline, credentials, config, opts), do: Sha512Newline.build_auth_message(credentials, config, opts) def build_auth_message(:listen_key, credentials, config, opts), do: ListenKey.build_auth_message(credentials, config, opts) def build_auth_message(:rest_token, credentials, config, opts), do: RestToken.build_auth_message(credentials, config, opts) def build_auth_message(:inline_subscribe, credentials, config, opts), do: InlineSubscribe.build_auth_message(credentials, config, opts) def build_auth_message(pattern, _credentials, _config, _opts), do: {:error, {:unknown_pattern, pattern}} @doc """ Builds inline auth data to merge into subscribe frames. `:inline_subscribe` (coinbase) produces per-subscribe HMAC fields. `:rest_token` (kraken) injects `%{"token" => token}` — the caller must thread the REST-acquired token through `opts[:token]` since pattern modules are stateless. Returns `nil` for all other patterns. """ @spec build_subscribe_auth( pattern(), Credentials.t(), config(), String.t() | nil, list(String.t()) | nil ) :: map() | nil def build_subscribe_auth(:inline_subscribe, credentials, config, channel, symbols), do: InlineSubscribe.build_subscribe_auth(credentials, config, channel, symbols) def build_subscribe_auth(:rest_token, _credentials, config, _channel, _symbols) do case Map.get(config, :token) do nil -> nil token when is_binary(token) -> %{"token" => token} end end def build_subscribe_auth(_pattern, _credentials, _config, _channel, _symbols), do: nil @doc "Dispatches an auth response to the pattern's classifier." @spec handle_auth_response(pattern(), map(), map()) :: :ok | {:ok, map()} | {:error, term()} def handle_auth_response(:direct_hmac_expiry, response, state), do: DirectHmacExpiry.handle_auth_response(response, state) def handle_auth_response(:iso_passphrase, response, state), do: IsoPassphrase.handle_auth_response(response, state) def handle_auth_response(:jsonrpc_linebreak, response, state), do: JsonrpcLinebreak.handle_auth_response(response, state) def handle_auth_response(:sha384_nonce, response, state), do: Sha384Nonce.handle_auth_response(response, state) def handle_auth_response(:sha512_newline, response, state), do: Sha512Newline.handle_auth_response(response, state) def handle_auth_response(:listen_key, response, state), do: ListenKey.handle_auth_response(response, state) def handle_auth_response(:rest_token, response, state), do: RestToken.handle_auth_response(response, state) def handle_auth_response(:inline_subscribe, response, state), do: InlineSubscribe.handle_auth_response(response, state) def handle_auth_response(pattern, _response, _state), do: {:error, {:unknown_pattern, pattern}} @doc "Returns `true` for patterns that require a REST pre-auth round-trip." @spec requires_pre_auth?(pattern()) :: boolean() def requires_pre_auth?(:listen_key), do: true def requires_pre_auth?(:rest_token), do: true def requires_pre_auth?(_), do: false @doc "Returns `true` for patterns that attach auth to each subscribe frame." @spec inline_auth?(pattern()) :: boolean() def inline_auth?(:inline_subscribe), do: true def inline_auth?(:rest_token), do: true def inline_auth?(_), do: false @doc "Returns the implementing module for a pattern, or `nil` if unknown." @spec module_for_pattern(pattern()) :: module() | nil def module_for_pattern(:direct_hmac_expiry), do: DirectHmacExpiry def module_for_pattern(:iso_passphrase), do: IsoPassphrase def module_for_pattern(:jsonrpc_linebreak), do: JsonrpcLinebreak def module_for_pattern(:sha384_nonce), do: Sha384Nonce def module_for_pattern(:sha512_newline), do: Sha512Newline def module_for_pattern(:listen_key), do: ListenKey def module_for_pattern(:rest_token), do: RestToken def module_for_pattern(:inline_subscribe), do: InlineSubscribe def module_for_pattern(_), do: nil end