# 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.
| `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:
```rust,ignore
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:
```yaml
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:
```rust,ignore
// 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):
```rust
#[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:
```rust
#[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.
1. Publisher imports the IPNS key under an ephemeral name, publishes, and
removes the key immediately after.
1. Returns `IpfsPublishDidResponse` with `did` and `cid`.
### 3. Verify published target
After publish, resolve the IPNS name and compare to expected CID path:
```rust
#[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
1. call `publisher.wait_until_ready(attempts)`
1. decode transport bytes
1. call `publisher.publish_signed_message(...)`
1. emit structured log with `did`, `cid`
1. optionally run a post-publish `name_resolve` check
Minimal orchestration example:
```rust
#[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
```bash
cargo build
cargo test
```
Wasm, slim iroh-only profile:
```bash
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):
```bash
cargo check --target wasm32-unknown-unknown --no-default-features --features "iroh gossip"
```
Run clippy when needed:
```bash
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.