Coming Soon
The Node.js SDK (@ascend/sdk) is not yet published to npm. This documentation is a preview of the planned API. For production use, please use the Python SDK or the REST API.
Node.js SDK Examples
Production-ready TypeScript code examples for common use cases with the ASCEND Node.js SDK.
Table of Contents
- Basic Action Evaluation
- Express.js Middleware
- Customer Service Agent
- Async Batch Processing
- Webhook Handler
- Error Handling Patterns
- Metrics and Monitoring
- Testing Patterns
Basic Action Evaluation
Simple pattern for evaluating and executing governed actions.
/**
* Basic action evaluation pattern
*/
import { AscendClient, Decision } from '@ascend/sdk';
const client = new AscendClient({
apiKey: process.env.ASCEND_API_KEY!,
agentId: 'basic-agent',
agentName: 'Basic Agent'
});
interface ActionResult {
status: 'success' | 'pending' | 'denied' | 'error';
data?: unknown;
approvalId?: string;
reason?: string;
}
async function governedAction(
actionType: string,
resource: string,
params: Record<string, unknown>
): Promise<ActionResult> {
const decision = await client.evaluateAction({
actionType,
resource,
parameters: params
});
if (decision.decision === Decision.ALLOWED) {
const startTime = Date.now();
try {
const result = await executeAction(actionType, resource, params);
await client.logActionCompleted(
decision.actionId,
{ success: true },
Date.now() - startTime
);
return { status: 'success', data: result };
} catch (error) {
await client.logActionFailed(
decision.actionId,
error as Error,
Date.now() - startTime
);
return { status: 'error', reason: (error as Error).message };
}
}
if (decision.decision === Decision.PENDING) {
return {
status: 'pending',
approvalId: decision.approvalRequestId,
reason: 'Awaiting human approval'
};
}
return {
status: 'denied',
reason: decision.reason
};
}
async function executeAction(
actionType: string,
resource: string,
params: Record<string, unknown>
): Promise<unknown> {
// Your action implementation
return { executed: true };
}
// Usage
const result = await governedAction(
'database.query',
'production_db',
{ query: 'SELECT * FROM users LIMIT 10' }
);
console.log(result);
Express.js Middleware
Integrate ASCEND governance into Express.js applications.
/**
* Express.js middleware for ASCEND governance
*/
import express, { Request, Response, NextFunction } from 'express';
import { AscendClient, Decision, FailMode } from '@ascend/sdk';
const app = express();
app.use(express.json());
// Initialize ASCEND client
const ascend = new AscendClient({
apiKey: process.env.ASCEND_API_KEY!,
agentId: 'express-api',
agentName: 'Express API Server',
failMode: FailMode.CLOSED
});
// Register on startup
await ascend.register({
agentType: 'api_server',
capabilities: ['api.read', 'api.write', 'api.delete'],
allowedResources: ['users', 'orders', 'products']
});
// Governance middleware factory
function requireGovernance(actionType: string, resource: string) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const decision = await ascend.evaluateAction({
actionType,
resource,
parameters: {
method: req.method,
path: req.path,
body: req.body
},
context: {
ip: req.ip,
userAgent: req.get('user-agent'),
sessionId: req.get('x-session-id')
}
});
if (decision.decision === Decision.ALLOWED) {
// Attach action ID for logging
res.locals.actionId = decision.actionId;
res.locals.actionStartTime = Date.now();
return next();
}
if (decision.decision === Decision.PENDING) {
return res.status(202).json({
status: 'pending_approval',
approvalId: decision.approvalRequestId,
message: 'Request requires approval'
});
}
return res.status(403).json({
status: 'denied',
reason: decision.reason,
policyViolations: decision.policyViolations
});
} catch (error) {
console.error('Governance error:', error);
return res.status(503).json({
status: 'error',
message: 'Governance service unavailable'
});
}
};
}
// Completion logging middleware
function logCompletion() {
return async (req: Request, res: Response, next: NextFunction) => {
res.on('finish', async () => {
const { actionId, actionStartTime } = res.locals;
if (!actionId) return;
const durationMs = Date.now() - actionStartTime;
if (res.statusCode < 400) {
await ascend.logActionCompleted(actionId, {
statusCode: res.statusCode
}, durationMs);
} else {
await ascend.logActionFailed(actionId, {
statusCode: res.statusCode,
error: 'Request failed'
});
}
});
next();
};
}
// Apply middleware
app.use(logCompletion());
// Routes with governance
app.get('/api/users',
requireGovernance('api.read', 'users'),
async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
}
);
app.post('/api/users',
requireGovernance('api.write', 'users'),
async (req, res) => {
const user = await db.insert('users', req.body);
res.status(201).json(user);
}
);
app.delete('/api/users/:id',
requireGovernance('api.delete', 'users'),
async (req, res) => {
await db.delete('users', req.params.id);
res.status(204).send();
}
);
app.listen(3000, () => {
console.log('Server running with ASCEND governance');
});
Customer Service Agent
Complete customer service agent with TypeScript.
/**
* Customer Service Agent with ASCEND Governance
*/
import { AscendClient, FailMode, Decision } from '@ascend/sdk';
interface RefundRequest {
customerId: string;
orderId: string;
amount: number;
currency: string;
reason: string;
}
interface RefundResult {
status: 'completed' | 'pending_approval' | 'denied' | 'error';
refundId?: string;
approvalId?: string;
message?: string;
}
class CustomerServiceAgent {
private client: AscendClient;
constructor(apiKey: string, environment: string = 'production') {
this.client = new AscendClient({
apiKey,
agentId: 'customer-service-agent',
agentName: 'Customer Service Bot',
environment,
failMode: FailMode.CLOSED
});
}
async initialize(): Promise<void> {
await this.client.register({
agentType: 'automation',
capabilities: [
'transaction.refund',
'customer.lookup',
'order.status'
],
allowedResources: [
'stripe_api',
'customer_db',
'order_db'
]
});
}
async processRefund(request: RefundRequest): Promise<RefundResult> {
const decision = await this.client.evaluateAction({
actionType: 'transaction.refund',
resource: 'stripe_api',
parameters: {
customerId: request.customerId,
orderId: request.orderId,
amount: request.amount,
currency: request.currency,
reason: request.reason
},
context: {
userRequest: `Refund $${request.amount} for order ${request.orderId}`
}
});
if (decision.decision === Decision.ALLOWED) {
return this.executeRefund(decision.actionId, request);
}
if (decision.decision === Decision.PENDING) {
return {
status: 'pending_approval',
approvalId: decision.approvalRequestId,
message: `Refund requires approval. ID: ${decision.approvalRequestId}`
};
}
return {
status: 'denied',
message: decision.reason
};
}
private async executeRefund(
actionId: string,
request: RefundRequest
): Promise<RefundResult> {
const startTime = Date.now();
try {
// Simulate refund processing
const refundId = `ref_${Date.now()}`;
await this.client.logActionCompleted(
actionId,
{ refundId, amount: request.amount },
Date.now() - startTime
);
return {
status: 'completed',
refundId,
message: `Refund ${refundId} processed successfully`
};
} catch (error) {
await this.client.logActionFailed(
actionId,
error as Error,
Date.now() - startTime
);
return {
status: 'error',
message: (error as Error).message
};
}
}
async lookupCustomer(customerId: string): Promise<Customer | ErrorResult> {
const decision = await this.client.evaluateAction({
actionType: 'customer.lookup',
resource: 'customer_db',
parameters: { customerId }
});
if (decision.decision === Decision.ALLOWED) {
return {
customerId,
name: 'John Doe',
email: 'john@example.com'
};
}
return { error: decision.reason };
}
}
interface Customer {
customerId: string;
name: string;
email: string;
}
interface ErrorResult {
error: string | undefined;
}
// Usage
const agent = new CustomerServiceAgent(process.env.ASCEND_API_KEY!);
await agent.initialize();
const result = await agent.processRefund({
customerId: 'cust_123',
orderId: 'ord_456',
amount: 75.00,
currency: 'USD',
reason: 'Product defect'
});
console.log(result);
Async Batch Processing
Process multiple actions concurrently.
/**
* Async batch processing with ASCEND
*/
import { AscendClient, Decision } from '@ascend/sdk';
interface BatchItem {
actionType: string;
resource: string;
parameters: Record<string, unknown>;
}
interface BatchResult {
index: number;
status: 'success' | 'denied' | 'error';
actionId?: string;
data?: unknown;
error?: string;
}
class BatchProcessor {
private client: AscendClient;
constructor(apiKey: string) {
this.client = new AscendClient({
apiKey,
agentId: 'batch-processor',
agentName: 'Batch Processor Agent'
});
}
async processBatch(
items: BatchItem[],
options: { concurrency?: number; continueOnError?: boolean } = {}
): Promise<BatchResult[]> {
const { concurrency = 5, continueOnError = true } = options;
const results: BatchResult[] = [];
// Process in chunks for controlled concurrency
for (let i = 0; i < items.length; i += concurrency) {
const chunk = items.slice(i, i + concurrency);
const chunkResults = await Promise.all(
chunk.map((item, idx) =>
this.processItem(item, i + idx, continueOnError)
)
);
results.push(...chunkResults);
// Check if we should stop on error
if (!continueOnError && chunkResults.some(r => r.status === 'error')) {
break;
}
}
return results;
}
private async processItem(
item: BatchItem,
index: number,
continueOnError: boolean
): Promise<BatchResult> {
try {
const decision = await this.client.evaluateAction({
actionType: item.actionType,
resource: item.resource,
parameters: item.parameters
});
if (decision.decision === Decision.ALLOWED) {
const data = await this.executeAction(item);
await this.client.logActionCompleted(decision.actionId, {
success: true
});
return {
index,
status: 'success',
actionId: decision.actionId,
data
};
}
return {
index,
status: 'denied',
actionId: decision.actionId,
error: decision.reason
};
} catch (error) {
return {
index,
status: 'error',
error: (error as Error).message
};
}
}
private async executeAction(item: BatchItem): Promise<unknown> {
// Implement actual action
return { processed: true };
}
}
// Usage
const processor = new BatchProcessor(process.env.ASCEND_API_KEY!);
const items: BatchItem[] = [
{ actionType: 'database.query', resource: 'db1', parameters: { query: 'Q1' } },
{ actionType: 'file.read', resource: '/logs', parameters: { path: '/app.log' } },
{ actionType: 'api.call', resource: 'api', parameters: { endpoint: '/status' } }
];
const results = await processor.processBatch(items, { concurrency: 3 });
console.log(`Processed: ${results.filter(r => r.status === 'success').length}/${results.length}`);
Webhook Handler
Receive and process ASCEND webhooks.
/**
* ASCEND webhook handler with Express
*/
import express, { Request, Response } from 'express';
import crypto from 'crypto';
const app = express();
const WEBHOOK_SECRET = process.env.ASCEND_WEBHOOK_SECRET!;
// Raw body parser for signature verification
app.use('/webhooks/ascend', express.raw({ type: 'application/json' }));
// Verify webhook signature
function verifySignature(payload: Buffer, timestamp: string, signature: string): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${payload.toString()}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(`v1=${expected}`),
Buffer.from(signature)
);
}
// Event handlers
type EventHandler = (data: Record<string, unknown>) => Promise<void>;
const eventHandlers: Record<string, EventHandler> = {
'action.approved': async (data) => {
console.log(`Action ${data.action_id} approved by ${data.approver}`);
await executePendingAction(data.action_id as string);
},
'action.denied': async (data) => {
console.log(`Action ${data.action_id} denied: ${data.reason}`);
await notifyUserOfDenial(data.action_id as string, data.reason as string);
},
'policy.violation': async (data) => {
console.log(`Policy violation by ${data.agent_id}: ${data.violations}`);
await alertSecurityTeam(data.agent_id as string, data.violations as string[]);
},
'agent.trust_changed': async (data) => {
console.log(`Agent ${data.agent_id} trust: ${data.old_level} -> ${data.new_level}`);
}
};
// Webhook endpoint
app.post('/webhooks/ascend', async (req: Request, res: Response) => {
const signature = req.headers['x-signature'] as string;
const timestamp = req.headers['x-timestamp'] as string;
// Verify signature
if (!verifySignature(req.body, timestamp, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
const eventType = payload.event;
const data = payload.data;
// Route to handler
const handler = eventHandlers[eventType];
if (handler) {
try {
await handler(data);
return res.json({ status: 'processed' });
} catch (error) {
console.error(`Handler error: ${error}`);
return res.status(500).json({ error: 'Processing failed' });
}
}
return res.json({ status: 'ignored', event: eventType });
});
// Placeholder functions
async function executePendingAction(actionId: string): Promise<void> {}
async function notifyUserOfDenial(actionId: string, reason: string): Promise<void> {}
async function alertSecurityTeam(agentId: string, violations: string[]): Promise<void> {}
app.listen(8080, () => {
console.log('Webhook server running on port 8080');
});
Error Handling Patterns
Comprehensive error handling with TypeScript.
/**
* Resilient error handling patterns
*/
import {
AscendClient,
Decision,
FailMode,
AuthenticationError,
AuthorizationError,
TimeoutError,
RateLimitError,
ConnectionError,
CircuitBreakerOpenError,
ValidationError
} from '@ascend/sdk';
interface ActionResult {
status: 'success' | 'denied' | 'error' | 'blocked';
data?: unknown;
errorType?: string;
message?: string;
recoverable?: boolean;
}
class ResilientAgent {
private client: AscendClient;
private failOpen: boolean;
constructor(apiKey: string, failOpen: boolean = false) {
this.client = new AscendClient({
apiKey,
agentId: 'resilient-agent',
agentName: 'Resilient Agent',
failMode: failOpen ? FailMode.OPEN : FailMode.CLOSED,
timeout: 5000,
maxRetries: 3
});
this.failOpen = failOpen;
}
async governedAction(
actionType: string,
resource: string,
parameters: Record<string, unknown>,
maxRetries: number = 3
): Promise<ActionResult> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await this.executeWithGovernance(actionType, resource, parameters);
} catch (error) {
const result = this.handleError(error as Error, attempt, maxRetries);
if (result) return result;
// Wait before retry
await this.sleep(Math.pow(2, attempt) * 1000);
}
}
return this.handleServiceUnavailable('max_retries');
}
private async executeWithGovernance(
actionType: string,
resource: string,
parameters: Record<string, unknown>
): Promise<ActionResult> {
const decision = await this.client.evaluateAction({
actionType,
resource,
parameters
});
if (decision.decision === Decision.ALLOWED) {
return { status: 'success', data: { executed: true } };
}
if (decision.decision === Decision.PENDING) {
return {
status: 'denied',
message: 'Awaiting approval',
recoverable: true
};
}
return {
status: 'denied',
message: decision.reason,
recoverable: false
};
}
private handleError(
error: Error,
attempt: number,
maxRetries: number
): ActionResult | null {
if (error instanceof AuthenticationError) {
return {
status: 'error',
errorType: 'authentication',
message: 'Invalid API credentials',
recoverable: false
};
}
if (error instanceof AuthorizationError) {
return {
status: 'denied',
errorType: 'authorization',
message: error.message,
recoverable: false
};
}
if (error instanceof ValidationError) {
return {
status: 'error',
errorType: 'validation',
message: error.message,
recoverable: false
};
}
if (error instanceof RateLimitError) {
// Don't return - will retry
console.log(`Rate limited. Waiting ${error.retryAfter}s...`);
return null;
}
if (error instanceof TimeoutError) {
if (attempt < maxRetries - 1) {
console.log(`Timeout. Retry ${attempt + 1}/${maxRetries}`);
return null;
}
return this.handleServiceUnavailable('timeout');
}
if (error instanceof ConnectionError) {
if (attempt < maxRetries - 1) {
console.log(`Connection error. Retry ${attempt + 1}/${maxRetries}`);
return null;
}
return this.handleServiceUnavailable('connection');
}
if (error instanceof CircuitBreakerOpenError) {
return this.handleServiceUnavailable('circuit_open');
}
// Unknown error
return {
status: 'error',
errorType: 'unexpected',
message: error.message,
recoverable: false
};
}
private handleServiceUnavailable(reason: string): ActionResult {
if (this.failOpen) {
console.warn(`ASCEND unavailable (${reason}). Allowing (fail-open).`);
return {
status: 'success',
data: { failOpen: true },
message: 'Allowed due to service unavailability'
};
}
return {
status: 'blocked',
errorType: 'service_unavailable',
message: 'Governance service unavailable',
recoverable: true
};
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const agent = new ResilientAgent(process.env.ASCEND_API_KEY!);
const result = await agent.governedAction(
'database.query',
'production_db',
{ query: 'SELECT * FROM users' }
);
console.log(result);
Metrics and Monitoring
Collect and export metrics.
/**
* Metrics and monitoring integration
*/
import { AscendClient, Decision } from '@ascend/sdk';
interface MetricData {
name: string;
value: number;
tags: Record<string, string>;
timestamp: Date;
}
class MonitoredAgent {
private client: AscendClient;
private metrics: MetricData[] = [];
constructor(apiKey: string) {
this.client = new AscendClient({
apiKey,
agentId: 'monitored-agent',
agentName: 'Monitored Agent',
enableMetrics: true
});
}
async evaluateWithMetrics(
actionType: string,
resource: string,
parameters: Record<string, unknown>
): Promise<void> {
const startTime = Date.now();
try {
const decision = await this.client.evaluateAction({
actionType,
resource,
parameters
});
const duration = Date.now() - startTime;
// Record latency
this.recordMetric('ascend.latency', duration, {
actionType,
resource,
decision: decision.decision
});
// Record decision
this.recordMetric('ascend.decision', 1, {
actionType,
resource,
decision: decision.decision,
riskLevel: this.getRiskLevel(decision.riskScore)
});
} catch (error) {
const duration = Date.now() - startTime;
this.recordMetric('ascend.error', 1, {
actionType,
resource,
errorType: (error as Error).constructor.name
});
this.recordMetric('ascend.latency', duration, {
actionType,
resource,
decision: 'error'
});
throw error;
}
}
private recordMetric(
name: string,
value: number,
tags: Record<string, string>
): void {
this.metrics.push({
name,
value,
tags,
timestamp: new Date()
});
// Export to monitoring system
this.exportToDatadog(name, value, tags);
}
private exportToDatadog(
name: string,
value: number,
tags: Record<string, string>
): void {
// Integration with Datadog
console.log(`[Datadog] ${name}: ${value}`, tags);
}
private getRiskLevel(score?: number): string {
if (!score) return 'unknown';
if (score < 45) return 'low';
if (score < 70) return 'medium';
if (score < 85) return 'high';
return 'critical';
}
getMetricsSnapshot() {
const snapshot = this.client.getMetrics();
return {
...snapshot,
customMetrics: this.metrics
};
}
}
Testing Patterns
Test your agents without hitting production.
/**
* Testing patterns for ASCEND SDK
*/
import { AscendClient, Decision, EvaluateActionResult } from '@ascend/sdk';
// Mock client for unit tests
class MockAscendClient {
private defaultDecision: Decision;
public calls: Array<Record<string, unknown>> = [];
constructor(defaultDecision: Decision = Decision.ALLOWED) {
this.defaultDecision = defaultDecision;
}
async evaluateAction(options: Record<string, unknown>): Promise<EvaluateActionResult> {
this.calls.push(options);
return {
decision: this.defaultDecision,
actionId: `test_action_${this.calls.length}`,
reason: 'Test decision',
riskScore: 25,
policyViolations: [],
conditions: [],
requiredApprovers: [],
metadata: {}
};
}
async logActionCompleted(actionId: string, result?: unknown): Promise<void> {
this.calls.push({ type: 'completed', actionId, result });
}
async logActionFailed(actionId: string, error: Error): Promise<void> {
this.calls.push({ type: 'failed', actionId, error: error.message });
}
setDecision(decision: Decision): void {
this.defaultDecision = decision;
}
}
// Jest tests
describe('MyAgent', () => {
let mockClient: MockAscendClient;
beforeEach(() => {
mockClient = new MockAscendClient();
});
test('executes allowed actions', async () => {
const agent = new MyAgent(mockClient as unknown as AscendClient);
const result = await agent.doSomething();
expect(result.status).toBe('success');
expect(mockClient.calls.length).toBeGreaterThan(0);
});
test('handles denied actions', async () => {
mockClient.setDecision(Decision.DENIED);
const agent = new MyAgent(mockClient as unknown as AscendClient);
const result = await agent.doSomething();
expect(result.status).toBe('denied');
});
test('sends correct parameters', async () => {
const agent = new MyAgent(mockClient as unknown as AscendClient);
await agent.queryDatabase('SELECT * FROM users');
const call = mockClient.calls[0];
expect(call.actionType).toBe('database.query');
expect((call.parameters as Record<string, unknown>).query).toContain('SELECT');
});
});
// Integration tests with staging
describe('Integration Tests', () => {
let client: AscendClient;
beforeAll(() => {
client = new AscendClient({
apiKey: process.env.ASCEND_STAGING_API_KEY!,
agentId: 'test-agent',
agentName: 'Integration Test Agent',
apiUrl: 'https://staging.owkai.app'
});
});
test('connects to staging', async () => {
const status = await client.testConnection();
expect(status.status).toBe('connected');
});
test('evaluates low-risk action', async () => {
const decision = await client.evaluateAction({
actionType: 'database.query',
resource: 'test_db',
parameters: { query: 'SELECT 1' }
});
expect(decision.decision).toBe(Decision.ALLOWED);
});
});
// Placeholder class
class MyAgent {
constructor(private client: AscendClient) {}
async doSomething(): Promise<{ status: string }> {
const decision = await this.client.evaluateAction({
actionType: 'test.action',
resource: 'test',
parameters: {}
});
return { status: decision.decision === Decision.ALLOWED ? 'success' : 'denied' };
}
async queryDatabase(query: string): Promise<unknown> {
await this.client.evaluateAction({
actionType: 'database.query',
resource: 'production_db',
parameters: { query }
});
return [];
}
}