hubert 0.5.0

Secure distributed substrate for multiparty transactions using write-once key-value storage with ARID-based addressing
Documentation
# Hubert Key-Value Storage API Manual

This document provides an overview of the key-value storage API provided by the Hubert crate. Hubert supports multiple storage backends, including Mainline DHT, IPFS, and Hybrid storage, through a common `KvStore` trait.

## Basic Example: Mainline DHT Storage

```rust
use bc_components::ARID;
use bc_envelope::Envelope;
use hubert::{KvStore, mainline::MainlineDhtKv};

#[tokio::main]
async fn main() -> hubert::Result<()> {
    // Create a Mainline DHT store
    let store = MainlineDhtKv::new().await?;

    // Generate an ARID for this storage location
    let arid = ARID::new();

    // Create an envelope
    let envelope = Envelope::new("Hello, Hubert!");

    // Store the envelope (write-once)
    // Parameters: arid, envelope, ttl_seconds (ignored for DHT), verbose
    let receipt = store.put(&arid, &envelope, None, false).await?;
    println!("Stored: {}", receipt);

    // Share the ARID with other parties via secure channel
    // (Signal, QR code, GSTP message, etc.)
    println!("ARID: {}", arid.ur_string());

    // Retrieve the envelope
    // Parameters: arid, timeout_seconds, verbose
    if let Some(retrieved) = store.get(&arid, None, false).await? {
        println!("Retrieved: {}", retrieved);
    }

    Ok(())
}
```

## Example: IPFS Storage

```rust
use bc_components::ARID;
use bc_envelope::Envelope;
use hubert::{KvStore, ipfs::IpfsKv};

#[tokio::main]
async fn main() -> hubert::Result<()> {
    // Create an IPFS store (requires running Kubo daemon)
    let store = IpfsKv::new("http://127.0.0.1:5001");

    let arid = ARID::new();
    let envelope = Envelope::new("Large data payload");

    // Store using IPFS (supports up to 10 MB)
    // TTL is used for IPNS record lifetime (default 24h if None)
    let receipt = store.put(&arid, &envelope, Some(86400), false).await?;
    println!("Stored: {}", receipt);

    // Retrieve with 10 second timeout
    if let Some(retrieved) = store.get(&arid, Some(10), false).await? {
        println!("Retrieved from IPFS: {}", retrieved);
    }

    Ok(())
}
```

## Example: Hybrid Storage

```rust
use bc_components::ARID;
use bc_envelope::Envelope;
use hubert::{KvStore, hybrid::HybridKv};

#[tokio::main]
async fn main() -> hubert::Result<()> {
    // Create a Hybrid store (combines DHT speed with IPFS capacity)
    let store = HybridKv::new("http://127.0.0.1:5001").await?;

    // Small envelopes (≤1KB) go to DHT automatically
    let arid1 = ARID::new();
    let small = Envelope::new("Small message");
    store.put(&arid1, &small, None, false).await?;

    // Large envelopes (>1KB) use DHT reference + IPFS storage
    let arid2 = ARID::new();
    let large = Envelope::new("x".repeat(2000));
    store.put(&arid2, &large, None, false).await?;

    // Retrieval is transparent - same API for both
    let _retrieved1 = store.get(&arid1, None, false).await?;
    let _retrieved2 = store.get(&arid2, None, false).await?;

    Ok(())
}
```

## KvStore Trait

All storage backends implement the `KvStore` trait, which provides a unified interface:

```rust
#[async_trait::async_trait(?Send)]
pub trait KvStore: Send + Sync {
    /// Store an envelope at the given ARID (write-once).
    async fn put(
        &self,
        arid: &ARID,
        envelope: &Envelope,
        ttl_seconds: Option<u64>,
        verbose: bool,
    ) -> Result<String>;

    /// Retrieve an envelope by ARID with optional timeout.
    async fn get(
        &self,
        arid: &ARID,
        timeout_seconds: Option<u64>,
        verbose: bool,
    ) -> Result<Option<Envelope>>;

    /// Check if an ARID exists without fetching the envelope.
    async fn exists(&self, arid: &ARID) -> Result<bool>;
}
```

This allows you to write storage-backend-agnostic code:

```rust
use bc_components::ARID;
use bc_envelope::Envelope;
use hubert::{KvStore, Result};

async fn store_envelope(
    store: &impl KvStore,
    arid: &ARID,
    envelope: &Envelope,
) -> Result<String> {
    // Works with any backend: MainlineDhtKv, IpfsKv, HybridKv, etc.
    store.put(arid, envelope, None, false).await
}
```

### Parameters

**`put` method:**
- `arid`: The ARID key for this storage location
- `envelope`: The envelope to store
- `ttl_seconds`: Optional time-to-live
  - **Mainline DHT**: Ignored (no TTL support)
  - **IPFS**: IPNS record lifetime (default 24h if None)
  - **Hybrid**: Uses IPFS TTL for large envelopes
  - **Server**: Clamped to server's max_ttl; uses max_ttl if None
- `verbose`: Enable verbose logging with timestamps

**`get` method:**
- `arid`: The ARID key to retrieve
- `timeout_seconds`: Maximum time to poll for the envelope
  - If `None`, uses backend-specific default (typically 30s)
  - Returns `Ok(None)` if not found within timeout
- `verbose`: Enable verbose logging with polling dots

**`exists` method:**
- `arid`: The ARID key to check
- Returns `Ok(true)` if exists, `Ok(false)` otherwise

## Write-Once Semantics

All storage backends enforce write-once semantics. Attempting to write to an existing ARID will fail:

```rust
use hubert::{Error, Result};

// First write succeeds
store.put(&arid, &envelope1, None, false).await?;

// Second write to same ARID fails
match store.put(&arid, &envelope2, None, false).await {
    Err(Error::AlreadyExists { arid }) => {
        println!("ARID {} already exists", arid);
    }
    _ => {}
}
```

You can also check for existence before attempting to put:

```rust
if store.exists(&arid).await? {
    println!("ARID already in use");
} else {
    store.put(&arid, &envelope, None, false).await?;
}
```

## Error Handling

The library uses a unified `Error` type with backend-specific variants:

```rust
use hubert::Error;

match store.put(&arid, &envelope, None, false).await {
    Ok(receipt) => println!("Stored: {}", receipt),
    Err(Error::AlreadyExists { arid }) => {
        println!("ARID {} already exists", arid);
    }
    Err(Error::Mainline(e)) => {
        // Mainline-specific error (e.g., ValueTooLarge)
        println!("DHT error: {}", e);
    }
    Err(Error::Ipfs(e)) => {
        println!("IPFS error: {}", e);
    }
    Err(e) => println!("Error: {}", e),
}
```

### Common Error Variants

- `Error::AlreadyExists { arid }`: The ARID already has a stored value
- `Error::NotFound`: The requested ARID was not found
- `Error::InvalidArid`: The ARID format is invalid
- `Error::Mainline(e)`: Mainline DHT-specific error
  - `ValueTooLarge { size }`: Envelope exceeds 1KB limit
- `Error::Ipfs(e)`: IPFS-specific error
- `Error::Hybrid(e)`: Hybrid storage-specific error
- `Error::Envelope(e)`: Envelope serialization/deserialization error
- `Error::Cbor(e)`: CBOR encoding/decoding error

## Polling and Timeouts

The `get` method polls the storage backend until the envelope appears or the timeout is reached. This is useful for coordination between parties:

```rust
// Party A: Store envelope
let arid = ARID::new();
store.put(&arid, &envelope, None, false).await?;

// Share ARID with Party B via secure channel...

// Party B: Poll for envelope with 30 second timeout and verbose output
match store.get(&arid, Some(30), true).await? {
    Some(envelope) => {
        println!("Received envelope");
        // Process envelope...
    }
    None => {
        println!("Envelope not found within 30 seconds");
    }
}
```

When `verbose` is enabled, the get operation will print:
- Start time
- Polling dots (one per retry)
- Success/timeout message with elapsed time