openlatch-provider 0.2.2

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
# Authoring a tool — Rust (axum)

Bare-axum guide. v0.1 has no `openlatch-tool-sdk` Rust crate yet
(deferred to v0.2 per PRD §10.1) — write the verifier inline. The
codebase's own `runtime/webhook.rs::verify` is the reference impl.

## Scaffold

```bash
openlatch-provider new tool --template rust --out my-tool
cd my-tool
cargo build
```

## Minimum viable tool

```rust
use axum::{
    body::Bytes,
    extract::State,
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
    routing::post,
    Router,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;

type HmacSha256 = Hmac<Sha256>;

#[derive(Clone)]
struct AppState {
    whsec: Arc<Vec<u8>>,  // already base64-decoded, whsec_ prefix stripped
}

#[tokio::main]
async fn main() {
    let whsec = std::fs::read(".secret").unwrap();
    let app = Router::new()
        .route("/event", post(handle_event))
        .with_state(AppState { whsec: Arc::new(whsec) });
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn handle_event(
    State(state): State<AppState>,
    headers: HeaderMap,
    body: Bytes,
) -> Response {
    let id = headers.get("webhook-id").and_then(|v| v.to_str().ok()).unwrap_or("");
    let ts = headers.get("webhook-timestamp")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<i64>().ok())
        .unwrap_or(0);
    let sig = headers.get("webhook-signature").and_then(|v| v.to_str().ok()).unwrap_or("");

    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
    if (now - ts).abs() > 300 {
        return (StatusCode::UNAUTHORIZED, "timestamp skew").into_response();
    }

    let mut mac = HmacSha256::new_from_slice(&state.whsec).unwrap();
    mac.update(format!("{id}.{ts}.").as_bytes());
    mac.update(&body);
    let expected = mac.finalize().into_bytes();

    let presented_b64 = sig.split(',').nth(1).unwrap_or("");
    use base64::engine::general_purpose::STANDARD;
    use base64::Engine;
    let presented = STANDARD.decode(presented_b64).unwrap_or_default();

    if !bool::from(expected.as_slice().ct_eq(&presented)) {
        return (StatusCode::UNAUTHORIZED, "signature mismatch").into_response();
    }

    let event: serde_json::Value = match serde_json::from_slice(&body) {
        Ok(v) => v,
        Err(_) => return (StatusCode::BAD_REQUEST, "malformed body").into_response(),
    };

    // ---- detection ----
    let _ = event;

    axum::Json(serde_json::json!({
        "riskScore": 5,
        "verdictHint": "allow",
        "latencyMs": 1
    }))
    .into_response()
}
```

## Best practices

- **rustls only**`openssl-sys` and `native-tls` MUST NOT appear in
  your dep tree. Match `openlatch-provider`'s own posture.
- **Constant-time compare** via `subtle::ConstantTimeEq`. Never `==`.
- **Body size cap** via `tower-http::limit::RequestBodyLimitLayer`.
- **Per-binding semaphore** if your detector is CPU-heavy and you want
  back-pressure independent of tokio's task pool.

## Verdict shape

See the typify-generated structs that openlatch-provider itself uses
in `src/generated/types.rs` once you've cargo-installed the schemas
locally. The shape is camelCase and matches the platform's
`ProviderCall` DTO — see `webhook-security.md` for the field table.

## v0.2 roadmap

A Rust `openlatch-tool-sdk` crate is on the v0.2 roadmap (per
PRD §10.1). It will export an `axum::Layer` that handles verification
+ verdict shaping out of the box, mirroring the Python and TS SDKs.