← Back to SDKs

C# .NET — Airlock.Gateway.Sdk

A .NET 8+ client SDK for the Airlock Integrations Gateway API.

Installation

dotnet add package Airlock.Gateway.Sdk

Quick Start

With Bearer Token

using Airlock.Gateway.Sdk;
using Airlock.Gateway.Sdk.Models;

var client = new AirlockGatewayClient("https://igw.airlocks.io", bearerToken: "your-jwt-token");

With Enforcer App Credentials

var client = new AirlockGatewayClient(
    "https://igw.airlocks.io",
    clientId: "your-client-id",
    clientSecret: "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.SetPat("airlock_pat_...");

// Clear PAT when no longer needed
client.SetPat(null);

Dual Auth (SetBearerToken)

After creating a client with credentials, set a user's Bearer token for user-scoped operations.

// After user login (Device Auth Grant or Auth Code + PKCE)
client.SetBearerToken(accessToken);

Authentication by Enforcer App Kind

Kind OAuth2 Flow SDK Methods
Agent / Desktop / VsCodeDevice Auth Grant (RFC 8628)LoginAsync(onUserCode)
WebAuth Code + PKCE (RFC 7636)LoginWithAuthCodeAsync or GetAuthorizationUrlAsync + ExchangeCodeAsync
MobileAuth Code + PKCE (RFC 7636)GetAuthorizationUrlAsync + ExchangeCodeAsync

Gateway Client API

MethodDescription
EchoAsync()Gateway discovery / health
SetPat(pat)Set Personal Access Token (X-PAT header)
SetBearerToken(token)Set Bearer token for user-scoped operations
CheckConsentAsync()Check user consent for this enforcer app
SubmitArtifactAsync(request)Submit artifact for approval
EncryptAndSubmitArtifactAsync(request)Encrypt plaintext and submit as artifact
GetExchangeStatusAsync(requestId)Get exchange status
WaitForDecisionAsync(requestId, timeout)Long-poll for decision
SubmitAckAsync(msgId, requestId?)Acknowledge decision delivery (POST /v1/acks, fire-and-forget)
VerifyDecision(decision, hash, publicKey)Verify decision signature and binding
WithdrawExchangeAsync(requestId)Withdraw pending exchange
InitiatePairingAsync(request)Start pairing session
ClaimPairingAsync(request)Claim a pre-generated pairing code
GetPairingStatusAsync(nonce)Poll pairing status
RevokePairingAsync(routingToken)Revoke a pairing
SendHeartbeatAsync(request)Presence heartbeat
GetEffectiveDndPoliciesAsync(enforcerId, workspaceId, sessionId)Fetch effective DND policies

Auth Client (AirlockAuthClient)

MethodPurpose
DiscoverAsyncLoad OIDC discovery
LoginAsyncDevice code login
LoginWithAuthCodeAsyncAuth code + PKCE (local callback)
GetAuthorizationUrlAsync, ExchangeCodeAsyncAuth code + PKCE (manual redirect)
RefreshTokenAsync, GetAccessTokenAsyncRefresh / access token
LogoutAsyncSign out (revoke)

Pairing

Standard Pairing (Enforcer-Initiated)

// 1. Initiate a pairing session
var pairing = await client.InitiatePairingAsync(new PairingInitiateRequest
{
    EnforcerId = "my-enforcer",
    WorkspaceName = "my-project",
    X25519PublicKey = myPublicKey, // For E2E encryption key exchange
});

// 2. Display pairing code to user (or show QR)
Console.WriteLine($"Pairing code: {pairing.PairingCode}");

// 3. Poll for approval from the mobile app
var status = await client.GetPairingStatusAsync(pairing.Nonce);
if (status.State == "Completed")
{
    var routingToken = status.RoutingToken; // Save for future requests
}

Pre-Generated Code Pairing (Approver-Initiated)

// Claim a pre-generated code from the mobile app
var claim = await client.ClaimPairingAsync(new PairingClaimRequest
{
    Code = "ABCD-1234",             // Code from the mobile app
    EnforcerId = "my-enforcer",
    WorkspaceName = "my-project",
    X25519PublicKey = myPublicKey,
});

// The response contains the routing token and pairing details
var routingToken = claim.RoutingToken;

Consent Check

try
{
    var status = await client.CheckConsentAsync();
    // status == "approved" — proceed normally
}
catch (AirlockGatewayException ex) when (ex.ErrorCode == "app_consent_required")
{
    // User hasn't granted consent — prompt them to approve in the mobile app
}
catch (AirlockGatewayException ex) when (ex.ErrorCode == "app_consent_pending")
{
    // Consent request sent, waiting for user approval
}

Submit and Poll

// Submit an artifact for approval
var requestId = await client.SubmitArtifactAsync(new ArtifactSubmitRequest
{
    EnforcerId = "my-enforcer",
    ArtifactHash = "sha256-hash",
    Ciphertext = new EncryptedPayload
    {
        Alg = "aes-256-gcm",
        Data = "base64-encrypted-content",
        Nonce = "nonce",
        Tag = "tag"
    },
    Metadata = new Dictionary<string, string>
    {
        ["routingToken"] = "rt-abc"
    }
});

// Wait for a decision (long-poll)
var decision = await client.WaitForDecisionAsync(requestId, timeoutSeconds: 30);
if (decision?.Body?.IsApproved == true)
{
    Console.WriteLine($"Approved: {decision.Body.Reason}");
}

Transparent Encryption (Encrypt + Submit)

var requestId = await client.EncryptAndSubmitArtifactAsync(new EncryptedArtifactRequest
{
    EnforcerId = "my-enforcer",
    Plaintext = "the content to approve",
    RoutingToken = "rt-abc",
    EncryptionKey = sharedAesKey, // Derived from X25519 ECDH during pairing
});

Decision Verification

var result = client.VerifyDecision(decision, expectedArtifactHash, signerPublicKeyBase64Url);
if (result.IsValid)
{
    Console.WriteLine($"Verified decision: {result.Decision}");
}
else
{
    Console.WriteLine($"Verification failed: {result.FailureReason}");
}

Error Handling

All errors throw AirlockGatewayException with helper properties:

try
{
    await client.SubmitArtifactAsync(request);
}
catch (AirlockGatewayException ex) when (ex.IsQuotaExceeded)
{
    // Handle quota exceeded (429)
}
catch (AirlockGatewayException ex) when (ex.IsPairingRevoked)
{
    // Handle revoked pairing (403)
}
catch (AirlockGatewayException ex) when (ex.IsConflict)
{
    // Handle idempotency conflict (409)
}

Encryption

The SDK includes CryptoHelpers for X25519 ECDH key exchange and AES-256-GCM encryption/decryption using NSec.Cryptography:

  • GenerateX25519KeyPair() — generates a raw 32-byte X25519 keypair (base64url)
  • DeriveSharedKey(myPrivate, peerPublic) — ECDH + HKDF-SHA256 (info: HARP-E2E-AES256GCM)
  • AesGcmEncrypt / AesGcmDecrypt — AES-256-GCM with detached nonce and tag

Test Enforcer CLI

A fully interactive TUI that demonstrates the complete enforcer lifecycle — setup wizard, Device Auth Grant sign-in, PAT configuration, consent check, workspace pairing, artifact submission, and sign-out.

# From the repo root
cd src/dotnet

# Run the test enforcer
dotnet run --project Airlock.Gateway.Sdk.TestEnforcer

Prerequisites: .NET 10 SDK. Configuration saved to ~/.airlock/test-enforcer.json.