Current section

Files

Jump to
nostr_lib lib nostr event http_auth.ex
Raw

lib/nostr/event/http_auth.ex

defmodule Nostr.Event.HttpAuth do
@moduledoc """
HTTP authentication events (kind 27235).
Implements NIP-98: https://github.com/nostr-protocol/nips/blob/master/98.md
Required tags:
- `u` - absolute request URL
- `method` - HTTP method
Optional tags:
- `payload` - request payload SHA256 hash (hex)
"""
@moduledoc tags: [:event, :nip98], nip: 98
alias Nostr.Event
alias Nostr.Tag
@kind 27_235
defstruct [:event, :url, :method, :payload]
@type t() :: %__MODULE__{
event: Event.t(),
url: String.t() | nil,
method: String.t() | nil,
payload: String.t() | nil
}
@doc """
Parses a kind 27235 event into an `HttpAuth` struct.
"""
@spec parse(Event.t()) :: t() | {:error, String.t(), Event.t()}
def parse(%Event{kind: @kind} = event) do
with {:ok, url} <- required_single_tag(event.tags, :u, :missing_u_tag, :duplicate_u_tag),
{:ok, method} <-
required_single_tag(event.tags, :method, :missing_method_tag, :duplicate_method_tag),
{:ok, payload} <- optional_single_tag(event.tags, :payload, :duplicate_payload_tag),
:ok <- validate_payload_tag(payload) do
%__MODULE__{event: event, url: url, method: method, payload: payload}
else
{:error, reason} -> {:error, error_message(reason), event}
end
end
def parse(%Event{} = event) do
{:error, "Event is not an HTTP auth event (expected kind 27235)", event}
end
@doc """
Creates a new HTTP auth event.
## Options
- `:payload` - raw request body to hash into a `payload` tag
- `:payload_hash` - precomputed SHA256 hex for the `payload` tag
- `:pubkey` - event pubkey (optional)
- `:created_at` - event timestamp (optional)
- `:tags` - additional tags to append (optional)
- `:content` - event content (defaults to empty string)
"""
@spec create(String.t(), String.t(), Keyword.t()) :: t()
def create(url, method, opts \\ []) when is_binary(url) and is_binary(method) do
payload_hash =
case {Keyword.get(opts, :payload_hash), Keyword.get(opts, :payload)} do
{hash, _payload} when is_binary(hash) -> String.downcase(hash)
{nil, payload} when is_binary(payload) -> payload_hash(payload)
_none -> nil
end
required_tags =
[Tag.create(:u, url), Tag.create(:method, method)] ++ maybe_payload_tag(payload_hash)
event_opts =
opts
|> Keyword.take([:pubkey, :created_at, :content, :tags])
|> Keyword.put_new(:content, "")
|> Keyword.update(:tags, required_tags, &(required_tags ++ &1))
event = Event.create(@kind, event_opts)
%__MODULE__{event: event, url: url, method: method, payload: payload_hash}
end
# Private helpers
defp required_single_tag(tags, type, missing_reason, duplicate_reason) do
case tags
|> Enum.filter(&(&1.type == type and is_binary(&1.data)))
|> Enum.map(& &1.data) do
[] -> {:error, missing_reason}
[value] -> {:ok, value}
[_first | _rest] -> {:error, duplicate_reason}
end
end
defp optional_single_tag(tags, type, duplicate_reason) do
case tags
|> Enum.filter(&(&1.type == type and is_binary(&1.data)))
|> Enum.map(& &1.data) do
[] -> {:ok, nil}
[value] -> {:ok, String.downcase(value)}
[_first | _rest] -> {:error, duplicate_reason}
end
end
defp validate_payload_tag(nil), do: :ok
defp validate_payload_tag(payload) when is_binary(payload) do
if valid_sha256_hex?(payload), do: :ok, else: {:error, :invalid_payload_tag}
end
defp maybe_payload_tag(nil), do: []
defp maybe_payload_tag(payload_hash), do: [Tag.create(:payload, payload_hash)]
defp payload_hash(payload) when is_binary(payload) do
:sha256
|> :crypto.hash(payload)
|> Base.encode16(case: :lower)
end
defp valid_sha256_hex?(hash) when is_binary(hash) and byte_size(hash) == 64 do
case Base.decode16(hash, case: :mixed) do
{:ok, decoded} -> byte_size(decoded) == 32
:error -> false
end
end
defp valid_sha256_hex?(_hash), do: false
defp error_message(:missing_u_tag), do: "HTTP auth event must have exactly one u tag"
defp error_message(:missing_method_tag), do: "HTTP auth event must have exactly one method tag"
defp error_message(:duplicate_u_tag), do: "HTTP auth event must not contain multiple u tags"
defp error_message(:duplicate_method_tag),
do: "HTTP auth event must not contain multiple method tags"
defp error_message(:duplicate_payload_tag),
do: "HTTP auth event must not contain multiple payload tags"
defp error_message(:invalid_payload_tag),
do: "HTTP auth payload tag must be a 64-char SHA256 hex"
end