← Back to SDKs Rs
Rust —
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
| Kind | OAuth2 Flow | SDK Methods |
|---|---|---|
| Agent / Desktop / VsCode | Device Auth Grant (RFC 8628) | login(on_user_code) |
| Web | Auth Code + PKCE (RFC 7636) | login_with_auth_code or get_authorization_url + exchange_code |
| Mobile | Auth Code + PKCE (RFC 7636) | get_authorization_url + exchange_code |
Gateway Client API
| Method | Description |
|---|---|
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)
| Method | Purpose |
|---|---|
discover | OIDC discovery |
login | Device code login |
login_with_auth_code | Auth code + PKCE (local callback) |
get_authorization_url, exchange_code | Auth code + PKCE (manual redirect) |
refresh_token, get_access_token | Token refresh / access |
logout | Sign 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_tokenPre-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 useConsent 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-enforcerPrerequisites: Rust 1.75+. Configuration saved to ~/.airlock/test-enforcer-rust.json.