ma-core 0.10.2

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.
  • Outbound delivery — transport-agnostic fire-and-forget send path. Messages are validated, serialized to CBOR, and transmitted.

Service model

  • Service trait — declares a protocol identifier and label.
  • MaEndpoint trait — shared interface for all transport endpoints: register services, get inboxes, send messages.

The crate currently ships with an iroh-based transport backend internally, but iroh-specific types are considered backend details.

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.
  • IpfsDidPublisher (non-WASM, kubo feature) — publishes validated documents to IPFS via the native RPC backend.
  • publish_did_document_to_kubo / handle_ipfs_publish — lower-level publish helpers.

DID resolution

  • DidDocumentResolver trait — async DID-to-Document resolution.
  • IpfsGatewayResolver — 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).

Native IPFS RPC (non-WASM, kubo feature)

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

Feature flags

These are Cargo compile-time feature flags.

Feature Default Description
kubo no Native IPFS RPC backend for publishing
iroh yes Internal iroh QUIC transport backend
gossip yes Internal iroh gossip support
config no Config model + YAML serialization + encrypted secret bundles (CLI/fs/logging remain native-only)

config feature

The config feature supports both native and wasm32 targets, but with different capability levels.

It provides on all targets:

  • Config — serializable config model (from_yaml_str, to_yaml_string).
  • SecretBundle — generate keys and encrypt/decrypt bundle bytes.
  • BrowserIdentityExport — JSON payload with inlined encrypted bundle (encrypted_secret_bundle_base64) for browser import/export.

Native-only additions:

  • MaArgs — a #[derive(Args)] struct you flatten into your own Parser.
  • Config::from_args — merge CLI/env/YAML/defaults for daemons.
  • Config::save / Config::gen_headless — filesystem persistence helpers.
  • SecretBundle::save / SecretBundle::load — encrypted file I/O.
  • Config::init_logging() — sets up tracing-subscriber with separate log levels for file and stdout.

Wasm logging behavior:

  • Config::init_logging() is also available on wasm and routes logs to browser console.
  • Console output is filtered by log_level_stdout.

Minimal usage:

use clap::Parser;
use ma_core::config::{Config, MaArgs};

const MA_DEFAULT_SLUG: &str = "myd";

#[derive(Parser)]
struct Cli {
    #[command(flatten)]
    ma: MaArgs,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    let config = Config::from_args(&cli.ma, MA_DEFAULT_SLUG)?;
    config.init_logging()?;
  let _resolver = config.ipfs_gateway_resolver();
    Ok(())
}

Config file ($XDG_CONFIG_HOME/ma/<slug>.yaml) example:

log_level: debug
kubo_rpc_url: http://127.0.0.1:5001
did_resolver_positive_ttl_secs: 60
did_resolver_negative_ttl_secs: 10

Platform support

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

  • This library is intended for both wasm and native targets.
  • IpfsGatewayResolver is available on both wasm and native for gateway-based DID fetch.
  • Only Kubo write/pin operations are native-only.
  • On wasm builds, the native kubo module is not compiled in.
  • config model serialization and SecretBundle crypto are available on wasm.
  • config filesystem and CLI/env facilities are native-only.
  • iroh transport compiles on wasm and native.
  • gossip is optional and can be enabled when needed.

Important: ma-core does provide gateway-based DID fetch on wasm via IpfsGatewayResolver. Native-only IPFS RPC operations (publish/pin/write) remain unavailable on wasm.

For wasm storage, persist encrypted SecretBundle bytes and serialized Config text in browser storage, and provide the passphrase from user input at runtime instead of storing it.

Compile-time split note:

  • On wasm, Config does not include Kubo-specific fields.
  • On native, Config includes daemon/Kubo fields and filesystem helpers.

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 native IPFS RPC (native only):

#[cfg(not(target_arch = "wasm32"))]
async fn publish(message_cbor: &[u8]) -> anyhow::Result<()> {
  let publisher = ma_core::IpfsDidPublisher::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 native-only flow for publishing a DID document through native IPFS RPC.

1. Preconditions

  • Native IPFS RPC 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 CBOR encoded IpfsPublishDidRequest.
  • The DID document is valid and signature-verifiable.

The DID document itself is always DAG-CBOR. Use Document::encode() and Document::decode() for serialization.

2. Validate and publish

Use IpfsDidPublisher 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::IpfsDidPublisher::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::IpfsDidPublisher,
  ipns_id: &str,
  expected_cid: &str,
) -> anyhow::Result<()> {
  let resolved = ma_core::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 IpfsDidPublisher::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::IpfsDidPublisher::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

Note: target-specific wasm dependencies in Cargo.toml enable required web RNG support (getrandom/js) automatically for wasm32-unknown-unknown.

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.