diff --git a/.changeset/nice-taxis-shake.md b/.changeset/nice-taxis-shake.md new file mode 100644 index 0000000000..eb56e71683 --- /dev/null +++ b/.changeset/nice-taxis-shake.md @@ -0,0 +1,5 @@ +--- +'@core/sync-service': patch +--- + +Add optional PhoenixLiveDashboard with configurable port to listen on. diff --git a/README.md b/README.md index 9e20fd25d7..dc1ea7741c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Real-time sync for Postgres. - [What is Electric?](#what-is-electric) - [Getting Started](#getting-started) - [HTTP API Docs](#http-api-docs) +- [Phoenix LiveDashboard](#phoenix-livedashboard) - [Developing Electric](#developing-electric) - [Mac setup](#mac-setup) - [Contributing](#contributing) @@ -98,6 +99,20 @@ Again, see the [Quickstart](https://electric-sql.com/docs/quickstart) and the [D The HTTP API is defined in an [OpenAPI spec](https://swagger.io/specification/) in [website/electric-api.yaml](./website/electric-api.yaml). +## Phoenix LiveDashboard + +Electric includes an optional [Phoenix LiveDashboard](https://github.com/phoenixframework/phoenix_live_dashboard) for real-time monitoring of the running system (VM metrics, process info, ETS tables, etc.). + +To enable it, set the `ELECTRIC_LIVE_DASHBOARD_PORT` environment variable: + +```sh +ELECTRIC_LIVE_DASHBOARD_PORT=4000 +``` + +The dashboard will be available at `http://localhost:4000` (or whichever port you choose). When the variable is not set, the dashboard is not started. + +> **WARNING: The LiveDashboard endpoint is completely unauthenticated.** Anyone with network access to the port can view internal system state. In production, you **must** restrict access to this port using firewall rules, network policies, or similar controls. Do not expose it to the public internet. + ## Developing Electric We use [asdf](https://asdf-vm.com/) to install Elixir, Erlang, and Node.js. Versions are defined in [.tool-versions](.tool-versions). diff --git a/packages/sync-service/.formatter.exs b/packages/sync-service/.formatter.exs index 27e06df52f..e7f97f0185 100644 --- a/packages/sync-service/.formatter.exs +++ b/packages/sync-service/.formatter.exs @@ -1,5 +1,5 @@ # Used by "mix format" [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], - import_deps: [:plug, :stream_data] + import_deps: [:plug, :stream_data, :phoenix] ] diff --git a/packages/sync-service/config/runtime.exs b/packages/sync-service/config/runtime.exs index 892b55cafd..554f83aed0 100644 --- a/packages/sync-service/config/runtime.exs +++ b/packages/sync-service/config/runtime.exs @@ -150,6 +150,7 @@ replication_stream_id = ) prometheus_port = env!("ELECTRIC_PROMETHEUS_PORT", :integer, nil) +live_dashboard_port = env!("ELECTRIC_LIVE_DASHBOARD_PORT", :integer, nil) call_home_telemetry_url = env!( @@ -240,6 +241,7 @@ config :electric, env!("ELECTRIC_TELEMETRY_LONG_MESSAGE_QUEUE_DISABLE_THRESHOLD", :integer, nil), telemetry_statsd_host: statsd_host, prometheus_port: prometheus_port, + live_dashboard_port: live_dashboard_port, db_pool_size: env!("ELECTRIC_DB_POOL_SIZE", :integer, nil), replication_stream_id: replication_stream_id, replication_slot_temporary?: env!("CLEANUP_REPLICATION_SLOTS_ON_SHUTDOWN", :boolean, nil), @@ -387,3 +389,31 @@ if Electric.telemetry_enabled?() do config :opentelemetry, processors: [] end end + +# Phoenix LiveDashboard Endpoint Configuration +# +# WARNING: The dashboard is completely unauthenticated and exposes internal +# system state (VM metrics, process info, ETS tables, etc.). In production, +# ensure the dashboard port is firewalled or otherwise restricted to trusted +# networks only. +if live_dashboard_port do + dashboard_ip = + if env!("ELECTRIC_LISTEN_ON_IPV6", :boolean, false), + do: {0, 0, 0, 0, 0, 0, 0, 0}, + else: {0, 0, 0, 0} + + config :electric, Electric.LiveDashboard.Endpoint, + adapter: Bandit.PhoenixAdapter, + http: [ + port: live_dashboard_port, + ip: dashboard_ip + ], + server: true, + render_errors: [ + formats: [html: Electric.LiveDashboard.ErrorView], + layout: false + ], + live_view: [signing_salt: "r5zw+GcXjt3wP3Z/snFRqQ5uH2cm8Vb7ldc8t0POZdo="], + secret_key_base: Base.encode64(:crypto.strong_rand_bytes(48)), + pubsub_server: Electric.PubSub +end diff --git a/packages/sync-service/lib/electric/application.ex b/packages/sync-service/lib/electric/application.ex index 2d22006f25..7771809364 100644 --- a/packages/sync-service/lib/electric/application.ex +++ b/packages/sync-service/lib/electric/application.ex @@ -53,7 +53,8 @@ defmodule Electric.Application do application_telemetry(config), [{Electric.StackSupervisor, Keyword.put(config, :name, Electric.StackSupervisor)}], api_server_children(config), - prometheus_endpoint(Electric.Config.get_env(:prometheus_port)) + prometheus_endpoint(Electric.Config.get_env(:prometheus_port)), + live_dashboard_endpoint(Electric.Config.get_env(:live_dashboard_port)) ]) end @@ -270,6 +271,15 @@ defmodule Electric.Application do ] end + defp live_dashboard_endpoint(nil), do: [] + + defp live_dashboard_endpoint(_port) do + [ + {Phoenix.PubSub, name: Electric.PubSub}, + Electric.LiveDashboard.Endpoint + ] + end + @doc false # REQUIRED (but undocumented) public API for Phoenix.Sync def api_server do diff --git a/packages/sync-service/lib/electric/config.ex b/packages/sync-service/lib/electric/config.ex index 83764bb332..9fd47cf729 100644 --- a/packages/sync-service/lib/electric/config.ex +++ b/packages/sync-service/lib/electric/config.ex @@ -121,7 +121,8 @@ defmodule Electric.Config do shape_db_synchronous: Electric.ShapeCache.ShapeStatus.ShapeDb.Connection.default!(:synchronous), shape_db_cache_size: Electric.ShapeCache.ShapeStatus.ShapeDb.Connection.default!(:cache_size), - exclude_spans: MapSet.new() + exclude_spans: MapSet.new(), + live_dashboard_port: nil ] @installation_id_key "electric_installation_id" diff --git a/packages/sync-service/lib/electric/live_dashboard/endpoint.ex b/packages/sync-service/lib/electric/live_dashboard/endpoint.ex new file mode 100644 index 0000000000..9475771a31 --- /dev/null +++ b/packages/sync-service/lib/electric/live_dashboard/endpoint.ex @@ -0,0 +1,44 @@ +defmodule Electric.LiveDashboard.Endpoint do + @moduledoc """ + Phoenix Endpoint for serving LiveDashboard on a separate port. + This runs independently from the main Plug-based HTTP server. + + **WARNING: This endpoint is completely unauthenticated.** It exposes internal + system state (VM metrics, process info, etc.). In production, ensure the + dashboard port is not publicly accessible — use firewall rules or network + policies to restrict access. + """ + + use Phoenix.Endpoint, otp_app: :electric + + # LiveView socket for dashboard interactions + socket "/live", Phoenix.LiveView.Socket, + websocket: true, + longpoll: true + + # Serve static assets for LiveDashboard + plug Plug.Static, + at: "/", + from: :phoenix_live_dashboard, + gzip: false, + only: ~w(assets fonts images priv) + + # Session configuration for LiveView + plug Plug.Session, + store: :cookie, + key: "_live_dashboard_key", + signing_salt: "abc43s8Z", + same_site: "Lax" + + # Parse request body for LiveView + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Jason + + plug Plug.MethodOverride + plug Plug.Head + + # Route all requests to the LiveDashboard router + plug Electric.LiveDashboard.Router +end diff --git a/packages/sync-service/lib/electric/live_dashboard/error_view.ex b/packages/sync-service/lib/electric/live_dashboard/error_view.ex new file mode 100644 index 0000000000..620d9c9501 --- /dev/null +++ b/packages/sync-service/lib/electric/live_dashboard/error_view.ex @@ -0,0 +1,10 @@ +defmodule Electric.LiveDashboard.ErrorView do + @moduledoc """ + Error view for the LiveDashboard endpoint. + Handles rendering of HTTP errors. + """ + + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/packages/sync-service/lib/electric/live_dashboard/favicon_controller.ex b/packages/sync-service/lib/electric/live_dashboard/favicon_controller.ex new file mode 100644 index 0000000000..d2f4e193fa --- /dev/null +++ b/packages/sync-service/lib/electric/live_dashboard/favicon_controller.ex @@ -0,0 +1,12 @@ +defmodule Electric.LiveDashboard.FaviconController do + @moduledoc """ + Simple controller to handle favicon requests. + Returns a 204 No Content response to avoid 500 errors. + """ + + use Phoenix.Controller, formats: [:html] + + def show(conn, _params) do + send_resp(conn, 204, "") + end +end diff --git a/packages/sync-service/lib/electric/live_dashboard/router.ex b/packages/sync-service/lib/electric/live_dashboard/router.ex new file mode 100644 index 0000000000..df467bec4c --- /dev/null +++ b/packages/sync-service/lib/electric/live_dashboard/router.ex @@ -0,0 +1,26 @@ +defmodule Electric.LiveDashboard.Router do + @moduledoc """ + Phoenix Router for LiveDashboard. + Mounts the dashboard at the root path. + """ + + use Phoenix.Router + import Phoenix.LiveDashboard.Router + + pipeline :browser do + plug :accepts, ["html", "json"] + plug :fetch_session + plug :put_root_layout, html: {Phoenix.LiveDashboard.LayoutView, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + scope "/" do + pipe_through :browser + + # Handle favicon requests gracefully + get "/favicon.ico", Electric.LiveDashboard.FaviconController, :show + + live_dashboard "/", ecto_repos: [] + end +end diff --git a/packages/sync-service/mix.exs b/packages/sync-service/mix.exs index db6e2b1170..c06759cb94 100644 --- a/packages/sync-service/mix.exs +++ b/packages/sync-service/mix.exs @@ -105,7 +105,8 @@ defmodule Electric.MixProject do {:remote_ip, "~> 1.2"}, {:req, "~> 0.5"}, {:stream_split, "~> 0.1"}, - {:tz, "~> 0.28"} + {:tz, "~> 0.28"}, + {:phoenix_live_dashboard, "~> 0.8"} ], dev_and_test_deps(), telemetry_deps(Mix.target()) diff --git a/packages/sync-service/mix.lock b/packages/sync-service/mix.lock index 4ba0f43d99..050dcf7c05 100644 --- a/packages/sync-service/mix.lock +++ b/packages/sync-service/mix.lock @@ -1,7 +1,7 @@ %{ "acceptor_pool": {:hex, :acceptor_pool, "1.0.1", "d88c2e8a0be9216cf513fbcd3e5a4beb36bee3ff4168e85d6152c6f899359cdb", [:rebar3], [], "hexpm", "f172f3d74513e8edd445c257d596fc84dbdd56d2c6fa287434269648ae5a421e"}, "backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"}, - "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"}, + "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, @@ -41,11 +41,17 @@ "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.2", "410ab4d76b0921f42dbccbe5a7c831b8125282850be649ee1f70050d3961118a", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "641ab469deb181957ac6d59bce6e1321d5fe2a56df444fc9c19afcad623ab253"}, "otel_metric_exporter": {:hex, :otel_metric_exporter, "0.4.3", "c8e47eae9f222e100b590eb95246e49ea4a376bf3595067ee089ddfc7169405a", [:mix], [{:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.15", [hex: :protobuf, repo: "hexpm", optional: false]}, {:retry, "~> 0.19", [hex: :retry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "6cdb0a9bcd6f4bee3e70b7b8664af8b6d23156e978ccedde47782f56b6636531"}, "pg_query_ex": {:hex, :pg_query_ex, "0.10.0", "3dc3a20f90173bd6e893db58062a6bbeda583bd7962793235da0cde046ac52da", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:protox, "~> 2.0", [hex: :protox, repo: "hexpm", optional: false]}], "hexpm", "783f2082ff9534298eab5570a9317f510c425fcbe4ae88d502561207fdd1bdd9"}, + "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, "protobuf": {:hex, :protobuf, "0.13.0", "7a9d9aeb039f68a81717eb2efd6928fdf44f03d2c0dfdcedc7b560f5f5aae93d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "21092a223e3c6c144c1a291ab082a7ead32821ba77073b72c68515aa51fef570"}, - "protox": {:hex, :protox, "2.0.4", "2a86ae3699696c5d92e15804968ce6a6827a8d9516d0bbabcf16584dec710ae1", [:mix], [], "hexpm", "8ac5a03bb84da4c75d76dc29cd46008081c2068ad0f6f0da4c051093d6e24c01"}, + "protox": {:hex, :protox, "2.0.7", "fa4639568a045f034fc756ad62b0edf59c934f6ad2aa163a8ba457209d54f8cb", [:mix], [], "hexpm", "42efb28326e7003f4a2d3a809176b8ba2f01a950e36f6470364b64c288919cb2"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "repatch": {:hex, :repatch, "1.6.1", "e2bc55d7358de5727a7d989886d9aebd6497ccab4232b41dcae54f8fd6539c19", [:mix], [{:ex2ms, "~> 1.7.0", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "486f80b503fad097ae9ff33f3c7ad3a24c2bad85e694d76ff214e15d13cf9d25"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, @@ -63,4 +69,5 @@ "tls_certificate_check": {:hex, :tls_certificate_check, "1.31.0", "9a910b54d8cb96cc810cabf4c0129f21360f82022b20180849f1442a25ccbb04", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "9d2b41b128d5507bd8ad93e1a998e06d0ab2f9a772af343f4c00bf76c6be1532"}, "tz": {:hex, :tz, "0.28.1", "717f5ffddfd1e475e2a233e221dc0b4b76c35c4b3650b060c8e3ba29dd6632e9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "bfdca1aa1902643c6c43b77c1fb0cb3d744fd2f09a8a98405468afdee0848c8a"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, }