enlace 0.2.5

Encrypted mailbox and latest-value slot fan-out.
Documentation
# enlace

Encrypted mailbox and latest-value slot fan-out for Rust applications.

`enlace` can send the same encrypted payload over multiple transports and accept
the first valid delivery. It currently includes HTTP relay, DHT, pkarr, and iroh
transports.

## Crates

- `enlace`: library API
- `enlace-agent`: local daemon for non-Rust clients
- `enlace-relay`: HTTP relay server
- `enlace-testkit`: in-memory, lossy, and delayed test transports

## Features

Default features enable all built-in transports. For smaller builds, disable
defaults and select only what you use:

```toml
enlace = { version = "0.1", default-features = false, features = ["http"] }
enlace = { version = "0.1", default-features = false, features = ["dht"] }
enlace = { version = "0.1", default-features = false, features = ["pkarr"] }
enlace = { version = "0.1", default-features = false, features = ["iroh"] }
enlace = { version = "0.1", default-features = false, features = ["http", "iroh"] }
```

## Modes

- Shared-seed mode uses `Namespace::open(&seed, config)`. Use it when every peer
  can share one 32-byte secret and should have equal access to the same mailbox
  and slot names.
- Public-key mode uses `PeerNamespace::open(identity, config)`. Use it when peers
  pair by exchanging public `PeerCard` values and need per-peer trust, revocation,
  pairwise messages, or caller-managed group keys.

HTTP and iroh support mailboxes. HTTP, DHT, pkarr, and iroh support slots. DHT and
pkarr are latest-record slot transports; they do not support mailbox delivery.

## Shared-Seed Example

```rust
use std::sync::Arc;

use enlace::{Config, ConfiguredTransport, Namespace, TransportKind};
use enlace_testkit::InMemoryTransport;

const SEED: [u8; 32] = [7; 32];

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let transport = InMemoryTransport::new();
    let config = |transport: InMemoryTransport| Config {
        transports: vec![ConfiguredTransport::new(
            TransportKind::Http,
            Arc::new(transport),
        )],
        ..Config::default()
    };

    let sender = Namespace::open(&SEED, config(transport.clone())).await?;
    let receiver = Namespace::open(&SEED, config(transport)).await?;

    sender.mailbox("chat")?.send(b"hello").await?;
    let message = receiver.mailbox("chat")?.recv().await?;
    assert_eq!(message.payload, b"hello");

    receiver.slot("status")?.put(b"online").await?;
    let status = sender.slot("status")?.get().await?.expect("slot value");
    assert_eq!(status.payload, b"online");

    Ok(())
}
```

## Public-Key Example

```rust
use std::sync::Arc;

use enlace::{
    ConfiguredTransport, PeerConfig, PeerIdentity, PeerNamespace, TransportKind, TrustedPeer,
};
use enlace_testkit::InMemoryTransport;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let transport = InMemoryTransport::new();

    let sender_identity = PeerIdentity::generate();
    let receiver_identity = PeerIdentity::generate();
    let sender_card = sender_identity.card();
    let receiver_card = receiver_identity.card();

    let sender = PeerNamespace::open(
        sender_identity,
        peer_config(
            transport.clone(),
            vec![TrustedPeer::try_from_card(receiver_card.clone())?],
        ),
    )
    .await?;
    let receiver = PeerNamespace::open(
        receiver_identity,
        peer_config(transport, vec![TrustedPeer::try_from_card(sender_card)?]),
    )
    .await?;

    sender
        .mailbox("chat")?
        .send_to_peers(std::slice::from_ref(&receiver_card), b"hello")
        .await?;

    let message = receiver.mailbox("chat")?.recv().await?;
    assert_eq!(message.sender, sender.peer_id());
    assert_eq!(message.payload, b"hello");

    Ok(())
}

fn peer_config(transport: InMemoryTransport, trusted_peers: Vec<TrustedPeer>) -> PeerConfig {
    PeerConfig {
        trusted_peers,
        transports: vec![ConfiguredTransport::new(
            TransportKind::Http,
            Arc::new(transport),
        )],
        ..PeerConfig::default()
    }
}
```

## Agent Daemon

`enlace-agent` exposes the public-key peer API to local clients over WebSocket
and Unix socket JSON.

Build a release binary:

```sh
cargo build -p enlace-agent --release --all-features --locked
```

Example host A config at `/etc/enlace/agent.toml`:

```toml
seed_file = "/etc/enlace/seed"
token_file = "/etc/enlace/token"
listen_ws = "127.0.0.1:3000"
listen_unix = "/run/enlace/agent.sock"
data_dir = "/var/lib/enlace-agent"
transports = ["http"]
relay = "https://relay.example.com"
```

Host B uses its own seed and a different local port:

```toml
seed_file = "/etc/enlace/seed"
token_file = "/etc/enlace/token"
listen_ws = "127.0.0.1:3001"
data_dir = "/var/lib/enlace-agent"
transports = ["http"]
relay = "https://relay.example.com"
```

Start each host:

```sh
enlace-agent --config /etc/enlace/agent.toml
```

Pair hosts by exporting each local card and adding it on the other host:

```json
{"id":"card","type":"export_card"}
{"id":"add","type":"add_peer","card":"<peer-card-from-other-host>"}
```

Client messages stay opaque base64 payloads:

```json
{"id":"sub","type":"subscribe","channel":"default"}
{"id":"send","type":"send","to":"<peer-id>","channel":"default","payload":"aGVsbG8="}
```

The systemd unit template is in `packaging/systemd/enlace-agent.service`.

## Relay

Run the HTTP relay locally:

```sh
cargo run -p enlace-relay -- --listen 127.0.0.1:7777
```

With basic auth:

```sh
ENLACE_RELAY_AUTH=user:pass cargo run -p enlace-relay -- --listen 127.0.0.1:7777
```

## Verification

```sh
cargo test --workspace --locked
cargo check -p enlace --no-default-features --locked
cargo check -p enlace --no-default-features --features http --locked
cargo run -p enlace-testkit --example two_peers --locked
```

## License

MIT