Main entry point for interacting with the Layr8 platform.
Lifecycle
# 1. Start the client
{:ok, client} = Layr8.Client.start_link(%{
node_url: "wss://node.example.com/plugin_socket/websocket",
api_key: "my-api-key"
})
# 2. Register message handlers BEFORE connect
: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: %{text: "pong"}}}
end)
# 3. Connect to the cloud-node
:ok = Layr8.Client.connect(client)
# 4. Send messages
:ok = Layr8.Client.send(client, %Layr8.Message{
type: "https://example.com/proto/1.0/request",
to: ["did:example:bob"],
body: %{text: "ping"}
})
# 5. Request/response
{:ok, response} = Layr8.Client.request(client, %Layr8.Message{
type: "https://example.com/proto/1.0/request",
to: ["did:example:bob"],
body: %{text: "ping"}
})
# 6. Shut down
:ok = Layr8.Client.close(client)Hosting more than one DID
connect/1 joins one DID — the one from config, or the one the node
assigns. join_did/3 hosts additional DIDs on the same WebSocket, each
with its own handlers and its own protocol subscription:
{:ok, handle} = Layr8.Client.join_did(client, workflow_did,
protocols: ["https://example.com/proto/1.0"],
handlers: %{"https://example.com/proto/1.0/request" => fn msg -> ... end},
did_spec: %{"storage" => "persistent", "controller" => owner_did}
)
:ok = Layr8.DidHandle.send(handle, %Layr8.Message{to: [peer], type: t, body: %{}})Messages arriving for a joined DID go to that DID's handlers first and to
the client-global ones (handle/3, handle_all/2) as the fallback; replies
it sends carry from = did, and the grants attached are that DID's.
Joined DIDs are re-joined automatically after a reconnect — see
Layr8.Channel.
Events
Subscribe to lifecycle events by passing callbacks in start_link/1 opts:
:on_disconnect—(reason :: term() -> any()):on_reconnect—(() -> any())
Credentials & Presentations
Credential and presentation operations use the REST API (no WebSocket required):
{:ok, jwt} = Layr8.Client.sign_credential(client, credential, issuer_did: "did:example:alice")
Summary
Functions
Returns a specification to start this module under a supervisor.
Gracefully shuts down the client.
Establishes the WebSocket connection and joins the Phoenix channel.
Returns the agent's DID — either provided in config or assigned by the node on connect.
Retrieves a stored credential by ID.
Registers a handler for a DIDComm message type.
Registers a catch-all handler invoked when no specific handler matches.
Joins an additional DID on this client's existing WebSocket connection and
returns a Layr8.DidHandle for it.
Lists the DIDs joined with join_did/3 (the connected DID is did/1).
Leaves a DID joined with join_did/3 and forgets its handlers. The
connection and every other DID stay up. No-op for a DID not joined.
Lists stored credentials for a holder. Defaults: holder = did/1.
Sets up MCP (Model Context Protocol) over DIDComm on a protocol base and
returns a binding whose Layr8.Mcp.peer/2 yields a caller.
Forgets the cached Verifiable Grants for did (default: this agent's), so the
next message re-reads them.
Sends a DIDComm message and waits for a correlated response (matched by thread ID).
request/3 from a DID joined with join_did/3. Prefer
Layr8.DidHandle.request/3.
request/3 without the raises: returns {:ok, Message.t()} or
{:error, reason} — :timeout, :not_connected, or
{:problem_report, code, comment}.
request_result/3 from a DID joined with join_did/3. Prefer
Layr8.DidHandle.request_result/3.
Sends a DIDComm message.
send/3 from a DID joined with join_did/3. Prefer Layr8.DidHandle.send/3.
Signs a W3C Verifiable Credential. Defaults: issuer = did/1, format = "compact_jwt".
Signs a W3C Verifiable Presentation. Defaults: holder = did/1, format = "compact_jwt".
Starts the Layr8 Client GenServer.
Stores a signed credential JWT for a holder. Defaults: holder = did/1.
Verifies a signed credential. Defaults: verifier = did/1.
Verifies a signed presentation. Defaults: verifier = did/1.
Types
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec close(pid()) :: :ok
Gracefully shuts down the client.
@spec connect(pid()) :: :ok
Establishes the WebSocket connection and joins the Phoenix channel.
Blocks until the join is acknowledged. Returns :ok on success or raises on error.
Returns the agent's DID — either provided in config or assigned by the node on connect.
Retrieves a stored credential by ID.
@spec handle(pid(), String.t(), Layr8.Handler.handler_fn()) :: :ok
Registers a handler for a DIDComm message type.
Must be called before connect/1. Raises Layr8.AlreadyConnectedError if called after.
Handler Signature
fn msg -> {:reply, %Layr8.Message{...}} | :noreply | :pass | {:error, reason} end
@spec handle_all(pid(), Layr8.Handler.handler_fn()) :: :ok
Registers a catch-all handler invoked when no specific handler matches.
Must be called before connect/1. Raises Layr8.AlreadyConnectedError if called after.
Handler Signature
fn msg -> {:reply, %Layr8.Message{...}} | :noreply | :pass | {:error, reason} end
@spec join_did(pid(), String.t(), keyword()) :: {:ok, Layr8.DidHandle.t()} | {:error, term()}
Joins an additional DID on this client's existing WebSocket connection and
returns a Layr8.DidHandle for it.
Must be called after connect/1 — it rides the connection that call
established. The counterpart of the node-sdk's joinDid (src/client.ts),
and the same shape mcp-pod uses to host one Instance DID per connected
account on a single socket.
Options
:protocols— this DID'spayload_typessubscription. Defaults to the protocols derived from:handlers. The problem-report protocol is appended unless already present (or a catch-all is registered), so the node can deliver denials addressed to this DID:handlers—%{msg_type => handler_fn}that fire for messages received on THIS DID; the client-global registry is the fallback:handle_all— catch-all handler for this DID:did_spec— merged over the SDK defaults, e.g.%{"storage" => "persistent", "controller" => owner_did}.controllermatters: helix enforcesissuer == resource_controlleron grants, so a DID whose controller is the node makes every grant issued by its owner fail
Returns {:error, %Layr8.NotConnectedError{}} before connect/1,
{:error, :already_joined} for a DID already hosted, and
{:error, :primary_did} for the DID connect/1 joined — that one is
reachable through send/3 and request/3 directly.
Lists the DIDs joined with join_did/3 (the connected DID is did/1).
Leaves a DID joined with join_did/3 and forgets its handlers. The
connection and every other DID stay up. No-op for a DID not joined.
Lists stored credentials for a holder. Defaults: holder = did/1.
@spec mcp(pid(), String.t()) :: {:ok, Layr8.Mcp.Binding.t()} | {:error, term()}
Sets up MCP (Model Context Protocol) over DIDComm on a protocol base and
returns a binding whose Layr8.Mcp.peer/2 yields a caller.
Must be called before connect/1 (like handle/3): it registers the
protocol subscription the cloud-node needs in order to deliver #{base}/*
replies. Idempotent per base — calling it twice returns a second binding over
the same subscription rather than raising the way a duplicate handle/3
would. Compose freely with your own handle/3 registrations.
See Layr8.Mcp for the call surface.
Forgets the cached Verifiable Grants for did (default: this agent's), so the
next message re-reads them.
The cache TTL is the whole freshness story: a grant minted seconds ago is invisible until it lapses. An agent that has just been TOLD it was granted something — by a request/approve flow, or by a person on the other end of a chat — should not have to wait out a timer it cannot see.
@spec request(pid(), Layr8.Message.t() | map(), keyword()) :: {:ok, Layr8.Message.t()}
Sends a DIDComm message and waits for a correlated response (matched by thread ID).
Returns {:ok, Layr8.Message.t()} or raises Layr8.ProblemReportError / Layr8.NotConnectedError.
Options
:timeout— milliseconds to wait for a reply (default 30s):parent_thread— setpthidfor nested thread correlation
@spec request_from(pid(), String.t(), Layr8.Message.t() | map(), keyword()) :: {:ok, Layr8.Message.t()}
request/3 from a DID joined with join_did/3. Prefer
Layr8.DidHandle.request/3.
@spec request_result(pid(), Layr8.Message.t() | map(), keyword()) :: {:ok, Layr8.Message.t()} | {:error, term()}
request/3 without the raises: returns {:ok, Message.t()} or
{:error, reason} — :timeout, :not_connected, or
{:problem_report, code, comment}.
Same request path, same options. This exists because a remote call failing is
an ordinary outcome for some callers (Layr8.Mcp routes on it rather than
rescuing), while for others an unanswered request is exceptional — the SDK
should not force either one to convert.
@spec request_result_from(pid(), String.t(), Layr8.Message.t() | map(), keyword()) :: {:ok, Layr8.Message.t()} | {:error, term()}
request_result/3 from a DID joined with join_did/3. Prefer
Layr8.DidHandle.request_result/3.
@spec send(pid(), Layr8.Message.t() | map(), keyword()) :: :ok
Sends a DIDComm message.
By default waits for server acknowledgment. Pass fire_and_forget: true to skip.
Returns :ok or raises Layr8.NotConnectedError.
@spec send_from(pid(), String.t(), Layr8.Message.t() | map(), keyword()) :: :ok
send/3 from a DID joined with join_did/3. Prefer Layr8.DidHandle.send/3.
Signs a W3C Verifiable Credential. Defaults: issuer = did/1, format = "compact_jwt".
Signs a W3C Verifiable Presentation. Defaults: holder = did/1, format = "compact_jwt".
@spec start_link(start_opts()) :: GenServer.on_start()
Starts the Layr8 Client GenServer.
Accepts the same keys as Layr8.Config.resolve!/1 plus:
:on_disconnect— called with the disconnect reason:on_reconnect— called after successful reconnection:on_grant_miss— called when a message went out with NO covering Verifiable Grant and the node then denied it, when the covering set had to be capped, or when the grants could not be read at all. SeeLayr8.Wallet: the sender is the only party that knows nothing was attached, and the node's denial names the grant it could not find, which sends people to check a grant that is fine.
Raises Layr8.Error if required config fields are missing.
Stores a signed credential JWT for a holder. Defaults: holder = did/1.
Verifies a signed credential. Defaults: verifier = did/1.
Verifies a signed presentation. Defaults: verifier = did/1.