defmodule Dialup.Auth.Grants do @moduledoc """ Helpers for scoping MCP grants based on authentication state. Unauthenticated sessions receive minimal read-only capabilities. After login, pages should extend this base grant with role-specific capabilities in `agent_grant/1`. """ @minimal_capabilities [:read_scene, :issue_browser_url] @minimal_projections [:state, :actions] @doc "Base grant for unauthenticated sessions (agent-first / pre-login)." @spec guest_grant() :: map() def guest_grant do %{ capabilities: @minimal_capabilities, projections: @minimal_projections, expires_in: :timer.minutes(15), require_version: true } end @doc """ Returns guest grant when `current_user` is absent, otherwise `logged_in_grant/1`. Pages can pass extra capabilities to merge when authenticated: def agent_grant(assigns) do Dialup.Auth.Grants.for_user(assigns, capabilities: [:add_item]) end """ @spec for_user(map(), keyword()) :: map() def for_user(assigns, opts \\ []) def for_user(%{current_user: user}, opts) when not is_nil(user) do logged_in_grant(user, opts) end def for_user(%{dialup_auth_enabled?: true}, _opts), do: guest_grant() def for_user(_assigns, _opts), do: logged_in_grant(nil, []) @doc "Default authenticated grant; override capabilities via opts." @spec logged_in_grant(map() | struct(), keyword()) :: map() def logged_in_grant(_user, opts) do capabilities = Keyword.get(opts, :capabilities, :all) projections = Keyword.get(opts, :projections, [:state, :regions, :actions]) %{ capabilities: capabilities, projections: projections, expires_in: Keyword.get(opts, :expires_in, :timer.minutes(15)), require_version: Keyword.get(opts, :require_version, true) } end end