← Back to SDKs

Rs Rust — airlock-gateway

An async Rust client SDK for the Airlock Integrations Gateway API. Uses reqwest + tokio.

Installation

[dependencies]
airlock-gateway = "0.1"

Quick Start

With Bearer Token

use airlock_gateway::*;

let client = AirlockGatewayClient::new(
    "https://igw.airlocks.io",
    Some("your-token"),
);

With Enforcer App Credentials

use airlock_gateway::*;

let client = AirlockGatewayClient::with_credentials(
    "https://igw.airlocks.io",
    "your-client-id",
    "your-client-secret",
);

Authentication

Personal Access Token (PAT)

PAT is the recommended authentication for user-scoped operations. Sent via the X-PAT header.

// After obtaining a PAT from the mobile app (Settings → Access Tokens)
client.set_pat(Some("airlock_pat_...".to_string()));

// Clear PAT when no longer needed
client.set_pat(None);

Dual Auth (set_bearer_token)

// After user login (Device Auth Grant or Auth Code + PKCE)
client.set_bearer_token(Some(access_token));

Authentication by Enforcer App Kind

KindOAuth2 FlowSDK Methods
Agent / Desktop / VsCodeDevice Auth Grant (RFC 8628)login(on_user_code)
WebAuth Code + PKCE (RFC 7636)login_with_auth_code or get_authorization_url + exchange_code
MobileAuth Code + PKCE (RFC 7636)get_authorization_url + exchange_code

Gateway Client API

MethodDescription
echo()Gateway discovery / health
set_pat(pat)Set Personal Access Token (X-PAT header)
set_bearer_token(token)Set Bearer token for user-scoped operations
check_consent()Check user consent for this enforcer app
submit_artifact(request)Submit artifact for approval
encrypt_and_submit_artifact(request)Encrypt plaintext and submit as artifact
get_exchange_status(request_id)Get exchange status
wait_for_decision(request_id, timeout)Long-poll for decision
submit_ack(msg_id, request_id)Acknowledge decision delivery (POST /v1/acks, fire-and-forget)
verify_decision(decision, hash, pub_key)Verify decision signature and binding
withdraw_exchange(request_id)Withdraw pending exchange
initiate_pairing(request)Start pairing session
claim_pairing(request)Claim a pre-generated pairing code
get_pairing_status(nonce)Poll pairing status
revoke_pairing(routing_token)Revoke a pairing
send_heartbeat(request)Presence heartbeat
get_effective_dnd_policies(enforcer_id, workspace_id, session_id)Fetch effective DND policies

Auth Client (AirlockAuthClient)

MethodPurpose
discoverOIDC discovery
loginDevice code login
login_with_auth_codeAuth code + PKCE (local callback)
get_authorization_url, exchange_codeAuth code + PKCE (manual redirect)
refresh_token, get_access_tokenToken refresh / access
logoutSign out (revoke)

Pairing

Standard Pairing (Enforcer-Initiated)

// 1. Initiate a pairing session
let resp = client.initiate_pairing(PairingInitiateRequest {
    enforcer_id: "my-enforcer".into(),
    workspace_name: "my-project".into(),
    x25519_public_key: my_public_key.clone(),
}).await?;

// 2. Display pairing code to user
println!("Pairing code: {}", resp.pairing_code);

// 3. Poll for approval from the mobile app
let status = client.get_pairing_status(&resp.nonce).await?;
// status.state == "Completed" → save status.routing_token

Pre-Generated Code Pairing (Approver-Initiated)

let claim = client.claim_pairing(PairingClaimRequest {
    code: "ABCD-1234".into(),
    enforcer_id: "my-enforcer".into(),
    workspace_name: "my-project".into(),
    x25519_public_key: my_public_key.clone(),
}).await?;
// claim.routing_token is ready to use

Consent Check

match client.check_consent().await {
    Ok(status) => {
        // status == "approved" — proceed normally
    }
    Err(GatewayError::ApiError { error_code, .. })
        if error_code == "app_consent_required" =>
    {
        // User hasn't granted consent
    }
    Err(GatewayError::ApiError { error_code, .. })
        if error_code == "app_consent_pending" =>
    {
        // Consent request sent, waiting for approval
    }
    Err(e) => return Err(e),
}

Submit and Poll

// Submit an artifact for approval
let request_id = client.submit_artifact(ArtifactSubmitRequest {
    enforcer_id: "my-enforcer".into(),
    artifact_hash: "sha256-hash".into(),
    ciphertext: EncryptedPayload {
        alg: "aes-256-gcm".into(),
        data: "base64-encrypted-content".into(),
        nonce: Some("nonce".into()),
        tag: Some("tag".into()),
        aad: None,
    },
    artifact_type: None,
    expires_at: None,
    metadata: None,
    request_id: None,
}).await?;

// Wait for a decision (long-poll)
if let Some(decision) = client.wait_for_decision(&request_id, 30).await? {
    if let Some(body) = &decision.body {
        if body.is_approved() {
            println!("Approved: {:?}", body.reason);
        }
    }
}

Transparent Encryption (Encrypt + Submit)

let request_id = client.encrypt_and_submit_artifact(EncryptedArtifactRequest {
    enforcer_id: "my-enforcer".into(),
    plaintext: "the content to approve".into(),
    routing_token: "rt-abc".into(),
    encryption_key: shared_aes_key,
}).await?;

Error Handling

All errors use GatewayError enum variants:

match client.submit_artifact(request).await {
    Ok(id) => println!("Submitted: {id}"),
    Err(GatewayError::QuotaExceeded { .. }) => { /* 429 */ }
    Err(GatewayError::PairingRevoked { .. }) => { /* 403 */ }
    Err(GatewayError::Conflict { .. }) => { /* 409 */ }
    Err(e) => eprintln!("Error: {e}"),
}

Encryption

Uses X25519 ECDH via x25519-dalek and AES-256-GCM via aes-gcm:

  • generate_x25519_keypair() — raw 32-byte X25519 keypair (base64url)
  • derive_shared_key(my_private, peer_public) — ECDH + HKDF-SHA256 (info: HARP-E2E-AES256GCM)
  • aes_gcm_encrypt / aes_gcm_decrypt — AES-256-GCM with detached nonce and tag

Test Enforcer CLI

A fully interactive TUI that demonstrates the complete enforcer lifecycle.

# From the repo root
cd src/rust

cargo run --bin test-enforcer

Prerequisites: Rust 1.75+. Configuration saved to ~/.airlock/test-enforcer-rust.json.