← Back to SDKs

TS TypeScript — @airlockapp/gateway-sdk

A zero-dependency TypeScript client SDK for the Airlock Integrations Gateway API. Uses native fetch — works in Node.js 18+ and modern browsers.

Installation

npm install @airlockapp/gateway-sdk

Quick Start

With Bearer Token

import { AirlockGatewayClient } from "@airlockapp/gateway-sdk";

const client = new AirlockGatewayClient({
  baseUrl: "https://igw.airlocks.io",
  token: "your-token",
});

With Enforcer App Credentials

const client = new AirlockGatewayClient({
  baseUrl: "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(undefined);

Dual Auth (setBearerToken)

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

Authentication by Enforcer App Kind

KindOAuth2 FlowSDK Methods
Agent / Desktop / VsCodeDevice Auth Grant (RFC 8628)login(onUserCode)
WebAuth Code + PKCE (RFC 7636)loginWithAuthCode or getAuthorizationUrl + exchangeCode
MobileAuth Code + PKCE (RFC 7636)getAuthorizationUrl + exchangeCode

Gateway Client API

MethodDescription
echo()Gateway discovery / health
setPat(pat)Set Personal Access Token (X-PAT header)
setBearerToken(token)Set Bearer token for user-scoped operations
checkConsent()Check user consent for this enforcer app
submitArtifact(request)Submit artifact for approval
encryptAndSubmitArtifact(request)JCS + SHA-256 + AES-GCM encrypt, then submit (Node.js)
getExchangeStatus(requestId)Get exchange status
waitForDecision(requestId, timeout)Long-poll for decision
submitAck(msgId, requestId?)Acknowledge decision delivery (POST /v1/acks, fire-and-forget)
withdrawExchange(requestId)Withdraw pending exchange
initiatePairing(request)Start pairing session
claimPairing(request)Claim a pre-generated pairing code
getPairingStatus(nonce)Poll pairing status
revokePairing(routingToken)Revoke a pairing
sendHeartbeat(request)Presence heartbeat
getEffectiveDndPolicies(enforcerId, workspaceId, sessionId?)Fetch effective DND policies

Auth Client (AirlockAuthClient)

MethodPurpose
discoverOIDC discovery
loginDevice code login
loginWithAuthCodeAuth code + PKCE (local callback)
getAuthorizationUrl, exchangeCodeAuth code + PKCE (manual redirect)
refreshTokenAsync, getAccessTokenToken refresh / access
logoutSign out (revoke)

Pairing

Standard Pairing (Enforcer-Initiated)

// 1. Initiate a pairing session
const resp = await client.initiatePairing({
    enforcerId: "my-enforcer",
    workspaceName: "my-project",
    x25519PublicKey: myPublicKey,
});

// 2. Display pairing code to user
console.log(`Pairing code: ${resp.pairingCode}`);

// 3. Poll for approval from the mobile app
const status = await client.getPairingStatus(resp.nonce);
// status.state === "Completed" → save status.routingToken

Pre-Generated Code Pairing (Approver-Initiated)

const claim = await client.claimPairing({
    code: "ABCD-1234",
    enforcerId: "my-enforcer",
    workspaceName: "my-project",
    x25519PublicKey: myPublicKey,
});
// claim.routingToken is ready to use

Consent Check

import { AirlockGatewayError } from "@airlockapp/gateway-sdk";

try {
    const status = await client.checkConsent();
    // status === "approved" — proceed normally
} catch (e) {
    if (e instanceof AirlockGatewayError) {
        if (e.errorCode === "app_consent_required") {
            // User hasn't granted consent
        } else if (e.errorCode === "app_consent_pending") {
            // Consent request sent, waiting for approval
        }
    }
}

Submit and Poll

Encrypt and Submit (Node.js)

const requestId = await client.encryptAndSubmitArtifact({
    enforcerId: "my-enforcer",
    plaintextPayload: JSON.stringify({ kind: "shell", cmd: "npm publish" }),
    encryptionKeyBase64Url: derivedAes256KeyFromPairing,
    metadata: { routingToken: "rt-abc" },
});

Manual Ciphertext

// Submit an artifact for approval
const requestId = await client.submitArtifact({
    enforcerId: "my-enforcer",
    artifactHash: "sha256-hash",
    ciphertext: {
        alg: "aes-256-gcm",
        data: "base64-encrypted-content",
        nonce: "nonce",
        tag: "tag",
    },
    metadata: { routingToken: "rt-abc" },
});

// Wait for a decision (long-poll)
const decision = await client.waitForDecision(requestId, 30);
if (decision?.body?.decision === "approve") {
    console.log(`Approved: ${decision.body.reason}`);
}

Error Handling

All errors throw AirlockGatewayError with helper getters:

import { AirlockGatewayError } from "@airlockapp/gateway-sdk";

try {
    await client.submitArtifact(request);
} catch (e) {
    if (e instanceof AirlockGatewayError) {
        if (e.isQuotaExceeded) { /* 429 */ }
        if (e.isPairingRevoked) { /* 403 pairing_revoked */ }
        if (e.isConflict) { /* 409 */ }
        console.error(`Error ${e.statusCode}: ${e.message}`);
    }
}

Encryption

Uses X25519 ECDH via libsodium-wrappers-sumo:

  • sodium.crypto_box_keypair() — generate X25519 keypair
  • sodium.crypto_scalarmult() — X25519 ECDH scalar multiplication
  • crypto.hkdfSync('sha256', ...) — HKDF-SHA256 (info: HARP-E2E-AES256GCM)

Test Enforcer CLI

A fully interactive TUI that demonstrates the complete enforcer lifecycle.

# From the repo root — build the SDK first (required once)
cd src/typescript
npm install
npm run build

# Run the test enforcer
cd test-enforcer
npm install
npm start

Prerequisites: Node.js 18+. Configuration saved to ~/.airlock/test-enforcer-typescript.json.