ma-core 0.6.8

DIDComm service library: inboxes, outboxes, DID document publishing, and transport abstraction
Documentation

ma-core

A lean DIDComm service library for the ma ecosystem.

ma-core provides everything an ma endpoint needs: DID document publishing, service inboxes, outbox delivery, and transport abstraction — without coupling to any specific runtime or application.

What it provides

Messaging primitives

  • Inbox — bounded, TTL-aware FIFO receive queue for service endpoints. Per-message TTL is computed from each message's created_at + ttl. Only endpoint implementations push to an inbox; consumers read via pop/peek/drain.
  • Outbox — transport-agnostic write handle for fire-and-forget delivery. Takes a Message, validates, serializes to CBOR, and transmits.

Service model

  • Service trait — declares a protocol identifier and label.
  • MaEndpoint trait — shared interface for all transport endpoints: register services, get inboxes, send messages.
  • IrohEndpoint (behind iroh feature) — iroh QUIC-backed implementation of MaEndpoint.

Every endpoint must provide ma/inbox/0.0.1. Endpoints may optionally provide ma/ipfs/0.0.1 to publish DID documents on behalf of others.

DID document publishing

  • validate_ipfs_publish_request — decodes a signed CBOR message, enforces application/x-ma-ipfs-request content type, validates the document, verifies sender matches IPNS identity.
  • KuboDidPublisher (non-WASM, kubo feature) — publishes validated documents to IPFS via Kubo RPC.
  • publish_did_document_to_kubo / handle_ipfs_publish — lower-level publish helpers.

Iroh startup metadata (ma.iroh)

When using iroh transport, update DID metadata from the live endpoint before publishing at startup:

let mut endpoint = IrohEndpoint::new(secret_bytes).await?;

// Optional service registrations here.
let _inbox = endpoint.service("/ma/inbox/0.0.1");

if endpoint.reconcile_document_ma_iroh(&mut document)? {
    // Re-sign and publish only when metadata changed.
}

DID resolution

  • DidResolver trait — async DID-to-Document resolution.
  • GatewayResolver — resolves via an IPFS/IPNS HTTP gateway.

Transport parsing

Parses DID document service strings like /iroh/<endpoint-id>/ma/inbox/0.0.1:

  • endpoint_id_from_transport / protocol_from_transport
  • resolve_endpoint_for_protocol / resolve_inbox_endpoint_id
  • transport_string — build service strings from parts.

Identity bootstrap

  • generate_secret_key_file / load_secret_key_bytes — secure 32-byte key persistence with OS-level permission hardening.

Pinning

  • pin_update_add_rm — pin new CID, unpin old, report unpin failures as metadata (not hard errors).

Kubo RPC (non-WASM, kubo feature)

HTTP client for Kubo /api/v0/ endpoints: add, cat, DAG put/get, IPNS publish/resolve, key management, pinning.

Feature flags

Feature Default Description
kubo no Kubo RPC client for IPFS publishing
iroh yes Iroh QUIC transport (IrohEndpoint, Channel, Outbox)
gossip yes Iroh gossip helpers (join_gossip_topic, gossip_send, broadcast helpers)

Platform support

Core types (Inbox, Service, transport parsing, validation) compile on all targets including wasm32-unknown-unknown.

  • kubo APIs are native-only.
  • GatewayResolver is native-only.
  • iroh transport compiles on wasm and native.
  • gossip is optional and can be enabled when needed.

Quick usage

Consumers receive validated Message objects from an Inbox — the endpoint handles deserialization and validation before messages enter the queue:

// Endpoint gives you an Inbox<Message> when you register a service
let mut inbox = endpoint.service("ma/inbox/0.0.1");

let now = current_time_secs();
while let Some(msg) = inbox.pop(now) {
    println!("from={} type={}", msg.from, msg.message_type);
}

Example: full publish flow against Kubo (non-WASM):

#[cfg(not(target_arch = "wasm32"))]
async fn publish(message_cbor: &[u8]) -> anyhow::Result<()> {
  let publisher = ma_core::KuboDidPublisher::new("http://127.0.0.1:5001/api/v0")?;
  let response = publisher.publish_signed_message(message_cbor).await?;
    println!("ok={} did={:?} cid={:?}", response.ok, response.did, response.cid);
    Ok(())
}

End-to-end operational flow

This section shows a concrete non-WASM flow for publishing a DID document through Kubo.

1. Preconditions

  • Kubo API is reachable at whichever base URL your environment uses.
  • Create a publisher once with that URL and reuse the same instance.
  • You have a signed CBOR Message payload where content_type is application/x-ma-ipfs-request, from is a DID whose IPNS id matches the DID document id, and content is JSON encoded IpfsPublishDidRequest.
  • The DID document is valid and signature-verifiable.

2. Validate and publish

Use KuboDidPublisher when you want a persisted endpoint configuration:

#[cfg(not(target_arch = "wasm32"))]
pub async fn publish_from_wire(
  kubo_url: &str,
  message_cbor: &[u8],
) -> anyhow::Result<ma_core::IpfsPublishDidResponse> {
  let publisher = ma_core::KuboDidPublisher::new(kubo_url)?;
  publisher.publish_signed_message(message_cbor).await
}

What it does internally:

  1. validate_ipfs_publish_request verifies message and document integrity.
  2. Publisher imports the IPNS key under an ephemeral name, publishes, and removes the key immediately after.
  3. Returns IpfsPublishDidResponse with did and cid.

3. Verify published target

After publish, resolve the IPNS name and compare to expected CID path:

#[cfg(not(target_arch = "wasm32"))]
pub async fn verify_publish(
  publisher: &ma_core::KuboDidPublisher,
  ipns_id: &str,
  expected_cid: &str,
) -> anyhow::Result<()> {
  let resolved = ma_core::kubo::name_resolve(publisher.kubo_url(), &format!("/ipns/{ipns_id}"), true).await?;
  let expected = format!("/ipfs/{expected_cid}");
  anyhow::ensure!(resolved == expected, "resolved target mismatch: {resolved} != {expected}");
  Ok(())
}

4. Production readiness pattern

Recommended startup/publish order:

  1. create KuboDidPublisher::new(kubo_url) once
  2. call publisher.wait_until_ready(attempts)
  3. decode transport bytes
  4. call publisher.publish_signed_message(...)
  5. emit structured log with did, cid
  6. optionally run a post-publish name_resolve check

Minimal orchestration example:

#[cfg(not(target_arch = "wasm32"))]
pub async fn publish_with_readiness(
  kubo_url: &str,
  message_cbor: &[u8],
) -> anyhow::Result<ma_core::IpfsPublishDidResponse> {
  let publisher = ma_core::KuboDidPublisher::new(kubo_url)?;
  publisher.wait_until_ready(5).await?;
  let response = publisher.publish_signed_message(message_cbor).await?;
  Ok(response)
}

5. Failure semantics you should rely on

  • Invalid CBOR, content type, DID document, or signatures fail fast.
  • Sender/document IPNS mismatch fails fast.
  • Missing/import-mismatched key material fails fast.
  • IPNS publish uses retry and may still be accepted if resolve confirms target.
  • Unpin failures in pin rotation are returned as metadata (PinUpdateOutcome) and do not hide successful new pin operations.

Build and test

cargo build
cargo test

Wasm, slim iroh-only profile:

cargo check --target wasm32-unknown-unknown --no-default-features --features iroh

Wasm, iroh + gossip profile (when you need broadcast):

cargo check --target wasm32-unknown-unknown --no-default-features --features "iroh gossip"

Run clippy when needed:

cargo clippy --all-targets --all-features -- -D warnings

Design principles

  • Strict input validation; never mutate malformed data.
  • Small and clear building blocks, without unnecessary complexity.
  • Shared types and contracts in the library; avoid duplication in consumers.
  • Fail hard when identity, signature, or key mapping does not match.