Packages

Elixir SDK for the Layr8 decentralized identity-native messaging network

Current section

Files

Jump to
layr8 lib layr8 message.ex
Raw

lib/layr8/message.ex

defmodule Layr8.Message.Context do
@moduledoc """
Metadata from the cloud-node attached to inbound messages.
"""
defstruct recipient: "", authorized: false, sender_credentials: []
@type sender_credential :: %{id: String.t(), name: String.t()}
@type t :: %__MODULE__{
recipient: String.t(),
authorized: boolean(),
sender_credentials: [sender_credential()]
}
end
defmodule Layr8.Message do
@moduledoc """
A DIDComm v2 message.
## Fields
- `id` — unique message identifier (UUID v4)
- `type` — DIDComm message type URI
- `from` — sender DID
- `to` — list of recipient DIDs
- `thread_id` — correlates messages in a thread (`thid`)
- `parent_thread_id` — links to a parent thread (`pthid`)
- `body` — message payload (arbitrary map)
- `attachments` — list of `Layr8.Attachment` structs (DIDComm v2 attachments)
- `context` — inbound-only metadata from the cloud-node
## Wire Format (outbound)
{
"id": "...",
"type": "...",
"from": "...",
"to": [...],
"thid": "...",
"pthid": "...",
"body": {...},
"attachments": [...]
}
## Wire Format (inbound, from cloud-node)
{
"context": {"recipient": "...", "authorized": true, "sender_credentials": [...]},
"plaintext": {"id": "...", "type": "...", "from": "...", "to": [...], "body": {...}, "thid": "...", "pthid": "..."}
}
"""
defstruct id: "",
type: "",
from: "",
to: [],
thread_id: "",
parent_thread_id: "",
body: nil,
attachments: [],
context: nil
@type t :: %__MODULE__{
id: String.t(),
type: String.t(),
from: String.t(),
to: [String.t()],
thread_id: String.t(),
parent_thread_id: String.t(),
body: map() | nil,
attachments: [Layr8.Attachment.t()],
context: Layr8.Message.Context.t() | nil
}
@doc """
Generate a new unique message ID (UUID v4).
"""
@spec generate_id() :: String.t()
def generate_id do
<<a1::32, a2::16, _::4, a3::12, _::2, a4::6, a5::8, a6::48>> =
:crypto.strong_rand_bytes(16)
# Set version bits (v4) and variant bits (10xx)
version = 4
variant = Bitwise.bor(0x80, Bitwise.band(a4, 0x3F))
:io_lib.format(
"~8.16.0b-~4.16.0b-~1.16.0b~3.16.0b-~2.16.0b~2.16.0b-~12.16.0b",
[a1, a2, version, a3, variant, a5, a6]
)
|> IO.iodata_to_binary()
end
@doc """
Serializes a `Layr8.Message` into a DIDComm JSON envelope map.
The result is suitable for encoding with `Jason.encode!/1`.
## Example
msg = %Layr8.Message{id: "abc", type: "...", from: "did:ex:alice", to: ["did:ex:bob"], body: %{text: "hi"}}
Layr8.Message.marshal(msg)
# => %{"id" => "abc", "type" => "...", "from" => "did:ex:alice", "to" => ["did:ex:bob"], "body" => %{text: "hi"}}
"""
@spec marshal(t()) :: map()
def marshal(%__MODULE__{} = msg) do
env = %{
"id" => msg.id,
"type" => msg.type,
"from" => msg.from,
"to" => msg.to,
"body" => msg.body || %{}
}
env =
if msg.thread_id != "" and not is_nil(msg.thread_id),
do: Map.put(env, "thid", msg.thread_id),
else: env
env =
if msg.parent_thread_id != "" and not is_nil(msg.parent_thread_id),
do: Map.put(env, "pthid", msg.parent_thread_id),
else: env
env =
case msg.attachments do
[] -> env
nil -> env
atts -> Map.put(env, "attachments", Enum.map(atts, &Layr8.Attachment.marshal/1))
end
env
end
@doc """
Parses an inbound cloud-node message envelope (with context + plaintext) into a `Layr8.Message`.
Accepts a map (already JSON-decoded) or a JSON string.
## Example
envelope = %{
"context" => %{"recipient" => "did:ex:bob", "authorized" => true, "sender_credentials" => []},
"plaintext" => %{"id" => "abc", "type" => "...", "from" => "did:ex:alice", "to" => ["did:ex:bob"], "body" => %{}}
}
Layr8.Message.parse(envelope)
"""
@spec parse(map() | String.t()) :: {:ok, t()} | {:error, term()}
def parse(data) when is_binary(data) do
case Jason.decode(data) do
{:ok, map} -> parse(map)
{:error, _} = err -> err
end
end
def parse(data) when is_map(data) do
pt = Map.get(data, "plaintext", %{})
msg = %__MODULE__{
id: Map.get(pt, "id", ""),
type: Map.get(pt, "type", ""),
from: Map.get(pt, "from", ""),
to: Map.get(pt, "to", []),
thread_id: Map.get(pt, "thid", ""),
parent_thread_id: Map.get(pt, "pthid", ""),
body: Map.get(pt, "body", nil),
attachments: parse_attachments(Map.get(pt, "attachments", [])),
context: parse_context(Map.get(data, "context"))
}
{:ok, msg}
end
def parse(_), do: {:error, "invalid message format"}
# ---
defp parse_attachments(list) when is_list(list) do
Enum.map(list, &Layr8.Attachment.parse/1)
end
defp parse_attachments(_), do: []
defp parse_context(nil), do: nil
defp parse_context(ctx) when is_map(ctx) do
raw_creds = Map.get(ctx, "sender_credentials", [])
creds =
Enum.map(raw_creds, fn c ->
subj = Map.get(c, "credential_subject", %{})
%{id: Map.get(subj, "id", ""), name: Map.get(subj, "name", "")}
end)
%Layr8.Message.Context{
recipient: Map.get(ctx, "recipient", ""),
authorized: Map.get(ctx, "authorized", false),
sender_credentials: creds
}
end
end