Skip to main content

README.md

# Layr8 Elixir SDK

Elixir client for the [Layr8](https://layr8.io) decentralized identity-native messaging network.

Full documentation at [docs.layr8.io/build/elixir-sdk](https://docs.layr8.io/build/elixir-sdk)

## Installation

Add to your `mix.exs` dependencies:

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

Requires Elixir ~> 1.15.

## Quick Start

```elixir
{: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_url`  | `LAYR8_NODE_URL`   | Yes      | WebSocket URL of the cloud-node |
| `api_key`   | `LAYR8_API_KEY`    | Yes      | Authentication key              |
| `agent_did` | `LAYR8_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:

- `{:reply, message}` — send a reply and mark as handled
- `:noreply` — mark as handled with no reply
- `:pass` — decline to handle; the cloud-node may route elsewhere
- `{:error, reason}` — signal an error to the cloud-node

```elixir
: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:

```elixir
: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

```elixir
# 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`):

```elixir
{: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_url`  | `LAYR8_NODE_URL`  | Yes      | WebSocket URL of the cloud-node         |
| `api_key`   | `LAYR8_API_KEY`   | Yes      | Authentication key                       |
| `agent_did` | `LAYR8_AGENT_DID` | No       | Agent DID (ephemeral if omitted)         |

HTTP(S) URLs are automatically normalized to WebSocket scheme:
- `https://``wss://`
- `http://``ws://`

```elixir
# 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:

```elixir
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`).

```elixir
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):

```elixir
{: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

```elixir
{: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:

```elixir
{: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.NotConnectedError`     | `send/3` or `request/3` called before `connect/1`  |
| `Layr8.AlreadyConnectedError` | `handle/4` called after `connect/1`                |
| `Layr8.ClientClosedError`     | `connect/1` called after `close/1`                 |
| `Layr8.ProblemReportError`    | Remote agent sends a DIDComm problem report        |

## Examples

See [`examples/echo_agent.ex`](examples/echo_agent.ex) for a standalone echo agent.

## Development

```sh
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

- [Layr8 Documentation](https://docs.layr8.io)
- [Elixir SDK Docs](https://docs.layr8.io/build/elixir-sdk)
- [DIDComm v2 Spec](https://identity.foundation/didcomm-messaging/spec/)
- [GitHub](https://github.com/layr8/elixir_sdk)

## License

MIT