Layr8.Identity (layr8 v0.2.11)

Copy Markdown View Source

Attaching an identity credential — a credential about who the sender is, not about what it may do.

An identity credential rides in the same attachments list, with the same media_type, as a Verifiable Grant. The cloud-node tells the two apart on one test — credentialSubject.scope: present and non-empty is a grant and feeds the policy's credentials input; absent or empty is an identity credential and feeds sender_credentials, where a grant's constraints.senderCredentials requirement can see it.

Why this is a separate path, not a wallet feature

Layr8.Wallet SELECTS grants: it ranks candidates by how well their scope covers the outbound message. An identity credential has no scope, so it cannot be ranked — and there is nothing here for the wallet to select BY either. The requirement it would have to satisfy lives in the grant held by the RECIPIENT and never reaches the sender before the call.

So an SDK that chose identity credentials automatically would have exactly one implementable behaviour: attach everything the holder has. That is a disclosure decision wearing the costume of a convenience feature. Which claims about a person or an organisation a counterparty gets to see is the holder's call, made per message.

The caller names the credential. This module only builds the envelope.

{:ok, creds} = Layr8.Client.list_credentials(client)
employment = Enum.find(creds, &(&1["id"] == chosen_id))

Layr8.Client.send_message(client, %Layr8.Message{
  to: [peer],
  type: "https://layr8.io/protocols/mcp/1.0/tools-call",
  body: %{"params" => %{"name" => "place_order"}},
  attachments: [Layr8.Identity.attachment!(employment["credential_jwt"])]
})

Attaching one does NOT cost the message its grants: the client appends the wallet's selection after attachments that are all identity credentials.

The same abstraction in the other SDKs

attachment/1 and attachment?/1 are this SDK's spelling of the node SDK's identityAttachment / isIdentityAttachment, the Python SDK's identity_attachment / is_identity_attachment, and the Go SDK's IdentityAttachment / IsIdentityAttachment. Same argument, same two refusal cases; only the name is module-qualified, as Elixir names are.

Summary

Types

Why a credential could not be attached as an identity credential.

Functions

Build the attachment that carries one identity credential.

attachment/1, raising ArgumentError instead of returning {:error, _}.

Is this attachment an identity credential — the same test the node routes on, applied to what is actually on the message?

The only media type the node's credential extractor keeps, matched by exact string equality.

Types

reason()

@type reason() :: :not_compact_jws | :credential_is_grant

Why a credential could not be attached as an identity credential.

  • :not_compact_jws — not three non-empty dot-separated segments, or a payload segment that is not base64url-encoded JSON object. The node can verify nothing else, so putting it on the wire only buys a denial that names the wrong problem. Three segments is not the same as three READABLE segments: a payload that does not decode says nothing about credentialSubject.scope, so nothing here can show it to be an identity credential rather than a grant.
  • :credential_is_grant — a non-empty credentialSubject.scope. That is a GRANT: the node would route it to the policy's credentials input, it would not satisfy a senderCredentials requirement, and the resulting denial is indistinguishable from having attached nothing. The check is local, exact and free — refusing here is the difference between an error at the call site and a silent misroute diagnosed at the far end. Grants belong to Layr8.Wallet, which selects and caps them; this path does neither.

Functions

attachment(credential_jws)

@spec attachment(String.t()) :: {:ok, Layr8.Attachment.t()} | {:error, reason()}

Build the attachment that carries one identity credential.

credential_jws is the credential itself — the compact JWS, which Layr8.Client.list_credentials/2 returns as "credential_jwt". It is not a credential id: an id would mean a read from the node, and this runs inside the send path where an unbounded read stalls the client (the reason Layr8.Config's grant_read_timeout_ms exists). A JWS also lets a caller attach a credential the node's store has never seen.

Returns {:error, reason} rather than an attachment the far end will misread. See reason/0.

attachment!(credential_jws)

@spec attachment!(String.t()) :: Layr8.Attachment.t()

attachment/1, raising ArgumentError instead of returning {:error, _}.

The shape for a call site that is naming a credential it already read from the node, where a refusal is a programming mistake and not a runtime condition.

attachment?(arg1)

@spec attachment?(Layr8.Attachment.t() | any()) :: boolean()

Is this attachment an identity credential — the same test the node routes on, applied to what is actually on the message?

Used by the client to decide whether caller-supplied attachments should still displace the wallet. Nothing here trusts how the attachment was built: a hand-assembled one counts exactly the same.

False for an attachment whose jws does not decode, as well as for one that carries a scope. Only a credential this can actually READ as scope-free is an identity credential.

credential_media_type()

The only media type the node's credential extractor keeps, matched by exact string equality.

Everything else — including application/vp+jwt, the Verifiable Presentation envelope — is dropped in silence, and the denial that follows is byte-for-byte the one for attaching nothing at all.