Files
self 157106aa66
CI / Test (push) Failing after 16s
feat: prefix Trust plugin slug
Rename the Trust plugin identity to tribe-one-trust and update config namespace/docs accordingly.
2026-06-17 22:33:43 +02:00

333 lines
12 KiB
Elixir

defmodule TribeOne.TribesPlugin.Trust do
@moduledoc false
alias TribeOne.TribesPlugin.Trust.Domain
@federation_capabilities [
"org.tribes.federation.hello@1",
"org.tribes.federation.relationship.status@1",
"org.tribes.trust.attest@1"
]
def federation_capabilities, do: @federation_capabilities
def federation_hidden? do
case Tribes.ConfigStore.get_for_plugin("tribe-one-trust", "federation.hidden", default: false) do
{:ok, value} -> value in [true, "true", "1", "on"]
{:error, _reason} -> false
end
end
def set_federation_hidden?(value, opts \\ []) when is_boolean(value) do
Tribes.ConfigStore.put_for_plugin("tribe-one-trust", "federation.hidden", value, opts)
end
def receive_hello(attrs, opts \\ []) when is_map(attrs) do
with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs),
{:ok, relationship_attrs} <- normalize_inbound_relationship_attrs(attrs),
{:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)),
{:ok, relationship} <-
upsert_inbound_relationship(tribe.pubkey, relationship_attrs, opts) do
{:ok, %{tribe: tribe, relationship: relationship}}
end
end
def observe_remote_tribe(attrs, opts \\ []) when is_map(attrs) do
with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs),
{:ok, relationship_attrs} <- normalize_observed_relationship_attrs(attrs),
{:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)),
{:ok, relationship} <-
ensure_observed_relationship(tribe.pubkey, relationship_attrs, opts) do
{:ok, %{tribe: tribe, relationship: relationship}}
end
end
def list_relationship_rows(opts \\ []) do
with {:ok, tribes} <- Domain.list_remote_tribes(default_trust_opts(opts)),
{:ok, relationships} <- Domain.list_tribe_relationships(default_trust_opts(opts)) do
tribes_by_pubkey = Map.new(tribes, &{&1.pubkey, &1})
rows =
relationships
|> Enum.map(fn relationship ->
%{
tribe: Map.get(tribes_by_pubkey, relationship.remote_tribe_pubkey),
relationship: relationship
}
end)
|> Enum.sort_by(fn %{relationship: relationship} ->
{status_rank(relationship.status), relationship.updated_at || relationship.inserted_at}
end)
{:ok, rows}
end
end
def access_trust_facts(%{type: "tribe", id: pubkey}), do: access_trust_facts(pubkey)
def access_trust_facts(%{"type" => "tribe", "id" => pubkey}), do: access_trust_facts(pubkey)
def access_trust_facts(pubkey) when is_binary(pubkey) do
with {:ok, pubkey} <- normalize_pubkey(pubkey),
{:ok, relationship} <-
Domain.get_tribe_relationship(
pubkey,
Keyword.put(default_trust_opts([]), :not_found_error?, false)
) do
{:ok,
%{
status: relationship_status(relationship),
trust_score: relationship_trust_score(relationship),
granted_capabilities: relationship_capabilities(relationship)
}}
end
end
def access_trust_facts(_subject), do: {:error, :unsupported_subject}
def update_relationship(remote_pubkey, attrs, opts \\ []) when is_binary(remote_pubkey) do
with {:ok, pubkey} <- normalize_pubkey(remote_pubkey),
{:ok, existing} <-
Domain.get_tribe_relationship(
pubkey,
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
),
{:ok, attrs} <- normalize_relationship_update(attrs, existing) do
Domain.upsert_tribe_relationship(attrs, default_trust_opts(opts))
end
end
defp upsert_inbound_relationship(remote_pubkey, attrs, opts) do
existing =
case Domain.get_tribe_relationship(
remote_pubkey,
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
) do
{:ok, relationship} -> relationship
{:error, _reason} -> nil
end
status =
case existing do
%{status: status} when status in [:active, :blocked, :revoked] -> status
_other -> :pending_inbound
end
attrs
|> Map.merge(%{remote_tribe_pubkey: remote_pubkey, status: status})
|> Domain.upsert_tribe_relationship(default_trust_opts(opts))
end
defp ensure_observed_relationship(remote_pubkey, attrs, opts) do
case Domain.get_tribe_relationship(
remote_pubkey,
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
) do
{:ok, nil} ->
Domain.upsert_tribe_relationship(
Map.merge(
%{
remote_tribe_pubkey: remote_pubkey,
status: :observed,
requested_capabilities: [],
granted_capabilities: [],
trust_score: 0
},
attrs
),
default_trust_opts(opts)
)
{:ok, relationship} ->
{:ok, relationship}
{:error, _reason} = error ->
error
end
end
defp normalize_remote_tribe_attrs(attrs) do
with {:ok, pubkey} <-
normalize_pubkey(attrs["from_tribe"] || attrs["pubkey"] || attrs[:pubkey]) do
{:ok,
%{
pubkey: pubkey,
name: normalize_optional_string(attrs["name"] || attrs[:name]) || pubkey,
description: normalize_optional_string(attrs["description"] || attrs[:description]),
last_seen_at: DateTime.utc_now()
}}
end
end
defp normalize_inbound_relationship_attrs(attrs) do
with {:ok, requested_capabilities} <-
normalize_string_list(
attrs["requested_capabilities"] || attrs[:requested_capabilities] || []
),
{:ok, remote_relay_urls} <-
normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []),
{:ok, remote_capabilities} <-
normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do
{:ok,
%{
remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]),
remote_profile_url:
normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]),
remote_relay_urls: remote_relay_urls,
remote_capabilities: remote_capabilities,
requested_capabilities: requested_capabilities,
inbound_message: normalize_optional_string(attrs["message"] || attrs[:message]),
last_inbound_at: DateTime.utc_now()
}}
end
end
defp normalize_observed_relationship_attrs(attrs) do
with {:ok, remote_relay_urls} <-
normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []),
{:ok, remote_capabilities} <-
normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do
{:ok,
%{
remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]),
remote_profile_url:
normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]),
remote_relay_urls: remote_relay_urls,
remote_capabilities: remote_capabilities
}}
end
end
defp normalize_relationship_update(_attrs, nil), do: {:error, :relationship_not_found}
defp normalize_relationship_update(attrs, existing) do
with {:ok, status} <- normalize_status(attrs["status"] || attrs[:status] || existing.status),
{:ok, trust_score} <-
normalize_trust_score(
attrs["trust_score"] || attrs[:trust_score] || existing.trust_score
),
{:ok, granted_capabilities} <-
normalize_capability_input(
attrs["granted_capabilities"] || attrs[:granted_capabilities] ||
existing.granted_capabilities || []
) do
{:ok,
%{
remote_tribe_pubkey: existing.remote_tribe_pubkey,
status: status,
remote_api_url: existing.remote_api_url,
remote_profile_url: existing.remote_profile_url,
remote_relay_urls: existing.remote_relay_urls || [],
remote_capabilities: existing.remote_capabilities || [],
requested_capabilities: existing.requested_capabilities || [],
granted_capabilities: granted_capabilities,
trust_score: trust_score,
trust_note:
normalize_optional_string(
attrs["trust_note"] || attrs[:trust_note] || existing.trust_note
),
inbound_message: existing.inbound_message,
last_inbound_at: existing.last_inbound_at,
last_outbound_at: existing.last_outbound_at
}}
end
end
defp normalize_capability_input(value) when is_binary(value) do
value
|> String.split([",", "\n"], trim: true)
|> Enum.map(&String.trim/1)
|> normalize_string_list()
end
defp normalize_capability_input(value), do: normalize_string_list(value)
defp normalize_pubkey(value) when is_binary(value) do
pubkey = value |> String.trim() |> String.downcase()
if String.match?(pubkey, ~r/\A[0-9a-f]{64}\z/) do
{:ok, pubkey}
else
{:error, :invalid_pubkey}
end
end
defp normalize_pubkey(_value), do: {:error, :invalid_pubkey}
defp normalize_status(:observed), do: {:ok, :observed}
defp normalize_status(:pending_inbound), do: {:ok, :pending_inbound}
defp normalize_status(:pending_outbound), do: {:ok, :pending_outbound}
defp normalize_status(:active), do: {:ok, :active}
defp normalize_status(:blocked), do: {:ok, :blocked}
defp normalize_status(:revoked), do: {:ok, :revoked}
defp normalize_status("observed"), do: {:ok, :observed}
defp normalize_status("pending_inbound"), do: {:ok, :pending_inbound}
defp normalize_status("pending_outbound"), do: {:ok, :pending_outbound}
defp normalize_status("active"), do: {:ok, :active}
defp normalize_status("blocked"), do: {:ok, :blocked}
defp normalize_status("revoked"), do: {:ok, :revoked}
defp normalize_status(_status), do: {:error, :invalid_status}
defp normalize_trust_score(value) when is_integer(value) and value >= -100 and value <= 100,
do: {:ok, value}
defp normalize_trust_score(value) when is_binary(value) do
case Integer.parse(String.trim(value)) do
{score, ""} -> normalize_trust_score(score)
_other -> {:error, :invalid_trust_score}
end
end
defp normalize_trust_score(_value), do: {:error, :invalid_trust_score}
defp normalize_string_list(values) when is_list(values) do
values
|> Enum.reduce_while({:ok, []}, fn
value, {:ok, acc} when is_binary(value) ->
trimmed = String.trim(value)
next = if trimmed == "", do: acc, else: [trimmed | acc]
{:cont, {:ok, next}}
_value, _acc ->
{:halt, {:error, :invalid_string_list}}
end)
|> case do
{:ok, values} -> {:ok, Enum.reverse(values)}
{:error, _reason} = error -> error
end
end
defp normalize_string_list(_values), do: {:error, :invalid_string_list}
defp normalize_optional_string(nil), do: nil
defp normalize_optional_string(value) when is_binary(value) do
case String.trim(value) do
"" -> nil
trimmed -> trimmed
end
end
defp normalize_optional_string(_value), do: nil
defp status_rank(:pending_inbound), do: 0
defp status_rank(:pending_outbound), do: 1
defp status_rank(:active), do: 2
defp status_rank(:observed), do: 3
defp status_rank(:blocked), do: 4
defp status_rank(:revoked), do: 5
defp status_rank(_status), do: 6
defp relationship_status(nil), do: "unknown"
defp relationship_status(%{status: status}), do: to_string(status)
defp relationship_trust_score(nil), do: nil
defp relationship_trust_score(%{trust_score: trust_score}), do: trust_score
defp relationship_capabilities(nil), do: []
defp relationship_capabilities(%{granted_capabilities: capabilities}), do: capabilities || []
defp default_trust_opts(opts) do
Keyword.put_new(opts, :context, %{private: %{system?: true, system_purpose: :trust_plugin}})
end
end