alpine-protocol-sdk 0.2.4

High-level SDK on top of the ALPINE protocol layer.
Documentation
# ALPINE Rust SDK

`alpine-protocol-sdk` is a high-level wrapper around the published `alpine-protocol-rs`
protocol artifacts. It keeps discovery, handshake, and streaming lifecycles explicit so
application code can reason about each step without diving into the lower-level
protocol helpers.

## When to use the SDK vs the protocol layer

- **Protocol layer** (`alpine-protocol-rs`) gives you access to every message, frame, and
  handshake primitive and is useful if you already have a transport layer or
  want to implement a custom state machine.
- **SDK** (`alpine-protocol-sdk`) builds on top of the protocol layer and manages sockets,
  keep-alive, and profile lifetimes so you can focus on discovery → connect →
  streaming flows.

## Quick lifecycle

1. Fetch and cache the root-signed attesters bundle (e.g., from Ops) and build a trust view.
2. Use the discovery runner (recommended) or `DiscoveryClient` (low-level) to find devices.
   The runner performs unicast/broadcast/cached fallbacks and optional subnet scans.
3. Call `AlpineClient::connect` with the discovered identity, capability set,
   and a credential pair; the SDK spins up the transport plus the keep-alive task.
4. Call `AlpineClient::start_stream`, pass a `StreamProfile`, and track the
   returned `config_id`.
5. Use `send_frame` to push encoded `FrameEnvelope`s or `send_control` for
   control envelopes.
6. Call `AlpineClient::ping`, `status`, `health`, `identity`, or `metadata` to
   send the corresponding control command and receive typed replies when the
   device returns structured CBOR payloads.

## Example

```ignore
use alpine_protocol_sdk::{
    AlpineClient,
    DiscoveryRunOptions,
    run_discovery_with_options,
    TrustConfig,
    load_or_fetch_trust_view,
};
use alpine_protocol_rs::{crypto::identity::NodeCredentials, messages::DeviceIdentity};
use std::net::{SocketAddr, IpAddr, Ipv4Addr};

#[tokio::main]
async fn main() -> Result<(), alpine_protocol_sdk::AlpineSdkError> {
    let trust = TrustConfig::new("https://panel.y-link.no/attesters/latest".into())
        .with_root_pubkey(alpine_protocol_sdk::parse_root_pubkey_base64(
            "BASE64_ED25519_ROOT_PUBKEY",
        )?);
    let trust_view = load_or_fetch_trust_view(&trust).await?;

    let mut opts = DiscoveryRunOptions::default();
    opts.attester_registry = Some(trust_view.registry.clone());
    let outcome = run_discovery_with_options(
        SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), 19455),
        opts,
    )
    .await?;

    let remote = SocketAddr::new(outcome.peer.ip(), outcome.peer.port());
    let identity = DeviceIdentity {
        device_id: outcome.reply.device_id.clone(),
        manufacturer_id: outcome.reply.manufacturer_id.clone(),
        model_id: outcome.reply.model_id.clone(),
        hardware_rev: outcome.reply.hardware_rev.clone(),
        firmware_rev: outcome.reply.firmware_rev.clone(),
    };
    let credentials = NodeCredentials::load("path/to/credentials")?;
    let capabilities = outcome.reply.capabilities.clone();

    let mut client = AlpineClient::connect(
        SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
        remote,
        identity,
        capabilities,
        credentials,
    )
    .await?;
    let config_id = client.start_stream(alpine_protocol_rs::profile::StreamProfile::auto())?;
    println!("Streaming with config id {}", config_id);
    Ok(())
}
```

## Discovery runner behavior

The runner is designed to "just work" on most networks:
- Unicast (if a concrete target is provided).
- Broadcast on all IPv4 interfaces (default).
- Cached unicast fallbacks (if you provide cached targets).
- Optional multicast (`prefer_multicast = true`).
- Optional subnet scan (opt-in via `scan_subnets = true`).

These behaviors are explicit in `DiscoveryRunOptions`; aggressive scanning is opt-in.

## Discovery diagnostics

If you need per-attempt visibility (e.g., broadcast blocked vs unicast denied),
use the report helpers:

- `run_discovery_with_report`
- `run_discovery_with_options_report`

These return a `DiscoveryRunReport` with a `result` plus a list of `DiscoveryAttempt`
entries that include target, mode, local bind, and any error details observed.

Common discovery error hints:
- `discovery channel permission denied` → UDP send/recv blocked by OS policy or firewall.
- `broadcast discovery blocked` → network policy prevents broadcast.
- `multicast discovery unavailable` → multicast not permitted on this network.
- `discovery timed out` → no replies observed before the timeout.

Discovery trust state:
- Use `DiscoveryOutcome::trust_state()` to get a strict `DeviceTrustState` enum
  instead of parsing the optional attestation error string.
- Use `DiscoveryOutcome::require_trusted()` to obtain a `TrustedDiscoveryOutcome`
  or a clear `AlpineSdkError::UntrustedDevice`.

Discovery retries:
- Use `run_discovery_with_retry` or `run_discovery_with_options_retry` with a
  `DiscoveryRetryPolicy` to add exponential backoff across discovery runs.

## Trust bundle setup

To verify device identity attestations, you must supply a root public key and bundle URL.
The SDK does not auto-load environment variables; your application should configure them.

Recommended variables for your app or CLI:
- `ALPINE_ATTESTERS_URL`
- `ALPINE_ROOT_PUBKEY_B64`

## Control helper note

The `ping`, `status`, `health`, `identity`, and `metadata` helpers send a vendor
control command payload (`{ "command": "status" }`) via `ControlOp::Vendor`.
Ensure your device firmware responds to those vendor commands (not only `get_status`).

## Standardized status probe

Use `AlpineClient::probe_status()` for a single canonical liveness + health check.
It runs `ping`, then `status`, and optionally `health` if status is unavailable.
The `ProbeResult` includes a normalized `ProbeState` (`Online`, `Degraded`, `Offline`)
along with per-step errors and round-trip timings.

Use `ProbeResult::to_device_state(trust_state)` to normalize probe + trust into a
`DeviceState` snapshot suitable for UI status badges.

## Control options

Use `AlpineClient::control_with_options` with `ControlOptions` to add per-call
timeouts and retries. The retry policy uses exponential backoff capped by
`backoff_max_ms`.

## Connect policy

Use `connect_with_policy` to enforce discovery → trust → handshake → first probe.
The default `ConnectPolicy` requires a trusted identity, probes after handshake,
and rejects `Offline` (and optionally `Degraded`) devices.

If you have a `DiscoveryRunReport`, use `connect_with_policy_from_report` to
include discovery attempt diagnostics in any discovery/probe failure.

## LAN defaults and cache helpers

Use `DiscoveryRunOptions::defaults_for_lan()` for a safe LAN baseline.
Use `discover_with_cache` or `discover_with_cache_report` to try cached targets
before falling back to broadcast.

Every exported module in this crate has `///` documentation so the generated
docs on docs.rs describe the same lifecycle described here.

## Discovery dry run

Use `discovery_dry_run` to inspect the computed interfaces and broadcast targets
without sending any packets. This is useful when debugging permissions or NIC
selection issues.

## Standard vs vendor status

If your device implements the standard control op, call
`AlpineClient::status_standard()` (uses `ControlOp::GetStatus`).
If your device only implements vendor commands, call `status_vendor()` or
`status()` (vendor command payload).

## Safe client wrapper

`SafeClient` wraps an `AlpineClient` and enforces a probe before control or
stream calls. Use `SafeClientOptions` to require online or accept degraded.

## Session guard

`SessionGuard` tracks idle time and lets you expire or close sessions after
an inactivity timeout.

## Trust policy helper

Use `enforce_trust_policy` with `TrustPolicy` to apply strict or warn-only
trust behavior based on `DeviceTrustState`.

## Logging helpers

Use `init_pretty_logging()` or `init_json_logging()` to set up a default
`tracing` subscriber with env-based filtering.

## Troubleshooting

See `sdk/docs/troubleshooting.md` for common failure modes and debugging tips.

## Common failure modes and remediation

- Discovery timed out: confirm the device is powered on, on the same subnet, and UDP is permitted.
- Broadcast or multicast blocked: switch to unicast or cached targets; ask network admin to allow UDP broadcast.
- Device untrusted: load a trusted attesters bundle and validate `ALPINE_ROOT_PUBKEY_B64`.
- Standard status rejected: use `status_vendor()` or `status()` for vendor-only devices.
- Incompatible protocol: upgrade/downgrade SDK or firmware to match `ALPINE_VERSION`.
- Unsupported environment: avoid WSL/Docker without host networking for UDP discovery/control.