Packages

fnord

0.9.40

AI code archaeology

Current section

Files

Jump to
fnord lib mcp oauth2 credentials_store.ex
Raw

lib/mcp/oauth2/credentials_store.ex

defmodule MCP.OAuth2.CredentialsStore do
@moduledoc """
Minimal credentials store for OAuth2 tokens.
- JSON file at `~/.fnord/credentials.json`
- Atomic writes (`.tmp` + rename), final perms set to 0600
- Per-server entries under the `"servers"` key
MVP: no encryption; keep tokens out of logs; caller is responsible for locking if needed.
"""
@default_rel ".fnord/credentials.json"
@spec path() :: String.t()
def path do
Path.join(Settings.get_user_home(), @default_rel)
end
@spec read(String.t()) :: {:ok, map()} | {:error, :not_found | term()}
def read(server) do
with {:ok, %{"servers" => servers}} <- read_all(),
entry when is_map(entry) <- Map.get(servers, server) do
{:ok, entry}
else
_ -> {:error, :not_found}
end
end
@spec write(String.t(), map()) :: :ok | {:error, term()}
def write(server, token_map) when is_map(token_map) do
base =
case read_all() do
{:ok, m} -> m
_ -> %{}
end
servers = base |> Map.get("servers", %{}) |> Map.put(server, token_map)
updated = %{"servers" => servers, "last_updated" => System.os_time(:second)}
atomic_write(updated)
end
@spec delete(String.t()) :: :ok | {:error, term()}
def delete(server) do
base =
case read_all() do
{:ok, m} -> m
_ -> %{}
end
servers = base |> Map.get("servers", %{}) |> Map.delete(server)
updated = %{"servers" => servers, "last_updated" => System.os_time(:second)}
atomic_write(updated)
end
@spec read_all() :: {:ok, map()} | {:error, term()}
def read_all do
p = path()
case File.read(p) do
{:ok, body} -> SafeJson.decode(body)
{:error, :enoent} -> {:ok, %{}}
{:error, e} -> {:error, e}
end
end
defp atomic_write(map) do
p = path()
dir = Path.dirname(p)
tmp = p <> ".tmp"
File.mkdir_p!(dir)
body = SafeJson.encode!(map, pretty: true)
with :ok <- File.write(tmp, body),
:ok <- File.rename(tmp, p),
:ok <- File.chmod(p, 0o600) do
:ok
else
err ->
_ = File.rm(tmp)
err
end
end
end