Layr8 Elixir SDK

Elixir client for the Layr8 decentralized identity-native messaging network.

Full documentation at docs.layr8.io/build/elixir-sdk

Installation

Add to your mix.exs dependencies:

def deps do
  [{:layr8, github: "layr8/elixir_sdk"}]
end

Requires Elixir ~> 1.15.

Quick Start

{:ok, client} = Layr8.Client.start_link(%{
  node_url: "wss://node.example.com/plugin_socket/websocket",
  api_key: System.fetch_env!("LAYR8_API_KEY")
})

:ok = Layr8.Client.handle(client, "https://example.com/proto/1.0/request", fn msg ->
  {:reply, %Layr8.Message{
    type: "https://example.com/proto/1.0/response",
    body: msg.body
  }}
end)

:ok = Layr8.Client.connect(client)

Configuration

Fields can be provided explicitly or resolved from environment variables:

Field Env Variable Required Description
node_urlLAYR8_NODE_URL Yes WebSocket URL of the cloud-node
api_keyLAYR8_API_KEY Yes Authentication key
agent_didLAYR8_AGENT_DID No Agent DID (ephemeral if omitted)

HTTP(S) URLs are automatically normalized (https:// to wss://, http:// to ws://).

Message Handlers

Register handlers before calling connect/1. Each handler receives a Layr8.Message and returns one of:

:ok = Layr8.Client.handle(client, "https://example.com/proto/1.0/request", fn msg ->
  {:reply, %Layr8.Message{type: "https://example.com/proto/1.0/response", body: %{"ok" => true}}}
end)

Reply messages auto-populate id, from, to, and thread_id from the inbound message.

Wildcard Handler

Register a catch-all handler for messages that don't match any specific type:

:ok = Layr8.Client.handle_all(client, fn msg ->
  Logger.info("Unhandled message type: #{msg.type}")
  :pass
end)

Dispatch priority: specific handler > catch-all > auto-pass.

Sending Messages

# Fire-and-wait (default: waits for server ack)
:ok = Layr8.Client.send(client, %Layr8.Message{
  type: "https://example.com/proto/1.0/request",
  to: ["did:example:bob"],
  body: %{"text" => "hello"}
})

# Fire-and-forget
:ok = Layr8.Client.send(client, msg, fire_and_forget: true)

Request/Response

Send a message and block until a correlated response arrives (matched by thid):

{:ok, response} = Layr8.Client.request(client, %Layr8.Message{
  type: "https://example.com/proto/1.0/request",
  to: ["did:example:bob"],
  body: %{"text" => "ping"}
}, timeout: 10_000)

<<<<<<< HEAD

Configuration

Configuration can be provided explicitly or via environment variables:

Field Env Variable Required Description
node_urlLAYR8_NODE_URL Yes WebSocket URL of the cloud-node
api_keyLAYR8_API_KEY Yes Authentication key
agent_didLAYR8_AGENT_DID No Agent DID (ephemeral if omitted)

HTTP(S) URLs are automatically normalized to WebSocket scheme:

# All from environment variables
{:ok, client} = Layr8.Client.start_link(%{})

# Explicit values override env vars
{:ok, client} = Layr8.Client.start_link(%{
  node_url: "wss://node.example.com/plugin_socket/websocket",
  api_key: "my-api-key",
  agent_did: "did:key:z6Mk..."
})

Protocol Registration

The SDK automatically derives protocol base URIs from registered handler message types and sends them to the cloud-node on connect. For example, handling "https://example.com/proto/1.0/request" registers the protocol "https://example.com/proto/1.0".

Note: The cloud-node requires at least one protocol on join. Unlike the Node and Go SDKs, the Elixir SDK does not auto-add the problem report protocol. Sender-only clients that don't register any handlers will fail to connect. Register at least one handler before connecting.

Message Handlers

Handlers are registered before connect/1 and called when inbound DIDComm messages arrive.

Return Values

Return value Effect
{:reply, message} Send a response to the sender
:noreply No response; message consumed

Manual Acknowledgment

By default messages are auto-acknowledged before the handler runs. For manual control:

Layr8.Client.handle(client, "https://example.com/proto/1.0/request", fn msg ->
  # Do your work, then ack manually (coming in a future version)
  :noreply
end, manual_ack: true)

Request/Response Pattern

Use Layr8.Client.request/3 to send a message and wait for a correlated response. Responses are matched by thid (thread ID).

Options: :timeout (default 30s), :parent_thread (sets pthid).

case Layr8.Client.request(client, msg, timeout: 10_000) do
  {:ok, response} ->
    IO.inspect(response.body)
  # Raises on error:
  # - Layr8.ProblemReportError — remote agent sent a problem report
  # - Layr8.NotConnectedError  — not connected
  # - Layr8.Error              — timeout or other error
end

W3C Verifiable Credentials

Credential operations use the REST API (work without a WebSocket connection):

{:ok, jwt} = Layr8.Client.sign_credential(client, %{
  "credentialSubject" => %{"id" => "did:example:bob", "name" => "Bob"}
}, issuer_did: "did:example:alice", format: "compact_jwt")

{:ok, verified} = Layr8.Client.verify_credential(client, jwt)

{:ok, stored} = Layr8.Client.store_credential(client, jwt)
{:ok, creds}  = Layr8.Client.list_credentials(client)
{:ok, cred}   = Layr8.Client.get_credential(client, stored["id"])

W3C Verifiable Presentations

{:ok, vp_jwt} = Layr8.Client.sign_presentation(client, [vc_jwt],
  nonce: "challenge-123", format: "compact_jwt")

{:ok, verified} = Layr8.Client.verify_presentation(client, vp_jwt)

Connection Lifecycle

The cloud-node assigns an ephemeral DID on connect when no agent_did is configured. Retrieve it with Layr8.Client.did/1.

The channel auto-reconnects with exponential backoff (1s to 30s). Subscribe to lifecycle events:

{:ok, client} = Layr8.Client.start_link(%{
  on_disconnect: fn reason -> Logger.warning("Disconnected: #{inspect(reason)}") end,
  on_reconnect: fn -> Logger.info("Reconnected") end
})

Error Handling

All errors are exceptions under the Layr8 namespace:

Exception Raised when
Layr8.Error General SDK error (missing config, send failure)
Layr8.ConnectionError WebSocket connection fails
Layr8.NotConnectedErrorsend/3 or request/3 called before connect/1
Layr8.AlreadyConnectedErrorhandle/4 called after connect/1
Layr8.ClientClosedErrorconnect/1 called after close/1
Layr8.ProblemReportError Remote agent sends a DIDComm problem report

Examples

See examples/echo_agent.ex for a standalone echo agent.

Development

mix deps.get
mix test
mix check          # format + compile warnings + test
mix docs           # generate ExDoc documentation

Architecture

Layr8.Client (GenServer)
  Layr8.Config        -- config resolution and URL normalization
  Layr8.Handler       -- message type -> handler registry
  Layr8.Message       -- DIDComm v2 message struct + marshal/parse
  Layr8.Attachment    -- DIDComm v2 attachment struct
  Layr8.Channel       -- Phoenix Channel WebSocket transport (GenServer + WebSockex)
  Layr8.REST          -- HTTP client for REST API (Req)
  Layr8.Credentials   -- W3C Verifiable Credential operations
  Layr8.Presentations -- W3C Verifiable Presentation operations

Links

License

MIT