towerops/lib/towerops_web/plugs/runtime_session.ex
Graham McIntire 8682cdce55 fix: resolve session salts at runtime so prod release boots
The @session_options module attribute used Application.compile_env, which
baked compile-time placeholders into the endpoint while runtime.exs set
the real values from SESSION_SIGNING_SALT / SESSION_ENCRYPTION_SALT env
vars. Phoenix detected the mismatch and refused to start (failing migrate
Job in k8s).

- Remove hardcoded salts from config/config.exs (no compile-time binding)
- Add stable per-env salts in dev.exs / test.exs so local + CI don't need
  the env vars
- Split static cookie opts (@static_session_options) from runtime-resolved
  opts in endpoint.ex; expose session_options/0 as an MFA tuple in socket
  connect_info so LiveView decodes sessions with the same runtime salts
- New ToweropsWeb.Plugs.RuntimeSession wraps Plug.Session, fetches salts
  from app env on first request, and caches the initialized opts in
  :persistent_term (zero per-request overhead after warm-up)
2026-05-12 16:26:54 -05:00

33 lines
992 B
Elixir

defmodule ToweropsWeb.Plugs.RuntimeSession do
@moduledoc """
Wrapper around `Plug.Session` that resolves session salts at runtime.
The signing and encryption salts come from environment variables
(`SESSION_SIGNING_SALT` / `SESSION_ENCRYPTION_SALT`) via `config/runtime.exs`.
Because `Plug.Session` is compiled into the endpoint pipeline with its
options baked in, this plug defers `Plug.Session.init/1` until the first
request and caches the result in `:persistent_term`.
"""
@behaviour Plug
@impl true
def init(static_opts), do: static_opts
@impl true
def call(conn, static_opts) do
Plug.Session.call(conn, session_opts(static_opts))
end
defp session_opts(static_opts) do
case :persistent_term.get({__MODULE__, :opts}, nil) do
nil ->
opts = Plug.Session.init(static_opts ++ ToweropsWeb.Endpoint.runtime_session_opts())
:persistent_term.put({__MODULE__, :opts}, opts)
opts
opts ->
opts
end
end
end