Beltic logo
Examples

Express.js Middleware Integration

Complete Express middleware setup for verifying agent requests with scope-based authorization

Express.js Middleware Integration

This example demonstrates how to integrate Beltic agent verification into an Express.js application with scope-based authorization.

Installation

npm install express @belticlabs/kya

Basic Middleware Setup

import express from 'express';
import { verifyAgentRequest } from '@belticlabs/kya';

const app = express();
app.use(express.json());

// Verification middleware factory
function requireAgent(options?: { scopes?: string[] }) {
  return async (
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    const result = await verifyAgentRequest(req, {
      requiredScopes: options?.scopes,
    });

    if (!result.verified) {
      return res.status(401).json({
        error: 'Unauthorized',
        code: result.error?.code,
        message: result.error?.message,
      });
    }

    // Attach verified context to request
    req.agent = result.agent;
    req.developer = result.developer;
    next();
  };
}

// Extend Express Request type
declare global {
  namespace Express {
    interface Request {
      agent?: {
        id: string;
        name: string;
        scopes: string[];
      };
      developer?: {
        id: string;
        legalName: string;
        kybTier: string;
      };
    }
  }
}

Scope-Based Authorization

Defining Scopes

Structure scopes hierarchically for flexible access control:

// Scope hierarchy
const SCOPES = {
  // Read access
  'data:read': 'Read data',
  'users:read': 'Read user information',
  'payments:read': 'Read payment history',

  // Write access
  'data:write': 'Write data',
  'users:write': 'Modify user information',
  'payments:write': 'Initiate payments',

  // Admin access
  'admin:*': 'Full admin access',
} as const;

Route Protection

// Public endpoint - no auth required
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Read-only endpoint
app.get('/api/users/:id', requireAgent({ scopes: ['users:read'] }), (req, res) => {
  const { agent, developer } = req;
  console.log(`Agent ${agent?.id} from ${developer?.legalName} accessing user data`);

  res.json({
    id: req.params.id,
    name: 'John Doe',
    email: 'john@example.com',
  });
});

// Write endpoint - requires elevated scope
app.post('/api/users', requireAgent({ scopes: ['users:write'] }), (req, res) => {
  const { name, email } = req.body;

  res.status(201).json({
    id: 'usr_new123',
    name,
    email,
    createdBy: req.agent?.id,
  });
});

// Payment endpoint - requires specific payment scope
app.post('/api/payments/transfer', requireAgent({ scopes: ['payments:write'] }), (req, res) => {
  const { amount, recipient } = req.body;

  // Log for audit
  console.log(`Payment initiated by agent ${req.agent?.id}`);
  console.log(`Developer: ${req.developer?.legalName} (${req.developer?.kybTier})`);

  res.json({
    transactionId: 'txn_abc123',
    amount,
    recipient,
    status: 'pending',
  });
});

KYB Tier-Based Access

Restrict endpoints based on developer verification level:

function requireKybTier(minTier: 'tier_1' | 'tier_2' | 'tier_3') {
  const tierLevels = { tier_1: 1, tier_2: 2, tier_3: 3 };

  return (
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    const developerTier = req.developer?.kybTier;
    if (!developerTier) {
      return res.status(403).json({
        error: 'Forbidden',
        message: 'Developer verification required',
      });
    }

    // Extract tier level (e.g., 'tier_2_enhanced' -> 'tier_2')
    const tierMatch = developerTier.match(/tier_(\d)/);
    const currentLevel = tierMatch ? parseInt(tierMatch[1]) : 0;
    const requiredLevel = tierLevels[minTier];

    if (currentLevel < requiredLevel) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `This endpoint requires ${minTier} verification or higher`,
        currentTier: developerTier,
        requiredTier: minTier,
      });
    }

    next();
  };
}

// High-value transactions require tier 2
app.post(
  '/api/payments/high-value',
  requireAgent({ scopes: ['payments:write'] }),
  requireKybTier('tier_2'),
  (req, res) => {
    res.json({ status: 'approved' });
  }
);

Error Handling

Comprehensive error handling for verification failures:

import { VerificationErrorCode } from '@belticlabs/kya';

function handleVerificationError(
  error: { code?: string; message?: string },
  res: express.Response
) {
  const errorResponses: Record<string, { status: number; message: string }> = {
    SIGNATURE_INVALID: {
      status: 401,
      message: 'Request signature is invalid or has been tampered with',
    },
    SIGNATURE_EXPIRED: {
      status: 401,
      message: 'Request signature has expired. Signatures are valid for 5 minutes.',
    },
    KEY_NOT_FOUND: {
      status: 401,
      message: 'Signing key not found in key directory',
    },
    CREDENTIAL_EXPIRED: {
      status: 401,
      message: 'Agent credential has expired. Please renew your credential.',
    },
    SCOPE_INSUFFICIENT: {
      status: 403,
      message: 'Agent does not have required scopes for this operation',
    },
    DEVELOPER_SUSPENDED: {
      status: 403,
      message: 'Developer account has been suspended',
    },
  };

  const errorInfo = errorResponses[error.code || ''] || {
    status: 401,
    message: error.message || 'Authentication failed',
  };

  return res.status(errorInfo.status).json({
    error: 'Authentication Error',
    code: error.code,
    message: errorInfo.message,
  });
}

// Enhanced middleware with detailed errors
function requireAgentWithDetailedErrors(options?: { scopes?: string[] }) {
  return async (
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    const result = await verifyAgentRequest(req, {
      requiredScopes: options?.scopes,
    });

    if (!result.verified) {
      return handleVerificationError(result.error || {}, res);
    }

    req.agent = result.agent;
    req.developer = result.developer;
    next();
  };
}

Audit Logging

Log all verified requests for compliance:

interface AuditLog {
  timestamp: string;
  agentId: string;
  developerId: string;
  developerName: string;
  kybTier: string;
  method: string;
  path: string;
  scopes: string[];
  ip: string;
}

function auditLogger(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  if (req.agent && req.developer) {
    const auditEntry: AuditLog = {
      timestamp: new Date().toISOString(),
      agentId: req.agent.id,
      developerId: req.developer.id,
      developerName: req.developer.legalName,
      kybTier: req.developer.kybTier,
      method: req.method,
      path: req.path,
      scopes: req.agent.scopes,
      ip: req.ip || 'unknown',
    };

    // Log to your audit system
    console.log('AUDIT:', JSON.stringify(auditEntry));
  }
  next();
}

// Apply to all protected routes
app.use('/api/*', auditLogger);

Complete Server Example

import express from 'express';
import { verifyAgentRequest } from '@belticlabs/kya';

const app = express();
app.use(express.json());

// Type definitions
declare global {
  namespace Express {
    interface Request {
      agent?: { id: string; name: string; scopes: string[] };
      developer?: { id: string; legalName: string; kybTier: string };
    }
  }
}

// Middleware factory
function requireAgent(scopes?: string[]) {
  return async (
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    const result = await verifyAgentRequest(req, { requiredScopes: scopes });

    if (!result.verified) {
      return res.status(401).json({
        error: 'Unauthorized',
        code: result.error?.code,
        message: result.error?.message,
      });
    }

    req.agent = result.agent;
    req.developer = result.developer;
    next();
  };
}

// Routes
app.get('/health', (req, res) => res.json({ status: 'ok' }));

app.get('/api/data', requireAgent(['data:read']), (req, res) => {
  res.json({
    data: 'sensitive information',
    accessedBy: req.agent?.id,
  });
});

app.post('/api/data', requireAgent(['data:write']), (req, res) => {
  res.status(201).json({
    created: true,
    createdBy: req.agent?.id,
  });
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Testing Your Middleware

Use the Beltic CLI to generate test requests:

# Generate a signed test request
beltic sign-request \
  --method POST \
  --url http://localhost:3000/api/data \
  --key agent-key.pem \
  --body '{"test": true}'

Or programmatically with the SDK:

import { signHttpRequest, importPrivateKey } from '@belticlabs/kya';
import fs from 'fs';

async function testEndpoint() {
  const privateKey = await importPrivateKey(
    fs.readFileSync('agent-key.pem', 'utf-8'),
    'EdDSA'
  );

  const body = JSON.stringify({ test: true });

  const headers = await signHttpRequest({
    method: 'POST',
    url: 'http://localhost:3000/api/data',
    headers: { 'content-type': 'application/json' },
    body,
    privateKey,
    keyId: process.env.AGENT_KEY_ID!,
    keyDirectoryUrl: process.env.KEY_DIRECTORY_URL!,
    credentialUrl: process.env.CREDENTIAL_URL!,
  });

  const response = await fetch('http://localhost:3000/api/data', {
    method: 'POST',
    headers: { ...headers, 'content-type': 'application/json' },
    body,
  });

  console.log(await response.json());
}

Next Steps