Layr8.Attachment (layr8 v0.3.2)

Copy Markdown View Source

A DIDComm v2 attachment.

Attachments carry payloads that travel alongside a message but are not part of the message body — signed credentials, large binaries, file references, etc.

Fields

  • id — unique attachment identifier (within the containing message)
  • description — human-readable description
  • filename — suggested filename for downloaded content
  • media_type — IANA media type of the content. For a Verifiable Grant this is the load-bearing field — see below.
  • format — protocol-specific format identifier
  • lastmod_time — a hint about when the content was last modified; an integer or a string, passed through unread. See below.
  • byte_count — size of the attached content in bytes
  • data — map carrying the payload; see DIDComm v2 spec §5

data map

The data map may contain one or more of:

  • "base64" — base64url-encoded bytes (no padding)
  • "json" — inline JSON value
  • "jws" — the JWS; the cloud-node's credential extractor reads a compact JWS string from here
  • "hash" — base64url-encoded SHA-256 of the referenced content
  • "links" — list of URLs pointing to the content

Attaching a credential for authorization

Contract: ~/Developments/contracts/sender-cn-vg-attachment.md. In normal use Layr8.Wallet builds these attachments automatically on every send — this section is for hand-built ones.

%Layr8.Attachment{
  id: "urn:uuid:…",              # the credential's own id
  media_type: "application/vc+jwt",
  data: %{"jws" => compact_jws}
}

media_type is the only thing the node's credential extractor filters on, by exact string equality, and it drops every other attachment SILENTLY before looking at the data at all. Two consequences that have each cost a team a day:

  • Putting "application/vc+jwt" in format and something else in media_type attaches nothing, as far as the node is concerned. (These docs used to say to do exactly that.)
  • A Verifiable Presentation (application/vp+jwt) — the standard envelope for a holder showing a credential, and what Layr8.Presentations.sign_presentation/3 produces — is dropped on the same rule. Attach the credential bare.

Either way the denial that follows is byte-for-byte the one you get for attaching nothing at all, which is why the mistake is expensive to find.

data.base64 also works (the extractor falls back to it and base64url-decodes it), but prefer "jws": it is read first and it is what the whole ecosystem writes.

lastmod_time is an integer OR a string, and this SDK reads neither

DIDComm v2 states no type for the field. Its Attachments section says only "OPTIONAL. A hint about when the content in this attachment was last modified", while the same document pins created_time and expires_time to "UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z) as an integer". The authors knew how to spell "epoch integer" and did not spell it here, so a receiver is not entitled to demand one. Epoch seconds are what this SDK writes and what the ecosystem mostly sends; an ISO-8601 string has also been sent. Both arrive here unchanged.

The typespec used to say non_neg_integer() | nil while parse/1 converted nothing, so a string sat in a field declared an integer.

Nothing here interprets the hint. A caller that wants a moment out of it matches on what arrived:

case att.lastmod_time do
  nil -> nil                                    # the sender sent no hint
  secs when is_integer(secs) -> DateTime.from_unix!(secs)
  iso when is_binary(iso) -> elem(DateTime.from_iso8601(iso), 1)
end

Absent, integer and string are three different values and stay three different values. "The sender sent no hint" and "the sender sent one I have to match on" are not the same fact.

Wire format

%{
  "id" => "...",
  "media_type" => "...",
  "data" => %{"jws" => "..."}
}

Optional fields are omitted when empty/nil.

Summary

Functions

Serializes an Attachment into a DIDComm v2 JSON envelope map.

Parses a DIDComm v2 attachment map into an Attachment struct.

Types

data_map()

@type data_map() :: %{optional(String.t()) => term()}

t()

@type t() :: %Layr8.Attachment{
  byte_count: non_neg_integer() | nil,
  data: data_map(),
  description: String.t(),
  filename: String.t(),
  format: String.t(),
  id: String.t(),
  lastmod_time: non_neg_integer() | String.t() | nil,
  media_type: String.t()
}

Functions

marshal(att)

@spec marshal(t()) :: map()

Serializes an Attachment into a DIDComm v2 JSON envelope map.

Optional string fields are omitted when empty; optional numeric fields are omitted when nil. data is always included (defaults to %{}).

parse(map)

@spec parse(map()) :: t()

Parses a DIDComm v2 attachment map into an Attachment struct.

Missing fields default to empty strings / nil / %{}.