@nextera.one/axis-client-sdk
v2.3.0
Published
Official TypeScript/JavaScript client SDK for the AXIS Protocol v1
Maintainers
Readme
@nextera.one/axis-client-sdk
Official TypeScript/JavaScript client SDK for the AXIS Protocol v1.
Features
- 🔐 Ed25519 Signing - Cryptographic signatures for frame integrity
- 📦 Binary Protocol - Efficient TLV (Type-Length-Value) encoding
- 🎯 Intent-Based - Route requests through configurable sensor pipeline
- 🔄 Retry Logic - Automatic retry with exponential backoff
- 📤 File Upload - Chunked upload with resume support
- 🎭 Multiple Proof Types - Capsule, JWT, mTLS, Loom, Witness signatures
- ⚡ Slim Runtime - Signing, transport, and DTO helpers in one package
Installation
npm install @nextera.one/axis-client-sdk
# or
yarn add @nextera.one/axis-client-sdk
# or
pnpm add @nextera.one/axis-client-sdkContract Surfaces
The 2.1.0 release folds the backend-authored client contract surface into this package so consumers no longer need temporary local wrapper packages.
- TLV DTO helpers:
AxisTlvDto,TlvField,TlvEnum,TlvMinLen,TlvRange,TlvUtf8Pattern,TlvValidate - Auth and payment contracts: QR login request builders, QR login intent constants, payment DTOs, and catalog helpers
- AXIS DTO and enum exports under
./axisfor actor keys, anomaly state, blocklist, capsules, identities, intent policy, intent schemas, intents registry, issuer keys, metrics, node identities, packet denylist, receipts, root certificates, sensor logs, stream events, stream subscriptions, traces, and upload sessions
The package also initializes reflect-metadata on import so decorator-backed DTO metadata is ready when these contracts are consumed.
Quick Start
Basic Usage (JSON)
import { AxisClient } from '@nextera.one/axis-client-sdk';
const client = new AxisClient({
baseUrl: 'http://localhost:3000/api/axis',
actorId: 'your-actor-uuid',
});
const result = await client.send('public.ping', { message: 'Hello' });
console.log(result.data);Binary Protocol with Signing
import { AxisClient, Ed25519Signer } from '@nextera.one/axis-client-sdk';
// Create signer from private key
const privateKey = Buffer.from('your-32-byte-private-key');
const signer = new Ed25519Signer(privateKey);
const client = new AxisClient({
baseUrl: 'http://localhost:3000/api/axis',
actorId: 'your-actor-uuid',
useBinary: true,
signer, // Frames will be automatically signed
});
const result = await client.send('schema.validate', {
data: {
/* your data */
},
});Frame Builder (Manual Frame Construction)
import {
AxisFrameBuilder,
generatePid,
generateNonce,
uuidToBytes,
ProofType,
} from '@nextera.one/axis-client-sdk';
const builder = new AxisFrameBuilder()
.setPid(generatePid())
.setTimestamp(BigInt(Date.now()))
.setIntent('public.ping')
.setActorId(uuidToBytes('your-actor-uuid'))
.setProofType(ProofType.CAPSULE)
.setProofRef(Buffer.alloc(16)) // Capsule ID or empty
.setNonce(generateNonce())
.setBodyJSON({ message: 'Hello' });
// Build unsigned frame
const unsignedFrame = builder.buildUnsigned();
// Sign and build signed frame
const signature = await signer.sign(unsignedFrame);
const signedFrame = builder.buildSigned(signature);File Upload with Progress
const result = await client.uploadFile('./large-file.dat', {
chunkSize: 1024 * 1024, // 1MB chunks
onProgress: (progress) => {
console.log(`Uploading: ${progress.percent}%`);
},
});
console.log(`File uploaded: ${result.fileId}`);File Download
await client.downloadFile('file-id', './downloaded-file.dat', {
onProgress: (progress) => {
console.log(`Downloaded: ${progress.percent}%`);
},
});NestFlow Device And Session Requests
import {
buildDeviceTrustPromoteRequest,
buildSessionRefreshRequest,
} from '@nextera.one/axis-client-sdk';
const actor = {
deviceUid: 'dev_mobile_primary_01',
identityUid: 'id_abc123',
sessionUid: 'sess_xyz',
origin: 'nestflow-key://mobile-app',
};
const promoteReq = buildDeviceTrustPromoteRequest(
{ target_device_uid: 'dev_web_01', label: 'Work Chrome' },
actor,
);
const refreshReq = buildSessionRefreshRequest({ reason: 'keepalive' }, actor);
await client.send(promoteReq.intent, promoteReq.payload);
await client.send(refreshReq.intent, refreshReq.payload);Backend QR Login Flow
import {
AxisQrAuthIntents,
Ed25519Signer,
buildAxisQrApproveRequest,
buildAxisQrAttachKeyRequest,
buildAxisQrChallengeRequest,
buildAxisQrVerifyRequest,
buildQrApprovalPayload,
ed25519PublicKeyToSpkiBase64Url,
signBrowserProofMessage,
signQrApprovalPayload,
} from '@nextera.one/axis-client-sdk';
const browserSigner = new Ed25519Signer(browserSeed);
const mobileSigner = new Ed25519Signer(mobileSeed);
const challenge = await client.send(
AxisQrAuthIntents.CHALLENGE,
buildAxisQrChallengeRequest({ origin: 'https://app.example.com' }),
);
const browserPublicKey = ed25519PublicKeyToSpkiBase64Url(
await browserSigner.getPublicKey(),
);
const browserProofSignature = await signBrowserProofMessage(
challenge.data.challengeUid,
challenge.data.nonce,
browserSigner,
);
await client.send(
AxisQrAuthIntents.ATTACH_KEY,
buildAxisQrAttachKeyRequest({
challengeUid: challenge.data.challengeUid,
browserPublicKey,
browserKeyAlgorithm: 'ed25519',
browserProofSignature,
trustDeviceRequested: true,
}),
);
const approvalPayload = buildQrApprovalPayload({
challengeUid: challenge.data.challengeUid,
browserPublicKey,
nonce: challenge.data.nonce,
tickauthChallengeUid: JSON.parse(challenge.data.qrPayload).tickAuthChallengeUid,
expiresAt: challenge.data.expiresAt,
actorId: 'actor_123',
approvedAt: Date.now(),
scope: ['axis.auth.*', 'axis.files.*'],
});
const { signedPayload, signature } = await signQrApprovalPayload(
approvalPayload,
mobileSigner,
);
await client.send(
AxisQrAuthIntents.APPROVE,
buildAxisQrApproveRequest({
challengeUid: challenge.data.challengeUid,
actorId: 'actor_123',
mobileDeviceUid: 'dev_mobile_primary_01',
signedPayload,
signature,
scope: ['axis.auth.*', 'axis.files.*'],
}),
);
const verified = await client.send(
AxisQrAuthIntents.VERIFY,
buildAxisQrVerifyRequest({
challengeUid: challenge.data.challengeUid,
browserPublicKey,
}),
);P-256 Signer Support
import {
P256Signer,
exportSignerPublicKeySpkiBase64Url,
generateP256KeyPair,
} from '@nextera.one/axis-client-sdk';
const keyPair = generateP256KeyPair();
const signer = new P256Signer(keyPair.privateKeyPkcs8Der, {
format: 'der',
type: 'pkcs8',
});
const publicKey = await exportSignerPublicKeySpkiBase64Url(signer);
const signature = await signer.sign(new TextEncoder().encode('hello'));High-Level QR Orchestration
import {
AxisClient,
Ed25519Signer,
approveAxisQrLogin,
initiateAxisQrLogin,
waitForAxisQrVerification,
} from '@nextera.one/axis-client-sdk';
const browserSigner = new Ed25519Signer(browserSeed);
const mobileSigner = new Ed25519Signer(mobileSeed);
const browserClient = new AxisClient({
baseUrl: 'http://localhost:3000',
actorId: 'actor:web',
});
const mobileClient = new AxisClient({
baseUrl: 'http://localhost:3000',
actorId: 'actor:mobile',
});
const started = await initiateAxisQrLogin(browserClient, browserSigner, {
origin: 'https://app.example.com',
trustDeviceRequested: true,
});
await approveAxisQrLogin(mobileClient, {
challengeUid: started.challenge.challengeUid,
browserPublicKey: started.browserPublicKey,
nonce: started.challenge.nonce,
tickAuthChallengeUid: started.qr.tickAuthChallengeUid,
expiresAt: started.challenge.expiresAt,
actorId: 'actor_123',
mobileDeviceUid: 'dev_mobile_01',
signer: mobileSigner,
scope: ['axis.auth.*', 'axis.files.*'],
});
const verified = await waitForAxisQrVerification(browserClient, {
challengeUid: started.challenge.challengeUid,
browserPublicKey: started.browserPublicKey,
});API Reference
AxisClient
Main client for communicating with AXIS servers.
Constructor
new AxisClient(config: AxisClientConfig)Config Options:
baseUrl(required) - Server endpoint URLactorId(required) - UUID of actor/usercapsuleId?- UUID of capsule for proofsigner?- Ed25519Signer instance for frame signingmaxRetries?- Max retry attempts (default: 3)retryDelayMs?- Delay between retries (default: 1000)timeout?- Request timeout in ms (default: 30000)useBinary?- Use binary protocol (default: false)
Methods
send(intent, body?): Promise
- Send an intent with automatic retry
- Returns
{ ok: boolean, data?: any, error?: string }
uploadFile(path, options?): Promise
- Upload file with chunking and resume
- Options:
chunkSize,resumeFileId,onProgress
downloadFile(fileId, destPath, options?): Promise
- Download file with range requests
- Options:
onProgress
streamTail(topic, callback, options?): Promise<() => void>
- Subscribe to event stream
- Returns unsubscribe function
Frame Builder
Low-level frame construction.
const builder = new AxisFrameBuilder();
builder
.setPid(pid) // 16-byte packet ID
.setTimestamp(ts) // Timestamp (bigint)
.setIntent(intent) // Intent name
.setActorId(actorId) // 16-byte actor UUID
.setProofType(type) // Proof type enum
.setProofRef(ref) // Proof reference
.setNonce(nonce) // 16-32 byte nonce
.setRealm(realm) // (Optional) Realm string
.setNode(node) // (Optional) Node string
.setTraceId(traceId) // (Optional) Trace ID
.setBody(body) // Binary body
.setBodyJSON(obj) // JSON body
.setFlags(bodyt, chain, witness);
// Get unsigned frame for signing
const unsigned = builder.buildUnsigned();
// Build signed frame
const signed = builder.buildSigned(signature);Ed25519Signer
Sign frames with Ed25519 signatures.
import { Ed25519Signer } from '@nextera.one/axis-client-sdk';
const signer = new Ed25519Signer(privateKey); // 32-byte seed
const signature = await signer.sign(message);
const publicKey = await signer.getPublicKey();Binary Utilities
import {
generatePid, // Generate random 16-byte PID
generateNonce, // Generate random 16-32 byte nonce
uuidToBytes, // Convert UUID string to bytes
bytesToUuid, // Convert bytes to UUID string
} from '@nextera.one/axis-client-sdk';
const pid = generatePid();
const nonce = generateNonce(32);
const bytes = uuidToBytes('550e8400-e29b-41d4-a716-446655440000');
const uuid = bytesToUuid(bytes);Frame Encoding/Decoding
import {
AxisBinaryFrame,
TLV_AUD,
TLV_REALM,
encodeFrame,
decodeFrame,
getSignTarget,
signFrame,
verifyFrameSignature,
} from '@nextera.one/axis-client-sdk/core';
// Encode frame to binary
const buffer = encodeFrame(frame);
// Decode binary to frame
const frame = decodeFrame(buffer);Shared Core API
The ./core subpath is intentionally aligned with the server SDK for shared protocol work.
AxisBinaryFrameis the low-level binary frame type.TLV_AUDis the canonical name for tag8.TLV_REALMremains available as a compatibility alias.- The client core exports the same protocol constants as the server core, including Loom, witness, body-profile, upload, and node-certificate constants.
- Prefer
./corefor code meant to run against both client and server SDKs.
Frame Format
The AXIS Protocol v1 binary frame format:
[MAGIC(5)][VERSION(1)][FLAGS(1)][HDR_LEN(V)][BODY_LEN(V)]
[HEADERS][BODY][SIG_LEN(V)][SIGNATURE(64)]
MAGIC: AXIS1 (0x41 0x58 0x49 0x53 0x31) - 5 bytes
VERSION: 0x01 - 1 byte
FLAGS: Bitfield - 1 byte
Bit 0: Body is TLV encoded
Bit 1: Receipt chaining requested
Bit 2: Witness included
HDR_LEN: Varint (LEB128) - variable length
BODY_LEN: Varint (LEB128) - variable length
HEADERS: TLV-encoded header fields
BODY: Raw or TLV-encoded body (up to 64KB)
SIG_LEN: Varint (LEB128) - signature length
SIGNATURE: 64-byte Ed25519 signature (optional)Header Fields (TLV)
Required headers:
- PID (1) - 16-byte packet ID (UUIDv7)
- TS (2) - 8-byte timestamp
- INTENT (3) - UTF-8 intent name
- ACTOR_ID (4) - 16-byte actor UUID
- PROOF_TYPE (5) - 1-byte proof type
- PROOF_REF (6) - 1-64 byte proof reference
- NONCE (7) - 16-32 byte anti-replay nonce
Optional headers:
- REALM (8) - Routing hint
- NODE (9) - Node selector
- TRACE_ID (10) - Trace correlation
- WITNESS_REF (11) - Witness identifier
- CONTRACT_ID (12) - Execution contract
- EXPECTED_EFFECT (13) - Declared effect
Protocol Constants
// Hard limits (enforced)
MAX_FRAME_LEN = 71,680 // 70 KB
MAX_HDR_LEN = 2,048 // 2 KB
MAX_BODY_LEN = 65,536 // 64 KB
MAX_SIG_LEN = 128 // 128 bytes
MAX_TLVS = 64 // Max header TLVs
// Varint limits
MAX_VARINT_BYTES = 5 // LEB128 encodingError Handling
const result = await client.send('public.ping', {});
if (!result.ok) {
console.error(`Error: ${result.error}`);
// Common error codes from server
// - INVALID_PACKET
// - BAD_SIGNATURE
// - REPLAY_DETECTED
// - RATE_LIMITED
// - INTENT_NOT_FOUND
// - INTERNAL_ERROR
}Development
Build
npm run buildOutputs:
dist/index.js- CommonJSdist/index.mjs- ESMdist/index.d.ts- TypeScript definitions
Testing
npm testEnvironment Support
- Node.js 18.0.0+
- Browsers with proper crypto support (via polyfills)
- Deno 1.40+
Security
- Frames are signed with Ed25519 before transmission
- Signatures verify integrity of entire frame
- Support for public key rotation via KID (Key ID)
- Nonce prevents replay attacks
- No sensitive data in logs
License
Apache License 2.0 - See LICENSE file
Contributing
Contributions welcome! Please submit PRs with:
- Unit tests
- TypeScript strict mode compliance
- Documentation updates
Support
AXIS Protocol - Auditable, Secure, Intent-based eXchange
