Packages
ex_infisical
0.1.0
Elixir client for the Infisical secrets API. Universal Auth with skew-aware token caching, optional Cloudflare Access headers, and ergonomic Livebook / Mix.install helpers that load an entire Infisical folder into System.put_env in a single call.
Current section
Files
Jump to
Current section
Files
lib/ex_infisical/token_cache.ex
defmodule ExInfisical.TokenCache do
@moduledoc """
GenServer that caches the bearer token returned by Universal Auth.
* Lazy: doesn't authenticate until first call.
* Re-authenticates when the token is within `:token_refresh_skew_seconds`
of expiry, or when explicitly refreshed via `refresh/1`.
* One cache per `{client_id, api_url}` key. If you call with multiple
`%Config{}`s holding different credentials, each gets its own slot.
"""
use GenServer
alias ExInfisical.{Auth, Config, Error}
@name __MODULE__
## Public API ---------------------------------------------------------------
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, name: Keyword.get(opts, :name, @name))
end
@doc "Returns a currently-valid bearer token (authenticating on demand)."
@spec token(Config.t()) :: {:ok, String.t()} | {:error, Error.t()}
def token(%Config{} = config) do
GenServer.call(@name, {:token, config, false}, timeout(config))
end
@doc "Forces re-authentication and returns the new token."
@spec refresh(Config.t()) :: {:ok, String.t()} | {:error, Error.t()}
def refresh(%Config{} = config) do
GenServer.call(@name, {:token, config, true}, timeout(config))
end
@doc "Drops every cached token. Mostly useful in tests."
@spec clear() :: :ok
def clear, do: GenServer.call(@name, :clear)
## GenServer ---------------------------------------------------------------
@impl true
def init(state), do: {:ok, state}
@impl true
def handle_call({:token, %Config{} = config, force?}, _from, state) do
key = cache_key(config)
now = DateTime.utc_now()
case Map.get(state, key) do
%{access_token: t, expires_at: exp}
when not force? ->
if DateTime.compare(exp, now) == :gt do
{:reply, {:ok, t}, state}
else
authenticate_and_cache(config, key, state)
end
_ ->
authenticate_and_cache(config, key, state)
end
end
def handle_call(:clear, _from, _state), do: {:reply, :ok, %{}}
## Helpers -----------------------------------------------------------------
defp authenticate_and_cache(config, key, state) do
case Auth.login(config) do
{:ok, %{access_token: t} = entry} ->
{:reply, {:ok, t}, Map.put(state, key, entry)}
{:error, _} = err ->
{:reply, err, Map.delete(state, key)}
end
end
defp cache_key(%Config{client_id: id, api_url: url}), do: {id, url}
defp timeout(%Config{request_timeout_ms: ms}), do: ms + 1_000
end