← Back to SDKs TS
TypeScript —
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-sdkQuick 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
| Kind | OAuth2 Flow | SDK Methods |
|---|---|---|
| Agent / Desktop / VsCode | Device Auth Grant (RFC 8628) | login(onUserCode) |
| Web | Auth Code + PKCE (RFC 7636) | loginWithAuthCode or getAuthorizationUrl + exchangeCode |
| Mobile | Auth Code + PKCE (RFC 7636) | getAuthorizationUrl + exchangeCode |
Gateway Client API
| Method | Description |
|---|---|
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)
| Method | Purpose |
|---|---|
discover | OIDC discovery |
login | Device code login |
loginWithAuthCode | Auth code + PKCE (local callback) |
getAuthorizationUrl, exchangeCode | Auth code + PKCE (manual redirect) |
refreshTokenAsync, getAccessToken | Token refresh / access |
logout | Sign 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.routingTokenPre-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 useConsent 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 keypairsodium.crypto_scalarmult()— X25519 ECDH scalar multiplicationcrypto.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 startPrerequisites: Node.js 18+. Configuration saved to ~/.airlock/test-enforcer-typescript.json.