@agentfield/hax-sdk
v0.2.8
Published
TypeScript SDK for the HAX (Human Approval eXchange) API
Maintainers
Readme
HAX TypeScript SDK
Official TypeScript SDK for the HAX (Human Approval eXchange) API.
Installation
npm install @agentfield/hax-sdk
# or
yarn add @agentfield/hax-sdk
# or
pnpm add @agentfield/hax-sdkQuick Start
import { HaxClient, isCompleted } from '@agentfield/hax-sdk';
const client = new HaxClient({
apiKey: process.env.HAX_API_KEY,
});
// Create an approval request
const request = await client.createRequest({
type: 'text-approval-v1',
payload: { text: 'Deploy v2.0.0 to production?' },
webhookUrl: 'https://myapp.com/webhook',
});
console.log('Approval URL:', request.url);
// Wait for the response
const result = await client.waitForResponse(request.id, {
timeout: 300, // 5 minutes
});
if (isCompleted(result)) {
console.log('Completed!', result.response);
}DID identity (no API key)
Instead of an API key, a client can authenticate with a DID identity — an
Ed25519 keypair encoded as a did:key. Each request is signed with a detached
JWS over the method, URL pathname, and exact body bytes (the "knock" protocol).
The server identifies the sender by its DID and a workspace can accept or block
it. This lets an agent introduce itself to a workspace it has no key for.
import { HaxClient } from '@agentfield/hax-sdk';
// No apiKey → an identity is resolved (or minted) and used to sign requests.
const client = new HaxClient({
baseUrl: 'https://your-hax.example.com/api/v1',
});
console.log('My DID:', client.getIdentity()?.did); // did:key:z6Mk…
// Knock a workspace by its public handle (or use `senderInvite`).
const created = await client.createRequest({
type: 'text-approval-v1',
payload: { text: 'Requesting access to acme' },
workspace: 'acme',
});
// Wait until a human accepts (or blocks) this sender.
const outcome = await client.waitForAcceptance({ workspace: 'acme' });
// → 'active' | 'blocked' | 'timeout'Identity resolution order
When the client constructs, it resolves a DID identity in this order:
- Explicit
{ did, privateKeyJwk }passed to the constructor. - Explicit
identityFilepath. HAX_IDENTITY_FILEenvironment variable (also overrides the default file location).~/.hax/identity.json, if it exists.- If no API key is configured, a fresh identity is minted, persisted to
~/.hax/identity.json(mode0600), and the DID is logged once:[hax] minted DID did:key:z6Mk… (~/.hax/identity.json).
If both an API key and a DID identity are absent, the client throws.
Identity file format
~/.hax/identity.json (mode 0600):
{
"did": "did:key:z6Mk…",
"privateKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "…", "d": "…" },
"createdAt": "2026-06-13T00:00:00.000Z"
}Wire contract
Signed requests carry two headers:
X-HAX-DID: <did>X-HAX-Signature: <compact JWS>
The JWS protected header is {"alg":"EdDSA","kid":"<did>#<multibase-suffix>"}
and the payload is {"iat","jti","htm","htu","bh":"sha256:<base64url(sha256(body))>"},
where htu is the URL pathname including the /api/v1 prefix and bh hashes
the exact body bytes sent on the wire.
CLI
# Mint and persist a new identity (default: ~/.hax/identity.json)
hax did create [--out <path>]
# Print the current DID + fingerprint
hax did show [--identity-file <path>]
# Knock a workspace and (optionally) wait for acceptance
hax knock <workspace> [--invite <code>] [--type <t>] [--text <s>] [--wait]Programmatic identity helpers
import {
generateIdentity, // { did, privateKeyJwk }
loadIdentity, // resolve per the order above
saveIdentity, // persist 0600
didFingerprint,
signKnockJws,
} from '@agentfield/hax-sdk';Features
- Type-safe: Full TypeScript support with comprehensive types
- Zero dependencies: Uses native
fetchand Web Crypto API - Automatic retries: Configurable retry logic with exponential backoff
- Webhook verification: Secure signature verification for webhooks
- End-to-end encryption: RSA-OAEP + AES-GCM hybrid encryption for sensitive response data
- FormBuilder: Fluent API for building typed forms
- Modern: ESM-first, Node.js 18+ compatible
API Reference
HaxClient
The main client for interacting with the HAX API.
import { HaxClient } from '@agentfield/hax-sdk';
const client = new HaxClient({
apiKey: 'hax_live_...',
baseUrl: 'https://hax-sdk-production.up.railway.app/api/v1', // optional
webhookUrl: 'https://myapp.com/webhook', // optional default webhook
timeout: 30000, // optional, in milliseconds
maxRetries: 3, // optional
encryptionKey: 'my-passphrase', // optional, for client-side encryption
});Request Methods
// Create a request
const request = await client.createRequest({
type: 'text-approval-v1',
payload: { text: 'Approve this action?' },
title: 'Optional title',
description: 'Optional description',
webhookUrl: 'https://myapp.com/webhook',
expiresInSeconds: 3600,
metadata: { key: 'value' },
});
// Create and send via email
const request = await client.requestViaEmail({
type: 'confirm-action-v1',
payload: { title: 'Confirm?', confirmPhrase: 'YES' },
toEmail: '[email protected]',
subject: 'Approval Required',
});
// Create and send via SMS
const request = await client.requestViaSms({
type: 'text-approval-v1',
payload: { text: 'Approve?' },
toPhone: '+15551234567',
});
// Get a request by ID
const request = await client.getRequest('req_123');
// List recent requests
const requests = await client.listRequests();
// Cancel a pending request
const cancelled = await client.cancelRequest('req_123');
// Submit a response (for testing)
const completed = await client.submitResponse('req_123', { decision: 'approve' });
// Wait for completion
const result = await client.waitForResponse('req_123', {
pollInterval: 2, // seconds
timeout: 60, // seconds
});
// List available template types (full catalog — use sparingly in agents)
const types = await client.listTypes();
// Search templates with compact metadata (preferred for agents)
const hits = await client.searchTypes({
q: 'destructive deploy',
tags: 'confirm-destructive',
pack: 'devops',
limit: 5,
});
const schema = await client.getTypeSchema(hits.templates[0].id);Status Helpers
import { isPending, isOpened, isCompleted, isExpired, isCancelled } from '@agentfield/hax-sdk';
if (isPending(request)) {
// Request is waiting for response
}
if (isCompleted(request)) {
console.log('Response:', request.response);
}Template Types
Available built-in templates:
| Template | Description |
|----------|-------------|
| text-approval-v1 | Show text and collect an approve/deny decision |
| confirm-action-v1 | Require typing a specific phrase to confirm a destructive action |
| collect-email-v1 | Prompt the user for an email address |
| form-builder | Advanced forms with field types, layouts, validation, and conditional logic |
| multi-choice-selection-v1 | Single or multiple selection from customizable option cards |
| code-changes-v1 | GitHub-style diff view with inline line comments |
| rich-text-editor-v1 | Markdown-formatted text editing for documents and reports |
| file-upload-v1 | Collect files (documents, images, CSVs) from users |
| signature-capture-v1 | Capture e-signatures with optional signer name and legal text |
| data-table-review-v1 | Review, select, or edit tabular data |
| scheduling-picker-v1 | Date/time slot selection with optional recurring schedules |
| multi-step-wizard-v1 | Sequential steps with navigation and progress indicator |
| side-by-side-comparison-v1 | Compare two versions with diff highlighting |
| terminal-output-v1 | Display command output/logs with approve-to-continue |
Example payloads:
// text-approval-v1
await client.createRequest({
type: 'text-approval-v1',
payload: {
text: 'The message to display',
approveLabel: 'Approve', // optional
denyLabel: 'Deny', // optional
},
});
// confirm-action-v1
await client.createRequest({
type: 'confirm-action-v1',
payload: {
title: 'Dangerous Action',
message: 'This cannot be undone.',
confirmPhrase: 'DELETE ALL',
confirmLabel: 'Confirm', // optional
cancelLabel: 'Cancel', // optional
warningLevel: 'danger', // 'info' | 'warning' | 'danger'
},
});Webhooks
Verify and parse webhook events:
import { verifyWebhook, WebhookVerificationError } from '@agentfield/hax-sdk';
// In your webhook handler
app.post('/webhooks/hax', async (req, res) => {
try {
const event = await verifyWebhook({
signature: req.headers['x-hax-signature'],
payload: req.body, // raw string body
secret: process.env.HAX_WEBHOOK_SECRET,
});
switch (event.type) {
case 'completed':
// Handle completion
break;
case 'expired':
// Handle expiration
break;
}
res.status(200).send('OK');
} catch (error) {
if (error instanceof WebhookVerificationError) {
res.status(400).send('Invalid signature');
}
throw error;
}
});Event Types
sent- Request notification was sentopened- Request was viewedcompleted- Request was completedexpired- Request expired
FormBuilder
Build typed forms with a fluent API:
import { HaxClient, FormBuilder } from '@agentfield/hax-sdk';
const client = new HaxClient({ apiKey: 'hax_live_...' });
// Create a typed form
const form = FormBuilder.create()
.title('Event Registration')
.input('name', { label: 'Full Name', required: true })
.input('email', { label: 'Email', variant: 'email', required: true })
.number('age', { label: 'Age', min: 0, max: 120 })
.checkbox('newsletter', { checkboxLabel: 'Subscribe to newsletter' });
// Create the form and get a typed handle
const handle = await client.createFormRequest(form);
console.log('Form URL:', handle.url);
// Wait for the typed response
const response = await handle.waitForResponse({ timeout: 300 });
console.log(response.values.name); // string
console.log(response.values.email); // string
console.log(response.values.age); // number
console.log(response.values.newsletter); // booleanRehydrating Form Handles
If you lose the handle (e.g., across requests), rehydrate it with the request ID:
// Rehydrate from a stored request ID
const handle = client.getFormHandle('req_abc123', form);
const response = await handle.waitForResponse();Encryption
For sensitive response data, use end-to-end encryption:
import { HaxClient } from '@agentfield/hax-sdk';
// Use a passphrase (RSA keypair is derived deterministically)
const client = new HaxClient({
apiKey: 'hax_live_...',
encryptionKey: 'my-secret-passphrase',
});
// Public key is sent with the request
// Server encrypts user responses with it
const request = await client.createRequest({
type: 'text-approval-v1',
payload: { text: 'Approve this sensitive action?' },
});
// Response is automatically decrypted
const completed = await client.getRequest(request.id);
console.log(completed.response); // DecryptedError Handling
import {
HaxError,
ApiError,
AuthenticationError,
ValidationError,
NotFoundError,
RateLimitError,
NetworkError,
} from '@agentfield/hax-sdk';
try {
await client.createRequest({ /* ... */ });
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid API key (401)
} else if (error instanceof ValidationError) {
// Invalid request data (400/422)
console.log(error.errors);
} else if (error instanceof NotFoundError) {
// Resource not found (404)
} else if (error instanceof RateLimitError) {
// Too many requests (429)
console.log(error.retryAfter);
} else if (error instanceof NetworkError) {
// Network/connection error
} else if (error instanceof ApiError) {
// Other API error
console.log(error.statusCode);
}
}Examples
See the examples directory for complete working examples:
- Basic Approval - Simple text approval request
- Confirm Action - Dangerous action confirmation
- Webhook Handler - Processing webhooks
Requirements
- Node.js 18+ (for native
fetchand Web Crypto API) - TypeScript 5.0+ (optional, for TypeScript users)
Agent CLI (Local)
The SDK now ships a local agent-oriented CLI as hax (JSON-first output, local consumer_id tracking).
Security model — read before relying on
consumer_id.consumer_idis a local-only convenience for keeping multiple agent sessions on one machine from stepping on each other. It is tracked in~/.hax/agent-state.jsonand is not sent to or enforced by the server. The API key is the only security boundary: anyone holding it can read or act on any request in its project, regardless ofconsumer_id, andrequest claim --force/handoffonly rewrite the local binding. Treat the API key like a password, and use separate API keys per team/machine/trust domain when you need real isolation — do not rely onconsumer_idfor it.
Fastest onboarding (copy/paste):
npm install -g @agentfield/hax-sdk
export HAX_API_KEY="hax_live_..."
export HAX_BASE_URL="https://your-hax-domain/api/v1"
export HAX_CONSUMER_ID="claude-local-session-1"Build and run locally from this repo:
cd sdks/typescript
npm install
npm run build
node dist/cli.js templates list --api-key "$HAX_API_KEY" --base-url "$HAX_BASE_URL"Install as a shell command:
cd sdks/typescript
npm link
hax templates list --api-key "$HAX_API_KEY" --base-url "$HAX_BASE_URL"Recommended agent discovery flow (keeps context small):
hax templates search --q "..." --tag approve --limit 5hax templates schema <chosen-id>hax request create --type <chosen-id> --payload ... --title "<short label>"
Core commands:
hax templates list
hax templates search --q "approval" --tag approve --pack core --limit 5
hax templates schema text-approval-v1
hax request create --type text-approval-v1 --payload '{"text":"Approve?"}' --title "Approve deploy" --consumer-id "$HAX_CONSUMER_ID"
hax request get --id <request_id> --consumer-id "$HAX_CONSUMER_ID"
hax request wait --id <request_id> --consumer-id "$HAX_CONSUMER_ID" --timeout 600
hax request resume --consumer-id "$HAX_CONSUMER_ID"
hax request handoff --id <request_id> --from-consumer-id "$OLD_CONSUMER" --to-consumer-id "$NEW_CONSUMER"
hax request claim --id <request_id> --consumer-id "$HAX_CONSUMER_ID" --forceDaemon commands (optional, lightweight local reliability layer):
hax daemon start --api-key "$HAX_API_KEY" --base-url "$HAX_BASE_URL"
hax daemon status --api-key "$HAX_API_KEY" --base-url "$HAX_BASE_URL"
hax daemon stop --api-key "$HAX_API_KEY" --base-url "$HAX_BASE_URL"When daemon is running, hax request wait automatically uses it. If daemon is unavailable, CLI falls back to direct polling.
MCP mode:
hax mcp serve --api-key "$HAX_API_KEY" --base-url "$HAX_BASE_URL"MCP config snippet (for stdio-based MCP clients):
{
"mcpServers": {
"hax": {
"command": "hax",
"args": ["mcp", "serve"],
"env": {
"HAX_API_KEY": "hax_live_...",
"HAX_BASE_URL": "https://your-hax-domain/api/v1",
"HAX_CONSUMER_ID": "claude-local-session-1"
}
}
}
}Exposed tools:
list_templatessearch_templatesget_template_schemacreate_requestwait_requestget_requestlist_active_requests
License
MIT
