# Changelog

All notable changes to `layr8`. Format loosely follows [Keep a Changelog](https://keepachangelog.com/); versioning follows [SemVer](https://semver.org/).

This file starts here. Earlier releases (through 0.2.8) are recorded only in git history.

## [0.2.13] - 2026-09-04

### Fixed

- **Mediation now binds both protocols at join.** A mediated client bound only
  `messagepickup/3.0` (via the delivery handler) and relied on the cloud-node
  negotiating `reply_protocol/1` to deliver the `coordinate-mediation/3.0`
  mediate-grant. Against a node that does not negotiate it, the grant reply was
  dropped and enrolment timed out. `connect/1` now also subscribes to
  `coordinate-mediation/3.0` when a mediator is configured (#29).

## [0.2.12] - 2026-09-04

### Added

- **`Layr8.Mediation` — store-and-forward through a Space mediator.** An
  agent that is not always connected gives the client a mediator DID
  (`:mediator` / `LAYR8_MEDIATOR_DID`) and, on every connect and reconnect,
  the client enrols (`mediate-request`, `recipient-update`), declares the
  mediator on its own node (`PUT /api/v1/dids/:did/mediator`, cloud-node
  ADR 0005), collects everything queued (`delivery-request` →
  re-injection through the node's `/didcomm` → `messages-received`) and
  turns live delivery on, handling the mediator's `delivery` pushes the same
  way. The SDK never decrypts: the mediator holds the original ciphertext and
  the node verifies it on re-injection as a first arrival. Each step is also
  callable by hand and none raises. New config `:mediator_live`,
  `:didcomm_url` (`LAYR8_MEDIATOR_LIVE`, `LAYR8_DIDCOMM_URL`); new
  `Layr8.REST.put/4`, `delete/3`, `post_didcomm/3`;
  `Layr8.Client.mediator/1`.

## [0.2.11] - 2026-08-21

### Added

- `Layr8.Identity.attachment/1` and `attachment!/1` — a first-class way to
  attach an **identity credential** (a credential about who the sender is, with
  no `credentialSubject.scope`) so it reaches the cloud-node's
  `sender_credentials` policy input, where a grant's `senderCredentials`
  requirement can see it. They build the attachment; **the caller names the
  credential**. The SDK does not choose: the requirement being satisfied lives
  in the recipient's grant and never reaches the sender, so automatic selection
  could only mean "attach everything the holder has", which is a disclosure
  decision, not a convenience. `{:error, :not_compact_jws}` for anything that is
  not a compact JWS, and `{:error, :credential_is_grant}` for a credential that
  carries a scope — that is a grant, and attached this way it would be routed as
  one and satisfy nothing.
- `Layr8.Identity.attachment?/1`, the same test applied to an attachment already
  on a message.

### Changed

- Caller-supplied attachments still displace the wallet, with one narrowing:
  when they are **all** identity credentials, the wallet's grants are appended
  after them instead — on the send path and on the handler-reply path alike.
  Saying who you are must not stop you saying what you may do; under the old
  rule it did, and the node's denial then read "no grant covers this call".
  Anything else a caller attaches behaves exactly as before.

## [0.2.10] - 2026-08-17

### Added

- **More than one DID on a single connection** — `Layr8.Client.join_did/3`
  joins an additional `plugins:<did>` topic on the WebSocket `connect/1`
  already opened, and returns a `Layr8.DidHandle` that sends and requests as
  that DID. Until now one client meant one DID, so an agent speaking for many
  identities (a workflow per DID, an account per DID) needed one connection
  each; the cloud-node's plugin socket has always matched `plugins:*`, so the
  limit was the SDK's alone.

  Each joined DID carries its own handlers (`:handlers`, `:handle_all`), its
  own protocol subscription (`:protocols`) and its own `:did_spec` — including
  `controller`, which decides whose grants can cover it. The client-global
  registry stays the fallback for every DID. Joined DIDs are re-joined
  automatically after a reconnect.

  New API: `Layr8.Client.join_did/3`, `leave_did/2`, `joined_dids/1`,
  `send_from/4`, `request_from/4`, `request_result_from/4`, and the
  `Layr8.DidHandle` module. Existing single-DID use is unchanged.

  Modelled on the node-sdk's `joinDid` / `DidHandle`, which mcp-pod uses in
  production to host one Instance DID per connected account.

- **A directory-visible check that the node really does host many DIDs** — a
  `multi_did` compat scenario. `join_did`'s three load-bearing properties belong
  to the cloud-node, not to this library, so no unit test can establish them:
  that the node accepts more than one topic on one socket, that an inbound frame
  reaches the handler of the DID it was addressed to, and that after the socket
  drops every joined DID comes back by itself. The last is the one worth having:
  losing an extra DID on a reconnect is silent, with nothing to observe on this
  side.

### Fixed

- **`joined_dids/1` no longer reports a DID the node has not acknowledged.** It
  answered from the client's handler table, which survives a dropped socket
  untouched, so across a reconnect it listed DIDs whose re-join was still in
  flight and a send to one of them raised `NotConnectedError`. Measured against a
  real node: at the moment `on_reconnect` fired, the re-join replies were still
  60ms and 100ms away. It now answers from the channel, which tracks what the
  server confirmed. Never shipped — `joined_dids/1` is new in this release.

## [0.2.9] - 2026-08-10

### Added

- **Verifiable Grants are attached to outbound messages** — automatically, on
  every send path (`send/3`, `request/3`, and a handler's reply). The cloud-node
  requires a grant for anything its policy does not allow outright, and nothing
  in this SDK attached one: an agent that connected directly sent nothing and
  was denied with "no grant covers this call", a message that reads as "your
  grant is misconfigured" when the truth is "no credential was ever put on the
  wire".

  `Layr8.Wallet` reads the holder's credentials from the node, caches them for
  `:grant_cache_ms` (default 60s) and selects the covering set with a mirror of
  helix's `structure_v2.rego`. Caller-supplied attachments are never displaced,
  and a wallet failure never blocks the send.

  New config: `:attach_grants` (default `true`, env `LAYR8_ATTACH_GRANTS`),
  `:grant_cache_ms`, `:grant_read_timeout_ms`, `:on_grant_miss`. New API:
  `Layr8.Client.refresh_grants/2`.

  Contract: `contracts/sender-cn-vg-attachment.md`.

- **`:on_grant_miss`** — told when the node denied a message that went out with
  nothing attached, when the covering set had to be capped at 16, or when the
  grants could not be read at all. It deliberately stays quiet on "nothing
  covered this message" alone: most traffic (discovery, trust-ping, problem
  reports) needs no grant.

- **`Layr8.Mcp`** — MCP over DIDComm. `Layr8.Client.mcp/2` registers the
  protocol subscription and returns a binding; `Layr8.Mcp.peer/2` yields a
  caller with `initialize/3`, `list_tools/2` and `call_tool/4`. It handles the
  `tools/call` → `#{base}/tools-call` type mapping, the JSON-RPC envelope and
  unwrapping `result`. Must be called before `connect/1`, like `handle/3`.
  Contract: `contracts/mcp-over-didcomm.md`.

- **`Layr8.Client.request_result/3`** — `request/3` without the raises, for
  callers that route on failure rather than rescuing it. `Layr8.Mcp` uses it,
  which is why its whole surface is tagged tuples.

- **`:rest_timeout_ms`** (default 30s, env `LAYR8_REST_TIMEOUT_MS`) — a deadline
  on every credential and presentation call, not just the grant read. `Req` has
  none of its own, so a node that accepted the connection and went quiet left
  those calls hanging. `0` disables it.

### Changed

- **Every send now performs a credential read against the node before the
  message goes out** (once per `:grant_cache_ms` per DID; failures are cached
  for a shorter window so a misconfigured API key is not a per-message round
  trip). A node that cannot serve `/api/v1/credentials` degrades to sending
  unattached — the previous behaviour — and `:on_grant_miss` reports it. Set
  `attach_grants: false` to opt out entirely.

- **`Layr8.REST` no longer retries.** `Req` retries safe requests by default
  with a 1s/2s/4s backoff, and because `receive_timeout` is per attempt, that
  silently multiplied every deadline by four: measured, a 2s grant read against
  a node that accepts the connection and goes quiet took **7.6 seconds** to
  return. A deadline exists here so a hung node cannot stall the sends queued
  behind it, so `retry: false` now goes wherever a deadline goes. Retrying is
  the caller's decision, with its own knowledge of whether the call is worth
  repeating.

- `Layr8.REST.new/2`, `get/2` and `post/3` gained optional trailing arguments;
  the existing arities are unchanged.

### Fixed

- **`Layr8.Attachment`'s documentation pointed at the wrong field.** It said to
  put `application/vc+jwt` in `format` and to carry credentials in
  `data.base64`. `media_type` is the ONLY field the node's credential extractor
  filters on, by exact string equality, and it drops everything else *before
  looking at the data* — producing a denial byte-for-byte identical to the one
  for attaching nothing. Following those docs attached nothing at all. The same
  correction is on `Layr8.Presentations.sign_presentation/3`, which is not the
  authorization path either.

[0.2.13]: https://github.com/layr8/elixir_sdk/releases/tag/v0.2.13
[0.2.12]: https://github.com/layr8/elixir_sdk/releases/tag/v0.2.12
[0.2.11]: https://github.com/layr8/elixir_sdk/releases/tag/v0.2.11
[0.2.10]: https://github.com/layr8/elixir_sdk/releases/tag/v0.2.10
[0.2.9]: https://github.com/layr8/elixir_sdk/releases/tag/v0.2.9
