← Back to SDKs Go
Go —
Go
Go — airlock
A Go client SDK for the Airlock Integrations Gateway API. Requires Go 1.21+.
Installation
Bash
go get github.com/airlockapp/gateway-clients/src/go/airlock go get github.com/airlockapp/gateway-clients/src/go/airlock Quick Start
With Bearer Token
Go
package main
import (
"fmt"
airlock "github.com/airlockapp/gateway-clients/src/go/airlock"
)
func main() {
client := airlock.NewClient("https://<your-gateway-host>", "your-token")
echo, err := client.Echo()
if err != nil {
panic(err)
}
fmt.Println(echo)
} package main
import (
"fmt"
airlock "github.com/airlockapp/gateway-clients/src/go/airlock"
)
func main() {
client := airlock.NewClient("https://<your-gateway-host>", "your-token")
echo, err := client.Echo()
if err != nil {
panic(err)
}
fmt.Println(echo)
} With Enforcer App Credentials
Go
client := airlock.NewClientWithCredentials(
"https://<your-gateway-host>",
"your-client-id",
"your-client-secret",
) client := airlock.NewClientWithCredentials(
"https://<your-gateway-host>",
"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.
Go
// After obtaining a PAT from your org admin or Mobile Approver
client.SetPat("airlock_pat_...")
// Clear PAT when no longer needed
client.SetPat("") // After obtaining a PAT from your org admin or Mobile Approver
client.SetPat("airlock_pat_...")
// Clear PAT when no longer needed
client.SetPat("") Dual Auth (SetBearerToken)
Go
// After user login (Device Auth Grant or Auth Code + PKCE)
client.SetBearerToken(accessToken) // 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 |
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) |
RefreshToken, GetAccessToken | Token refresh / access |
Logout | Sign out (revoke) |
Pairing
Standard Pairing (Enforcer-Initiated)
Go
// 1. Initiate a pairing session
resp, err := client.InitiatePairing(airlock.PairingInitiateRequest{
EnforcerID: "my-enforcer",
WorkspaceName: "my-project",
X25519PublicKey: myPublicKey,
})
// 2. Display pairing code to user
fmt.Printf("Pairing code: %s\n", resp.PairingCode)
// 3. Poll for approval from the Mobile Approver
status, err := client.GetPairingStatus(resp.Nonce)
// status.State == "Completed" → save status.RoutingToken // 1. Initiate a pairing session
resp, err := client.InitiatePairing(airlock.PairingInitiateRequest{
EnforcerID: "my-enforcer",
WorkspaceName: "my-project",
X25519PublicKey: myPublicKey,
})
// 2. Display pairing code to user
fmt.Printf("Pairing code: %s\n", resp.PairingCode)
// 3. Poll for approval from the Mobile Approver
status, err := client.GetPairingStatus(resp.Nonce)
// status.State == "Completed" → save status.RoutingToken Pre-Generated Code Pairing (Approver-Initiated)
Go
claim, err := client.ClaimPairing(airlock.PairingClaimRequest{
Code: "ABCD-1234",
EnforcerID: "my-enforcer",
WorkspaceName: "my-project",
X25519PublicKey: myPublicKey,
})
// claim.RoutingToken is ready to use claim, err := client.ClaimPairing(airlock.PairingClaimRequest{
Code: "ABCD-1234",
EnforcerID: "my-enforcer",
WorkspaceName: "my-project",
X25519PublicKey: myPublicKey,
})
// claim.RoutingToken is ready to use Consent Check
Go
status, err := client.CheckConsent()
if err != nil {
var gwErr *airlock.GatewayError
if errors.As(err, &gwErr) {
switch gwErr.ErrorCode {
case "app_consent_required":
// User hasn't granted consent
case "app_consent_pending":
// Consent request sent, waiting for approval
}
}
} status, err := client.CheckConsent()
if err != nil {
var gwErr *airlock.GatewayError
if errors.As(err, &gwErr) {
switch gwErr.ErrorCode {
case "app_consent_required":
// User hasn't granted consent
case "app_consent_pending":
// Consent request sent, waiting for approval
}
}
} Submit and Poll
Go
// Submit an artifact for approval
requestID, err := client.SubmitArtifact(airlock.ArtifactSubmitRequest{
EnforcerID: "my-enforcer",
ArtifactHash: "sha256-hash",
Ciphertext: airlock.EncryptedPayload{
Alg: "aes-256-gcm",
Data: "base64-encrypted-content",
Nonce: "nonce",
Tag: "tag",
},
Metadata: map[string]string{"routingToken": "rt-abc"},
})
if err != nil {
panic(err)
}
// Wait for a decision (long-poll)
decision, err := client.WaitForDecision(requestID, 30)
if err != nil {
panic(err)
}
if decision != nil && decision.Body.IsApproved() {
fmt.Printf("Approved: %s\n", decision.Body.Reason)
} // Submit an artifact for approval
requestID, err := client.SubmitArtifact(airlock.ArtifactSubmitRequest{
EnforcerID: "my-enforcer",
ArtifactHash: "sha256-hash",
Ciphertext: airlock.EncryptedPayload{
Alg: "aes-256-gcm",
Data: "base64-encrypted-content",
Nonce: "nonce",
Tag: "tag",
},
Metadata: map[string]string{"routingToken": "rt-abc"},
})
if err != nil {
panic(err)
}
// Wait for a decision (long-poll)
decision, err := client.WaitForDecision(requestID, 30)
if err != nil {
panic(err)
}
if decision != nil && decision.Body.IsApproved() {
fmt.Printf("Approved: %s\n", decision.Body.Reason)
} Error Handling
All errors use *GatewayError with helper methods:
Go
requestID, err := client.SubmitArtifact(req)
if err != nil {
var gwErr *airlock.GatewayError
if errors.As(err, &gwErr) {
if gwErr.IsQuotaExceeded() { /* 429 */ }
if gwErr.IsPairingRevoked() { /* 403 pairing_revoked */ }
if gwErr.IsConflict() { /* 409 */ }
}
} requestID, err := client.SubmitArtifact(req)
if err != nil {
var gwErr *airlock.GatewayError
if errors.As(err, &gwErr) {
if gwErr.IsQuotaExceeded() { /* 429 */ }
if gwErr.IsPairingRevoked() { /* 403 pairing_revoked */ }
if gwErr.IsConflict() { /* 409 */ }
}
} Encryption
Uses X25519 ECDH via golang.org/x/crypto:
GenerateX25519KeyPair()— 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.
Bash
# From the repo root
cd src/go
go run ./cmd/test-enforcer # From the repo root
cd src/go
go run ./cmd/test-enforcer Prerequisites: Go 1.21+. Configuration saved to ~/.airlock/test-enforcer-go.json.