JSPM

  • Created
  • Published
  • Downloads 5
  • Score
    100M100P100Q104210F
  • License UNLICENSED

RODiT-based authentication system for Express.js applications

Package Exports

  • @rodit/rodit-auth-be

Readme

RODiT Authentication SDK

A comprehensive Node.js SDK for implementing RODiT-based mutual authentication, authorization, self-configuration, and session management in Express.js applications.

Version: 4.0.2
License: Proprietary
Author: Discernible Inc.

Table of Contents

Quick Start

Installation

npm install @rodit/rodit-auth-be

Basic Server Setup

const express = require('express');
const { RoditClient, setExpressSessionStore } = require('@rodit/rodit-auth-be');
const { ulid } = require('ulid');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);

const app = express();
let roditClient;

// Configure session storage BEFORE initializing RoditClient
const sessionStore = new SQLiteStore({
  db: 'sessions.db',
  dir: './data',
  table: 'sessions'
});
setExpressSessionStore(sessionStore);

// Configure Express middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// Request context middleware
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || ulid();
  req.startTime = Date.now();
  next();
});

// Server startup with SDK initialization
async function startServer() {
  try {
    // Initialize RODiT client (use 'server' for server applications)
    roditClient = await RoditClient.create('server');
    
    // Store client in app.locals for route access
    app.locals.roditClient = roditClient;
    
    // Get logger and other services from client
    const logger = roditClient.getLogger();
    const config = roditClient.getConfig();
    const loggingmw = roditClient.getLoggingMiddleware();
    
    // Apply logging middleware
    app.use(loggingmw);
    
    // Create authentication middleware
    const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
    const authorize = (req, res, next) => roditClient.authorize(req, res, next);
    
    // Public routes
    app.post('/api/login', (req, res) => {
      req.logAction = 'login-attempt';
      return roditClient.login_client(req, res);
    });
    
    // Protected routes
    app.post('/api/logout', authenticate, (req, res) => {
      req.logAction = 'logout-attempt';
      return roditClient.logout_client(req, res);
    });
    
    app.get('/api/protected', authenticate, (req, res) => {
      res.json({ message: 'Protected data', user: req.user });
    });
    
    // Protected + authorized routes
    app.use('/api/admin', authenticate, authorize, adminRoutes);
    
    const port = config.get('SERVERPORT', 3000);
    app.listen(port, () => {
      logger.info(`RODiT Authentication Server running on port ${port}`);
    });
  } catch (error) {
    console.error('Server initialization failed:', error);
    process.exit(1);
  }
}

startServer();

Core Concepts

The RoditClient Pattern

The SDK centers around the RoditClient class, which provides a unified interface for all RODiT operations:

  • Single Initialization: Create once with RoditClient.create(role) where role is 'server', 'client', or 'portal'
  • Shared Instance: Store in app.locals for access across routes and middleware
  • Self-Configuring: Automatically loads configuration from Vault, files, or environment variables
  • Encapsulated: All SDK functionality accessed through the client instance
  • Session Management: Built-in session tracking with pluggable storage backends
  • Performance Monitoring: Integrated request tracking and metrics collection

App.locals Pattern

Store the initialized client in app.locals for consistent access across your application:

// In main app.js
roditClient = await RoditClient.create('server');
app.locals.roditClient = roditClient;

// In route modules
const router = express.Router();

router.get('/data', (req, res) => {
  const client = req.app.locals.roditClient;
  const logger = client.getLogger();
  
  logger.info('Processing request', {
    component: 'DataRoute',
    userId: req.user?.id
  });
  
  res.json({ data: 'example' });
});

Authentication Middleware Pattern

Create middleware functions that delegate to the RoditClient:

// Create reusable middleware
const authenticate = (req, res, next) => {
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authentication service unavailable' });
  }
  return client.authenticate(req, res, next);
};

const authorize = (req, res, next) => {
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authorization service unavailable' });
  }
  return client.authorize(req, res, next);
};

// Use in routes
app.get('/api/protected', authenticate, handler);
app.post('/api/admin', authenticate, authorize, adminHandler);

Installation & Setup

Dependencies

Required:

npm install @rodit/rodit-auth-be express config winston

Recommended for Production:

npm install express-session connect-sqlite3

Optional:

npm install node-vault  # For Vault-based credentials
npm install winston-loki  # For Grafana Loki logging

Environment Variables

Vault Configuration (Production):

export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id
export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
export SERVICE_NAME=your-service-name
export NEAR_CONTRACT_ID=your-contract.testnet

Application Configuration:

export SERVERPORT=3000
export NODE_ENV=production  # Environment: production, development, test
export LOG_LEVEL=info       # Logging: error, warn, info, debug, trace
export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite

Logging Configuration:

export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:password

Configuration Files

Create config/default.json:

{
  "NEAR_CONTRACT_ID": "your-contract.testnet",
  "SERVICE_NAME": "your-service",
  "SERVERPORT": 3000,
  "API_DEFAULT_OPTIONS": {
    "LOG_DIR": "/app/logs",
    "DB_PATH": "/app/data/database.sqlite"
  },
  "SECURITY_OPTIONS": {
    "SILENT_LOGIN_FAILURES": false,
    "JWT_DURATION": 3600
  }
}

Authentication

RODiT-Based Authentication

RODiT provides cryptographic mutual authentication using blockchain-verified identities.

Client Login Request

Clients authenticate by sending RODiT credentials:

// POST /api/login
{
  "roditid": "01K4G3D95QF6NR0RSJK9WEK6KA",
  "timestamp": 1640995200,
  "roditid_base64url_signature": "base64url-encoded-signature"
}

Server Response

// Success (200)
{
  "message": "Login successful",
  "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
  "requestId": "01HQXYZ123ABC"
}

// Headers:
// Authorization: Bearer eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...

Authentication Flow

  1. Client sends RODiT credentials - RODiT ID, timestamp, and cryptographic signature
  2. SDK verifies signature - Validates against blockchain records (NEAR Protocol)
  3. Session created - New session stored in session manager
  4. JWT token issued - Token contains session ID and user claims
  5. Subsequent requests - Client sends JWT in Authorization: Bearer <token> header
  6. Token validation - SDK validates JWT and checks session status

Login Implementation

// routes/login.js
const express = require('express');
const router = express.Router();

router.post('/login', async (req, res) => {
  req.logAction = 'login-attempt';
  
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authentication service unavailable' });
  }
  
  // Delegate to SDK's login_client method
  await client.login_client(req, res);
});

module.exports = router;

Logout Implementation

// Logout invalidates the JWT token and closes the session
router.post('/logout', authenticate, async (req, res) => {
  req.logAction = 'logout-attempt';
  
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authentication service unavailable' });
  }
  
  // Delegate to SDK's logout_client method
  await client.logout_client(req, res);
});

Protected Routes

// Require authentication for access
app.get('/api/data', authenticate, (req, res) => {
  // req.user contains authenticated user information
  const logger = req.app.locals.roditClient.getLogger();
  
  logger.info('Protected route accessed', {
    component: 'API',
    userId: req.user.id,
    roditId: req.user.roditId,
    requestId: req.requestId
  });
  
  res.json({
    message: 'Authenticated data',
    user: req.user,
    requestId: req.requestId
  });
});

Authentication Middleware

The authenticate middleware validates JWT tokens and populates req.user:

const authenticate = (req, res, next) => {
  const client = req.app.locals.roditClient;
  return client.authenticate(req, res, next);
};

// After successful authentication, req.user contains:
// {
//   id: 'user-unique-id',
//   roditId: '01K4G3D95QF6NR0RSJK9WEK6KA',
//   aud: 'audience',
//   iss: 'issuer',
//   exp: 1640999999,
//   iat: 1640995200,
//   session_id: '01HQXYZ123ABC'
// }

Authorization & Permissions

Route-Based Permissions

Permissions are configured in your RODiT token metadata using the permissioned_routes field:

{
  "permissioned_routes": {
    "entities": {
      "/": {
        "methods": "+0"
      },
      "/api/echo": {
        "methods": "+0"
      },
      "/api/cruda/create": {
        "methods": "+0"
      },
      "/api/cruda/list": {
        "methods": "+0"
      },
      "/api/admin": {
        "methods": "+0"
      }
    }
  }
}

Permission Format:

  • "+0" = All methods allowed (GET, POST, PUT, DELETE, etc.)
  • "+1" = GET only
  • "+2" = POST only
  • Custom combinations can be defined

Permission Validation Middleware

The authorize middleware validates that the authenticated user has permission to access the requested route:

const authenticate = (req, res, next) => {
  return req.app.locals.roditClient.authenticate(req, res, next);
};

const authorize = (req, res, next) => {
  return req.app.locals.roditClient.authorize(req, res, next);
};

// Apply both authentication and authorization
app.use('/api/admin', authenticate, authorize, adminRoutes);

// CRUDA endpoints with full protection
app.use('/api/cruda', authenticate, authorize, crudaRoutes);

Permission Enforcement

// Example: CRUDA routes with permission checking
const router = express.Router();

// All routes require authentication + authorization
router.post('/create', async (req, res) => {
  // User must have permission for POST /api/cruda/create
  const { comment, author } = req.body;
  
  // Create record in database
  const result = await db.run(
    'INSERT INTO comments (comment, author) VALUES (?, ?)',
    [comment, author || req.user.roditId]
  );
  
  res.json({ id: result.lastID, requestId: req.requestId });
});

router.post('/list', async (req, res) => {
  // User must have permission for POST /api/cruda/list
  const records = await db.all('SELECT * FROM comments ORDER BY created_at DESC');
  res.json({ records, requestId: req.requestId });
});

module.exports = router;

Dynamic Permission Checking

// Check permissions programmatically
const client = req.app.locals.roditClient;
const hasPermission = client.isOperationPermitted('POST', '/api/admin/users');

if (!hasPermission) {
  return res.status(403).json({
    error: 'Forbidden',
    message: 'You do not have permission to access this resource',
    requestId: req.requestId
  });
}

// Proceed with operation

Permission Validation in Client Token Minting

When minting client tokens via /api/signclient, the server validates that requested permissions are a subset of the server's own permissions:

// Client requests these permissions:
const requestedPermissions = {
  "/": "+0",
  "/api/echo": "+0",
  "/api/cruda/create": "+0"
};

// Server validates against its own permissioned_routes
// If any requested route is not in server's config, request is rejected with HTTP 400

Session Management

Overview

The SDK includes a comprehensive session management system that:

  • Tracks active user sessions
  • Validates JWT tokens against session state
  • Supports pluggable storage backends
  • Automatically cleans up expired sessions
  • Integrates with performance metrics

Session Storage Backends

1. In-Memory Storage (Default)

No configuration needed - works out of the box:

const client = await RoditClient.create('server');
// Uses InMemorySessionStorage by default

Pros: Fast, zero configuration
Cons: Sessions lost on server restart, not suitable for multi-server deployments

Persistent storage using SQLite database:

const express = require('express');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const { RoditClient, setExpressSessionStore } = require('@rodit/rodit-auth-be');

// Configure BEFORE initializing RoditClient
const sessionStore = new SQLiteStore({
  db: 'sessions.db',
  dir: './data',
  table: 'sessions'
});

setExpressSessionStore(sessionStore);

// Now initialize client
const client = await RoditClient.create('server');

Pros: Persistent across restarts, simple setup, uses existing database infrastructure
Cons: Not suitable for multi-server deployments

3. Redis Storage (For Multi-Server)

npm install express-session connect-redis redis
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const { setExpressSessionStore } = require('@rodit/rodit-auth-be');

// Create Redis client
const redisClient = createClient({
  url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
});
await redisClient.connect();

// Create Redis store
const redisStore = new RedisStore({
  client: redisClient,
  prefix: 'rodit:sess:',
  ttl: 86400 // 24 hours
});

setExpressSessionStore(redisStore);

const client = await RoditClient.create('server');

Pros: Shared sessions across multiple servers, high performance
Cons: Requires Redis infrastructure

Session Operations

// Get session manager
const sessionManager = roditClient.getSessionManager();

// Get active session count
const activeCount = await sessionManager.getActiveSessionCount();

// Enumerate sessions via storage
const allSessions = await sessionManager.storage.getAll();
// Or fallback using keys() + get()
const sessionIds = await sessionManager.storage.keys();
const sessions = [];
for (const id of sessionIds) {
  const session = await sessionManager.storage.get(id);
  if (session) sessions.push(session);
}

// Check if token is invalidated
const isInvalidated = await sessionManager.isTokenInvalidated(jwtToken);

// Manually close a session
await sessionManager.closeSession(sessionId);

// Run manual cleanup (removes expired sessions)
const cleanup = await sessionManager.runManualCleanup();
console.log(`Removed ${cleanup.removedSessionsCount} expired sessions`);

Session Lifecycle

  1. Login - Session created, JWT token issued with session ID
  2. Active - Token validated on each request, session last_accessed updated
  3. Logout - Session closed, token invalidated, termination token issued
  4. Expiration - Sessions automatically expire based on JWT duration
  5. Cleanup - Expired sessions removed by automatic cleanup process

Token Invalidation

The SDK validates tokens by checking session state:

// Authentication middleware checks:
// 1. JWT signature validity
// 2. JWT expiration
// 3. Session exists and is active
// 4. Session not expired

// After logout, tokens are invalidated because:
// - Session status set to 'closed'
// - Subsequent requests fail authentication

Configuration

Automatic Configuration Loading

The SDK automatically configures itself from multiple sources (in priority order):

  1. Environment Variables (Highest priority)
  2. Configuration Files (config/default.json, config/production.json)
  3. Vault Credentials (Production)
  4. SDK Defaults (Fallback)

Environment Configuration: NODE_ENV and LOG_LEVEL

The SDK uses two separate environment variables for configuration, following Node.js ecosystem standards:

NODE_ENV - Environment Type & Security Behavior

Controls environment-specific behavior and security settings:

Values:

  • production - Production environment (strict security, no error details)
  • development - Development environment (relaxed security, detailed errors)
  • test - Testing environment (allows bypasses for automated testing)
  • staging - Staging environment (production security with optional verbose logging)

Default: production (secure by default)

Controls:

  • ✅ Error detail exposure in API responses
  • ✅ Peer public key requirement enforcement
  • ✅ Webhook verification bypass (test mode only)
  • ✅ Security-critical behavior

LOG_LEVEL - Logging Verbosity

Controls Winston logger verbosity independently from environment:

Values:

  • error - Only errors
  • warn - Warnings and errors
  • info - Informational messages, warnings, and errors (recommended for production)
  • debug - Detailed debugging information
  • trace - Maximum verbosity with full traces

Default: info

Controls:

  • ✅ Winston logger output level
  • ✅ Debug payload logging
  • ✅ Log verbosity only (not security)

Separation of Concerns

// Environment detection (security)
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';
const isTest = process.env.NODE_ENV === 'test';

// Logging verbosity (independent)
const config = roditClient.getConfig();
const logLevel = config.get('LOG_LEVEL', 'info');

Configuration Examples

Production (normal):

export NODE_ENV=production
export LOG_LEVEL=info
# Results in:
# - Strict security enforcement
# - No error details in responses
# - Minimal logging output

Production (troubleshooting):

export NODE_ENV=production
export LOG_LEVEL=debug
# Results in:
# - Strict security enforcement (still production)
# - No error details in responses (still secure)
# - Verbose logging for debugging

Development:

export NODE_ENV=development
export LOG_LEVEL=debug
# Results in:
# - Relaxed security for development
# - Detailed error messages in responses
# - Verbose logging

Testing:

export NODE_ENV=test
export LOG_LEVEL=error
# Results in:
# - Test mode (allows bypasses)
# - Detailed error messages
# - Only errors logged (cleaner test output)

Staging:

export NODE_ENV=production
export LOG_LEVEL=warn
# Results in:
# - Production security
# - No error details exposed
# - Only warnings and errors logged

Behavior Matrix

Scenario NODE_ENV LOG_LEVEL Security Error Details Logging
Production production info ✅ Strict ❌ Hidden Minimal
Production Debug production debug ✅ Strict ❌ Hidden Verbose
Development development debug ⚠️ Relaxed ✅ Shown Verbose
Testing test error ⚠️ Bypass OK ✅ Shown Errors only
Staging production warn ✅ Strict ❌ Hidden Warnings

Vault-Based Configuration (Production)

For production deployments, credentials are loaded from HashiCorp Vault:

# Environment variables for vault
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id
export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
export SERVICE_NAME=your-service-name
export NEAR_CONTRACT_ID=your-contract.testnet

File-Based Configuration (Development)

For development, credentials can be loaded from files:

export RODIT_NEAR_CREDENTIALS_SOURCE=file
export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json

Accessing Configuration

// Get complete RODiT configuration
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;

// Access RODiT token metadata
const jwtDuration = metadata.jwt_duration;  // JWT expiration time
const maxRequests = metadata.max_requests;  // Rate limit
const maxRqWindow = metadata.maxrq_window;  // Rate limit window
const apiEndpoint = metadata.subjectuniqueidentifier_url;  // API URL
const webhookUrl = metadata.webhook_url;  // Webhook endpoint

// Parse permissioned routes
const permissionedRoutes = JSON.parse(metadata.permissioned_routes || '{}');

// Use SDK config for application settings
const config = roditClient.getConfig();
const serverPort = config.get('SERVERPORT', 3000);
const logLevel = config.get('LOG_LEVEL', 'info');
const dbPath = config.get('API_DEFAULT_OPTIONS.DB_PATH');

Dynamic Rate Limiting

// Configure rate limiting from RODiT token
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;

if (metadata.max_requests && metadata.maxrq_window) {
  const maxRequests = parseInt(metadata.max_requests);
  const windowSeconds = parseInt(metadata.maxrq_window);
  
  const rateLimiter = roditClient.getRateLimitMiddleware();
  app.use(rateLimiter(maxRequests, windowSeconds));
}

Logging & Monitoring

Structured Logging

The SDK provides comprehensive structured logging:

const { logger } = require('@rodit/rodit-auth-be');

// Basic logging
logger.info('Operation completed', {
  component: 'UserService',
  operation: 'createUser',
  userId: '123',
  duration: 150
});

// Context-aware logging
logger.infoWithContext('Request processed', {
  component: 'API',
  method: 'POST',
  path: '/api/users',
  requestId: req.requestId,
  userId: req.user?.id,
  duration: Date.now() - req.startTime
});

// Error logging with metrics
logger.errorWithContext('Operation failed', {
  component: 'UserService',
  operation: 'createUser',
  requestId: req.requestId,
  error: error.message,
  stack: error.stack
}, error);

Loki with the SDK (canonical)

Use this as the authoritative guide for configuring logging with the SDK.

Environment variables

export LOKI_URL=https://<your-loki-host>:3100
export LOKI_BASIC_AUTH="username:password"   # store in secrets
export LOKI_TLS_SKIP_VERIFY=true              # only for self-signed/test
export LOG_LEVEL=info
export SERVICE_NAME=clienttestapi-api

These are already mapped in config/custom-environment-variables.json, so container/CI env vars will flow into the app.

How the SDK selects/configures the logger

  • Default: JSON to stdout only (no Loki). Honors LOG_LEVEL, adds service_name.
  • Production: Create a Winston logger with a winston-loki transport and inject it once: logger.setLogger(customLogger).
  • Access: const { logger } = require('@rodit/rodit-auth-be') or roditClient.getLogger() both delegate to the same facade.
const { logger } = require('@rodit/rodit-auth-be');
const winston = require('winston');
const LokiTransport = require('winston-loki');

const transports = [new winston.transports.Console({ format: winston.format.json() })];

if (process.env.LOKI_URL) {
  const lokiOptions = {
    host: process.env.LOKI_URL,
    basicAuth: process.env.LOKI_BASIC_AUTH, // Basic Auth for Loki
    labels: { app: process.env.SERVICE_NAME || 'clienttestapi-api', component: 'rodit-sdk' },
    json: true,
    batching: true
  };
  if ((process.env.LOKI_TLS_SKIP_VERIFY || '').toLowerCase() === 'true') {
    lokiOptions.ssl = { rejectUnauthorized: false };
  }
  transports.push(new LokiTransport(lokiOptions));
}

const customLogger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports
});

logger.setLogger(customLogger);

Promtail (optional alternative)

  • Only needed if you must ship file logs (e.g., Nginx) or cannot push directly from the process.
  • SDK logs do not need Promtail when using winston-loki.
  • If you keep Promtail, ensure the scrape path matches files (see promtail/promtail-config.yml).

CI/CD notes

  • .github/workflows/deploy.yml passes LOKI_URL, LOKI_TLS_SKIP_VERIFY, LOKI_BASIC_AUTH into the container; src/app.js config injects the transport at startup.
  • Promtail steps are commented out. If you don’t need file-based ingestion, you can remove Promtail steps and the promtail/ directory entirely. Keep it only for Nginx/file logs.
  • Store LOKI_BASIC_AUTH in CI/CD secrets; never commit credentials.

Quick verification

  1. Start the app with LOKI_URL and LOKI_BASIC_AUTH set.
  2. Emit a test log: logger.info('Loki test', { component: 'SmokeTest' }).
  3. In Grafana Explore, query with {app="clienttestapi-api"} and confirm logs.

Performance Tuning

Session Storage Optimization

Production Recommendation: Use persistent storage (SQLite or Redis) instead of in-memory storage.

// ✅ Best for single-server deployments
const SQLiteStore = require('connect-sqlite3')(require('express-session'));
const sessionStore = new SQLiteStore({
  db: 'sessions.db',
  dir: './data',
  table: 'sessions',
  // Optimize for performance
  concurrentDB: true
});
setExpressSessionStore(sessionStore);

// ✅ Best for multi-server deployments
const RedisStore = require('connect-redis').default;
const redisClient = createClient({
  url: process.env.REDIS_URL,
  // Connection pooling
  socket: {
    keepAlive: true,
    reconnectStrategy: (retries) => Math.min(retries * 50, 500)
  }
});
await redisClient.connect();

const redisStore = new RedisStore({
  client: redisClient,
  prefix: 'rodit:sess:',
  ttl: 86400,
  // Disable touch for better performance if sessions don't need frequent updates
  disableTouch: false
});
setExpressSessionStore(redisStore);

Performance Impact:

  • In-memory: Fastest but sessions lost on restart
  • SQLite: Good performance, persistent, suitable for single server
  • Redis: Best for distributed systems, shared sessions across servers

Logging Optimization

Production Settings:

// Use appropriate log level
export LOG_LEVEL=info  # Avoid 'debug' or 'trace' in production

// Batch Loki logs for better performance
const lokiOptions = {
  host: process.env.LOKI_URL,
  basicAuth: process.env.LOKI_BASIC_AUTH,
  labels: { app: 'your-app', component: 'rodit-sdk' },
  json: true,
  batching: true,           // ✅ Enable batching
  interval: 5,              // Send every 5 seconds
  timeout: 30000,           // 30 second timeout
  gracefulShutdown: true    // Flush logs on shutdown
};

Log Level Impact:

  • error: Minimal overhead, production default
  • warn: Low overhead, recommended for production
  • info: Moderate overhead, good for production monitoring
  • debug: High overhead, development only
  • trace: Very high overhead, debugging only

Rate Limiting Configuration

Configure rate limiting based on your RODiT token metadata:

const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;

// Use RODiT token rate limits
const maxRequests = parseInt(metadata.max_requests);
const windowSeconds = parseInt(metadata.maxrq_window);

const rateLimiter = roditClient.getRateLimitMiddleware();
app.use(rateLimiter(maxRequests, windowSeconds));

// Example: 100 requests per 15 minutes
// maxRequests = 100
// windowSeconds = 900

Tuning Tips:

  • Set max_requests based on expected traffic patterns
  • Use shorter maxrq_window for burst protection
  • Monitor rate limit hits via performance metrics

JWT Token Duration

Optimize JWT duration for security vs. performance trade-off:

// In your RODiT token metadata:
{
  "jwt_duration": 3600  // 1 hour (default)
}

Recommendations:

  • Short duration (900-1800s): High security, more token refreshes
  • Medium duration (3600-7200s): Balanced, recommended for most applications
  • Long duration (>7200s): Fewer refreshes but lower security

Impact:

  • Shorter durations = More authentication overhead
  • Longer durations = Less overhead but longer exposure if token compromised

Database Connection Pooling

For CRUDA operations with SQLite:

const sqlite3 = require('sqlite3');
const { open } = require('sqlite');

const db = await open({
  filename: '/app/data/database.sqlite',
  driver: sqlite3.Database,
  // Enable WAL mode for better concurrent access
  mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE
});

// Enable WAL mode for better performance
await db.run('PRAGMA journal_mode = WAL');
await db.run('PRAGMA synchronous = NORMAL');
await db.run('PRAGMA cache_size = 10000');
await db.run('PRAGMA temp_store = MEMORY');

Request Context Middleware Optimization

Minimize middleware overhead:

// ✅ Efficient request context
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || ulid();
  req.startTime = Date.now();
  next();
});

// ❌ Avoid expensive operations in every request
app.use((req, res, next) => {
  // Don't do heavy computation here
  // Don't make external API calls here
  next();
});

Performance Metrics Collection

Use the performance service efficiently:

const performanceService = roditClient.getPerformanceService();

// Record metrics selectively
if (duration > 1000) {  // Only log slow requests
  performanceService.recordMetric('slow_request', duration, {
    method: req.method,
    path: req.path
  });
}

// Batch metric collection
const metrics = {
  request_count: 1,
  duration_ms: duration,
  status_code: res.statusCode
};
performanceService.recordBatch(metrics);

Memory Management

Session Cleanup:

// Automatic cleanup runs every 15 minutes by default
// Adjust if needed for your use case

const sessionManager = roditClient.getSessionManager();

// Manual cleanup for immediate memory reclamation
await sessionManager.runManualCleanup();

// Monitor session count
const activeCount = await sessionManager.getActiveSessionCount();
if (activeCount > 10000) {
  logger.warn('High session count detected', { activeCount });
}

Caching Strategies

Cache frequently accessed data:

// Cache RODiT configuration (changes rarely)
let cachedConfig = null;
let configCacheTime = 0;
const CONFIG_CACHE_TTL = 300000; // 5 minutes

async function getConfig() {
  const now = Date.now();
  if (cachedConfig && (now - configCacheTime) < CONFIG_CACHE_TTL) {
    return cachedConfig;
  }
  
  cachedConfig = await roditClient.getConfigOwnRodit();
  configCacheTime = now;
  return cachedConfig;
}

Monitoring Recommendations

Key Metrics to Track:

  1. Request duration (p50, p95, p99)
  2. Authentication success/failure rate
  3. Session count and growth rate
  4. Rate limit hits
  5. Error rates by endpoint
  6. Memory usage and session storage size

Grafana Dashboard Queries:

# Request duration
histogram_quantile(0.95, rate(request_duration_ms_bucket[5m]))

# Authentication failures
rate(authentication_errors_total[5m])

# Active sessions
rodit_active_sessions

# Rate limit hits
rate(rate_limit_exceeded_total[5m])

Webhooks

Overview

The SDK supports sending webhooks for important events. Webhook URLs are configured in the RODiT token metadata. Webhooks are configured in your RODiT token:

{
  "webhook_url": "https://webhook.example.com:3444",
  "webhook_cidr": "0.0.0.0/0"
}

Sending Webhooks

// Get webhook handler from client
const roditClient = req.app.locals.roditClient;

// Send webhook for an event
const webhookPayload = {
  event: 'comment_created',
  data: {
    id: comment.id,
    author: comment.author,
    timestamp: new Date().toISOString()
  },
  isError: false
};

try {
  const result = await roditClient.send_webhook(webhookPayload, req);
  
  if (result.success) {
    logger.info('Webhook sent successfully', {
      component: 'CRUDA',
      event: webhookPayload.event,
      requestId: req.requestId
    });
  }
} catch (error) {
  // Webhook failures don't crash the application
  logger.warn('Webhook delivery failed', {
    component: 'CRUDA',
    event: webhookPayload.event,
    error: error.message,
    requestId: req.requestId
  });
}

Webhook Error Handling

// Graceful webhook handling in CRUDA operations
const logAndSendWebhook = async (payload, req = null) => {
  try {
    const roditClient = req?.app?.locals?.roditClient;
    
    if (!roditClient) {
      logger.warn('RoditClient not available, skipping webhook', {
        component: 'CRUDA',
        event: payload?.event
      });
      return { success: false, error: 'RoditClient not available' };
    }
    
    return await roditClient.send_webhook(payload, req);
  } catch (error) {
    // Log but don't throw - webhook failures shouldn't crash the app
    logger.error('Webhook delivery failed', {
      component: 'CRUDA',
      event: payload?.event,
      error: error.message
    });
    return { success: false, error: error.message };
  }
};

Advanced Usage

Route Module Pattern

Create reusable route modules that access the shared RoditClient:

// routes/protected.js
const express = require('express');
const { logger } = require('@rodit/rodit-auth-be');
const router = express.Router();

// Middleware that uses the shared client
const authenticate = (req, res, next) => {
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authentication service unavailable' });
  }
  return client.authenticate(req, res, next);
};

const authorize = (req, res, next) => {
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authentication service unavailable' });
  }
  return client.authorize(req, res, next);
};

// Protected route with full authentication and authorization
router.get('/data', authenticate, authorize, async (req, res) => {
  const startTime = Date.now();
  
  try {
    // Your business logic here
    const data = await processUserData(req.user.id);
    
    logger.infoWithContext('Data retrieved successfully', {
      component: 'ProtectedRoutes',
      method: 'getData',
      userId: req.user.id,
      requestId: req.requestId,
      duration: Date.now() - startTime
    });
    
    res.json({ data, requestId: req.requestId });
  } catch (error) {
    logger.errorWithContext('Failed to retrieve data', {
      component: 'ProtectedRoutes',
      method: 'getData',
      userId: req.user.id,
      requestId: req.requestId,
      duration: Date.now() - startTime,
      error: error.message
    }, error);
    
    res.status(500).json({
      error: 'Internal server error',
      requestId: req.requestId
    });
  }
});

module.exports = router;

Portal Authentication (Server-to-Server)

For server-to-server authentication (e.g., minting client tokens):

// routes/signclient.js
const router = express.Router();

router.post('/signclient', authenticate, authorize, async (req, res) => {
  const { tobesignedValues, mintingfee, mintingfeeaccount } = req.body;
  const client = req.app.locals.roditClient;
  const logger = client.getLogger();
  
  try {
    // Validate requested permissions against server's permissions
    const configObject = await client.getConfigOwnRodit();
    const serverPermissions = JSON.parse(
      configObject.own_rodit.metadata.permissioned_routes || '{}'
    );
    
    const requestedPermissions = JSON.parse(
      tobesignedValues.permissioned_routes || '{}'
    );
    
    // Validate that all requested routes exist in server config
    // (Implementation details in actual code)
    
    // Authenticate to portal and mint client token
    const port = configObject.port || 8443;
    const result = await client.login_portal(configObject, port);
    
    if (result.error) {
      return res.status(500).json({
        error: 'Portal authentication failed',
        details: result.message,
        requestId: req.requestId
      });
    }
    
    // Sign the client token via portal
    const signedToken = await signPortalRodit(
      port,
      tobesignedValues,
      mintingfee,
      mintingfeeaccount,
      client
    );
    
    res.json({
      signedToken,
      requestId: req.requestId
    });
  } catch (error) {
    logger.errorWithContext('Client token minting failed', {
      component: 'SignClient',
      requestId: req.requestId,
      error: error.message
    }, error);
    
    res.status(500).json({
      error: 'Token minting failed',
      requestId: req.requestId
    });
  }
});

module.exports = router;

SignPortal URL Configuration

Overview

When performing server-to-server authentication with SignPortal (e.g., minting client tokens), the SDK automatically constructs the SignPortal URL from the serviceprovider_id field in your RODiT token metadata.

Smart Contract Name Format

The SignPortal URL is derived from the smart contract component (sc=) in your serviceprovider_id. The SDK supports two formats:

Standard Format (3+ components):

sc=<number>-<domain>-<tld>.near

Example:

serviceprovider_id: "bc=near.org;sc=10975-discernible-org.near;id=..."

Parsing:

  • Split by .: ["10975-discernible-org", "near"]
  • Take first part: 10975-discernible-org
  • Split by -: ["10975", "discernible", "org"]
  • Extract domain: discernible (index 1)
  • Extract TLD: org (index 2)
  • Result: https://signportal.discernible.org:8443

Alternative Format (2 components):

sc=<domain>-<tld>.near

Example:

serviceprovider_id: "bc=near.org;sc=roditcorp-com.near;id=..."

Parsing:

  • Split by .: ["roditcorp-com", "near"]
  • Take first part: roditcorp-com
  • Split by -: ["roditcorp", "com"]
  • Extract domain: roditcorp (index 0)
  • Extract TLD: com (index 1)
  • Result: https://signportal.roditcorp.com:8443

serviceprovider_id Structure

The complete serviceprovider_id format:

bc=<blockchain>;sc=<smart-contract>;id=<identifier>[;id=<additional-id>]

Components:

  • bc= - Blockchain identifier (e.g., near.org)
  • sc= - Smart contract name (used to construct SignPortal URL)
  • id= - One or more identifier components

Example:

{
  "serviceprovider_id": "bc=near.org;sc=roditcorp-com.near;id=01K8QECHMKFVNWQ54PJ2W2GMA7;id=01K8QECHMM1214VMDHSH7JM6H8"
}

URL Construction Method

The SDK uses roditClient.getPortalUrl(serviceProviderId, port) to construct the SignPortal URL:

const client = req.app.locals.roditClient;
const configObject = await client.getConfigOwnRodit();
const serviceProviderId = configObject.own_rodit.metadata.serviceprovider_id;
const portalPort = 8443;

// Automatically constructs: https://signportal.<domain>.<tld>:8443
const portalUrl = client.getPortalUrl(serviceProviderId, portalPort);

Troubleshooting

Error: "Failed to parse URL from " (empty string)

  • Cause: serviceprovider_id is empty or undefined in your RODiT configuration
  • Solution: Verify your RODiT token has a valid serviceprovider_id field
  • Check: Run ./infra/roditwallet.sh <private-key> <token-id> to view token metadata

Error: "Invalid serviceprovider_id format: missing sc= component"

  • Cause: The serviceprovider_id doesn't contain an sc= component
  • Solution: Ensure your token includes the smart contract identifier
  • Format: bc=near.org;sc=<contract-name>.near;id=...

Error: "Invalid domain format in smart contract"

  • Cause: Smart contract name has fewer than 2 components when split by -
  • Solution: Use format <domain>-<tld> or <number>-<domain>-<tld>
  • Valid: roditcorp-com.near, 10975-discernible-org.near
  • Invalid: roditcorp.near, mycontract.near

Configuration Verification

To verify your SignPortal URL configuration:

const client = req.app.locals.roditClient;
const logger = client.getLogger();

try {
  const configObject = await client.getConfigOwnRodit();
  const serviceProviderId = configObject.own_rodit.metadata.serviceprovider_id;
  
  logger.info('RODiT Configuration', {
    component: 'SignPortal',
    serviceProviderId,
    hasServiceProviderId: !!serviceProviderId
  });
  
  if (serviceProviderId) {
    const portalUrl = client.getPortalUrl(serviceProviderId, 8443);
    logger.info('SignPortal URL constructed', {
      component: 'SignPortal',
      portalUrl
    });
  }
} catch (error) {
  logger.error('SignPortal URL construction failed', {
    component: 'SignPortal',
    error: error.message
  });
}

CRUDA Operations Example

Complete CRUD implementation with authentication, authorization, webhooks, and performance tracking:

// protected/cruda.js
const express = require('express');
const router = express.Router();
const { RoditClient } = require('@rodit/rodit-auth-be');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
const { ulid } = require('ulid');

const sdkClient = new RoditClient();
const logger = sdkClient.getLogger();

let db;

// Initialize database
const initializeDatabase = async () => {
  db = await open({
    filename: '/app/data/database.sqlite',
    driver: sqlite3.Database
  });
  
  await db.run(`CREATE TABLE IF NOT EXISTS comments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    comment TEXT NOT NULL,
    author TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )`);
};

// Webhook helper
const logAndSendWebhook = async (payload, req) => {
  try {
    const roditClient = req?.app?.locals?.roditClient;
    if (!roditClient) return { success: false };
    
    return await roditClient.send_webhook(payload, req);
  } catch (error) {
    logger.error('Webhook failed', { error: error.message });
    return { success: false, error: error.message };
  }
};

// CREATE
router.post('/create', async (req, res) => {
  const { comment, author } = req.body;
  const requestId = req.requestId || ulid();
  
  try {
    const result = await db.run(
      'INSERT INTO comments (comment, author) VALUES (?, ?)',
      [comment, author || req.user.roditId]
    );
    
    // Send webhook
    await logAndSendWebhook({
      event: 'comment_created',
      data: { id: result.lastID, comment, author },
      isError: false
    }, req);
    
    res.json({ id: result.lastID, requestId });
  } catch (error) {
    logger.errorWithContext('Create failed', {
      component: 'CRUDA',
      error: error.message,
      requestId
    }, error);
    
    res.status(500).json({ error: 'Create failed', requestId });
  }
});

// LIST
router.post('/list', async (req, res) => {
  try {
    const records = await db.all(
      'SELECT * FROM comments ORDER BY created_at DESC'
    );
    
    res.json({ records, requestId: req.requestId });
  } catch (error) {
    res.status(500).json({ error: 'List failed', requestId: req.requestId });
  }
});

// READ
router.post('/read', async (req, res) => {
  const { id } = req.body;
  
  try {
    const record = await db.get('SELECT * FROM comments WHERE id = ?', [id]);
    
    if (!record) {
      return res.status(404).json({ error: 'Not found', requestId: req.requestId });
    }
    
    res.json({ record, requestId: req.requestId });
  } catch (error) {
    res.status(500).json({ error: 'Read failed', requestId: req.requestId });
  }
});

// UPDATE
router.post('/update', async (req, res) => {
  const { id, comment } = req.body;
  
  try {
    await db.run(
      'UPDATE comments SET comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
      [comment, id]
    );
    
    await logAndSendWebhook({
      event: 'comment_updated',
      data: { id, comment },
      isError: false
    }, req);
    
    res.json({ success: true, requestId: req.requestId });
  } catch (error) {
    res.status(500).json({ error: 'Update failed', requestId: req.requestId });
  }
});

// DELETE
router.post('/destroy', async (req, res) => {
  const { id } = req.body;
  
  try {
    await db.run('DELETE FROM comments WHERE id = ?', [id]);
    
    await logAndSendWebhook({
      event: 'comment_deleted',
      data: { id },
      isError: false
    }, req);
    
    res.json({ success: true, requestId: req.requestId });
  } catch (error) {
    res.status(500).json({ error: 'Delete failed', requestId: req.requestId });
  }
});

// Export initialization function
module.exports = router;
module.exports.initializeDatabase = initializeDatabase;

API Reference

RoditClient Class

The main client class for all RODiT operations.

Static Methods

RoditClient.create(role)

Create and initialize a RODiT client in one step.

const client = await RoditClient.create('server');  // For server applications
const client = await RoditClient.create('client');  // For client applications
const client = await RoditClient.create('portal');  // For portal authentication

Parameters:

  • role (string): Client role - 'server', 'client', or 'portal'

Returns: Promise<RoditClient> - Fully initialized client instance

Throws: Error if initialization fails (e.g., missing credentials, Vault connection failure)

RoditClient.createTestInstance()

Create an independent test instance with isolated state for testing purposes.

const testClient1 = await RoditClient.createTestInstance();
const testClient2 = await RoditClient.createTestInstance();

// Each client has independent stateManager for concurrent testing
console.log(testClient1.stateManager.instanceId !== testClient2.stateManager.instanceId); // true

Returns: Promise<RoditClient> - Test client instance with testMode: true

Use Case: Testing concurrent sessions, session isolation, or multiple client scenarios

Instance Methods

authenticate(req, res, next)

Express middleware for authenticating API requests. Validates JWT tokens and populates req.user.

const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
app.use('/api/protected', authenticate, handler);

Validates:

  • JWT signature
  • JWT expiration
  • Session exists and is active
  • Token not invalidated

Populates: req.user with decoded JWT claims

authorize(req, res, next)

Express middleware for validating route permissions. Must be used after authenticate.

const authorize = (req, res, next) => roditClient.authorize(req, res, next);
app.use('/api/admin', authenticate, authorize, handler);

Validates: User has permission for the requested route and HTTP method

login_client(req, res)

Handle Express login requests from clients. Validates RODiT credentials and issues JWT token.

app.post('/api/login', (req, res) => roditClient.login_client(req, res));

Request Body:

{
  roditid: string,
  timestamp: number,
  roditid_base64url_signature: string
}

Response:

{
  message: 'Login successful',
  token: 'eyJhbGci...',
  requestId: '01HQXYZ...'
}
logout_client(req, res)

Handle Express logout requests. Closes session and invalidates JWT token.

app.post('/api/logout', authenticate, (req, res) => {
  return roditClient.logout_client(req, res);
});

Response:

{
  message: 'Logout successful',
  terminationToken: 'eyJhbGci...',  // Short-lived token
  requestId: '01HQXYZ...'
}
login_portal(configObject, port)

Authenticate to RODiT portal for server-to-server operations.

const configObject = await roditClient.getConfigOwnRodit();
const result = await roditClient.login_portal(configObject, 8443);

Returns: Promise<Object> - Portal authentication result

login_server(options)

Authenticate this server to another RODiT server.

const result = await roditClient.login_server({
  serverUrl: 'https://api.example.com',
  credentials: {...}
});

Returns: Promise<Object> - Authentication result with token

logout_server()

Logout from server-to-server session.

const result = await roditClient.logout_server();

Returns: Promise<Object> - Logout result with session closure status

getConfigOwnRodit()

Get the complete RODiT configuration including token metadata.

const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
const tokenId = configObject.own_rodit.token_id;

Returns: Promise<Object> - Complete RODiT configuration

Structure:

{
  own_rodit: {
    token_id: string,
    metadata: {
      jwt_duration: number,
      max_requests: string,
      maxrq_window: string,
      permissioned_routes: string,  // JSON string
      subjectuniqueidentifier_url: string,
      webhook_url: string,
      // ... other metadata fields
    }
  },
  port: number
}
isOperationPermitted(method, path)

Check if an operation is permitted based on token permissions.

const hasPermission = roditClient.isOperationPermitted('POST', '/api/admin/users');
if (!hasPermission) {
  return res.status(403).json({ error: 'Forbidden' });
}

Parameters:

  • method (string): HTTP method
  • path (string): API path

Returns: boolean

getStateManager()

Get the authentication state manager.

const stateManager = roditClient.getStateManager();

Returns: AuthStateManager instance

getRoditManager()

Get the RODiT manager for credential operations.

const roditManager = roditClient.getRoditManager();
const credentials = await roditManager.getCredentials('server');

Returns: RoditManager instance

getSessionManager()

Get the session manager.

const sessionManager = roditClient.getSessionManager();
const activeCount = await sessionManager.getActiveSessionCount();

Returns: SessionManager instance

getLogger()

Get the logger instance.

const logger = roditClient.getLogger();
logger.info('Message', { component: 'MyComponent' });

Returns: Logger instance

getLoggingMiddleware()

Get the logging middleware.

const loggingmw = roditClient.getLoggingMiddleware();
app.use(loggingmw);

Returns: Express middleware function

getRateLimitMiddleware()

Get the rate limiting middleware factory.

const ratelimitmw = roditClient.getRateLimitMiddleware();
const limiter = ratelimitmw(100, 900);  // 100 requests per 15 minutes
app.use(limiter);

Parameters:

  • maxRequests (number): Maximum requests allowed
  • windowSeconds (number): Time window in seconds

Returns: Express middleware function

getPerformanceService()

Get the performance tracking service.

const performanceService = roditClient.getPerformanceService();
performanceService.recordRequest(req);
performanceService.recordMetric('operation_duration', 150, { operation: 'db_query' });

Returns: PerformanceService instance

getConfig()

Get the configuration service.

const config = roditClient.getConfig();
const port = config.get('SERVERPORT', 3000);
const dbPath = config.get('API_DEFAULT_OPTIONS.DB_PATH');

Returns: Config instance

getWebhookHandler()

Get the webhook handler.

const webhookHandler = roditClient.getWebhookHandler();

Returns: WebhookHandler instance

send_webhook(payload, req)

Send a webhook notification.

const result = await roditClient.send_webhook({
  event: 'user_action',
  data: { userId: '123', action: 'login' },
  isError: false
}, req);

Parameters:

  • payload (Object): Webhook payload
    • event (string): Event name
    • data (Object): Event data
    • isError (boolean): Whether this is an error event
  • req (Object): Express request object (optional)

Returns: Promise<Object> - { success: boolean, ... }

Exported Components

The SDK exports these components for direct use:

const {
  RoditClient,           // Main client class
  logger,                // Logger instance
  stateManager,          // Authentication state manager
  roditManager,          // RODiT credential manager
  sessionManager,        // Session manager
  setExpressSessionStore, // Configure session storage
  configureStorageFromConfig, // Auto-configure storage
  createExpressSessionMiddleware, // Create session middleware
  InMemorySessionStorage, // Default storage class
  SessionManager,        // SessionManager facade
  blockchainService,     // Blockchain operations
  utils,                 // Utility functions
  config,                // Configuration service
  performanceService,    // Performance tracking
  authenticate_apicall,  // Authentication middleware
  login_client,          // Login handler
  logout_client,         // Logout handler
  login_client_withnep413, // NEP-413 login
  login_portal,          // Portal authentication
  login_server,          // Server authentication
  logout_server,         // Server logout
  validate_jwt_token_be, // JWT validation
  generate_jwt_token,    // JWT generation
  validatepermissions,   // Permission middleware
  webhookHandler,        // Webhook handler
  versioningMiddleware,  // API versioning
  loggingmw,             // Logging middleware
  ratelimitmw,           // Rate limiting middleware
  versionManager,        // Version manager
  VersionManager         // Version manager class
} = require('@rodit/rodit-auth-be');

RODiT Token Metadata Fields

When you call roditClient.getConfigOwnRodit(), you get access to these metadata fields:

Field Type Description
token_id string Unique RODiT token identifier
allowed_cidr string Permitted IP address ranges (CIDR format)
allowed_iso3166list string Geographic restrictions (JSON string)
jwt_duration number JWT token lifetime in seconds
max_requests string Rate limit - maximum requests per window
maxrq_window string Rate limit - time window in seconds
not_before string Token validity start date (ISO format)
not_after string Token validity end date (ISO format)
openapijson_url string OpenAPI specification URL
permissioned_routes string Allowed API routes and methods (JSON string)
serviceprovider_id string Blockchain contract and service provider info
serviceprovider_signature string Cryptographic signature for verification
subjectuniqueidentifier_url string Primary API service endpoint
userselected_dn string User-selected display name
webhook_cidr string Allowed IP ranges for webhooks
webhook_url string Webhook endpoint URL

Best Practices

1. Single Client Initialization

Always initialize the RoditClient once in your main application file:

// ✅ Good - Single initialization
async function startServer() {
  const roditClient = await RoditClient.create('server');
  app.locals.roditClient = roditClient;
  
  // Mount protected routes AFTER client initialization
  const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
  const authorize = (req, res, next) => roditClient.authorize(req, res, next);
  
  app.use('/api/echo', authenticate, echoRoutes);
  app.use('/api/cruda', authenticate, authorize, crudaRoutes);
  
  // ... rest of server setup
}

// ❌ Bad - Multiple initializations
app.get('/route1', async (req, res) => {
  const client = await RoditClient.create('server'); // Don't do this
});

2. Use App.locals for Shared Access

Store the client in app.locals for access across all routes:

// ✅ Good - Shared instance via app.locals
const router = express.Router();

router.get('/data', (req, res) => {
  const client = req.app.locals.roditClient;
  const logger = client.getLogger();
  
  logger.info('Processing request', {
    component: 'DataRoute',
    userId: req.user?.id,
    requestId: req.requestId
  });
  
  res.json({ data: 'example' });
});

// ❌ Bad - Creating new instances in routes
const { RoditClient } = require('@rodit/rodit-auth-be');
const client = new RoditClient(); // Don't do this in routes

3. Proper Error Handling

Always wrap SDK operations in try-catch blocks and include request context:

// ✅ Good - Comprehensive error handling
app.get('/api/data', authenticate, async (req, res) => {
  const startTime = Date.now();
  const client = req.app.locals.roditClient;
  const logger = client.getLogger();
  
  try {
    const data = await processData(req.user.id);
    
    logger.infoWithContext('Request successful', {
      component: 'API',
      method: 'getData',
      userId: req.user.id,
      requestId: req.requestId,
      duration: Date.now() - startTime
    });
    
    res.json({ data, requestId: req.requestId });
  } catch (error) {
    logger.errorWithContext('Request failed', {
      component: 'API',
      method: 'getData',
      userId: req.user.id,
      requestId: req.requestId,
      duration: Date.now() - startTime,
      error: error.message
    }, error);
    
    res.status(500).json({
      error: 'Internal server error',
      requestId: req.requestId
    });
  }
});

4. Structured Logging

Use consistent logging patterns with context:

// ✅ Good - Structured logging with context
const logger = req.app.locals.roditClient.getLogger();

logger.infoWithContext('User action completed', {
  component: 'UserService',
  action: 'updateProfile',
  userId: user.id,
  requestId: req.requestId,
  duration: Date.now() - startTime,
  changes: Object.keys(updates)
});

// For errors, pass the error object
logger.errorWithContext('Operation failed', {
  component: 'UserService',
  action: 'updateProfile',
  userId: user.id,
  requestId: req.requestId,
  error: error.message
}, error);

// ❌ Bad - Unstructured logging
console.log('User updated profile'); // Don't do this

5. Environment-Specific Configuration

Use environment variables for sensitive and environment-specific values:

// ✅ Good - Environment-aware configuration
const config = roditClient.getConfig();
const logLevel = config.get('LOG_LEVEL', 'info');
const isProduction = ['info', 'warn', 'error'].includes(logLevel);

// Production should use vault credentials
if (isProduction && process.env.RODIT_NEAR_CREDENTIALS_SOURCE !== 'vault') {
  logger.warn('Production environment should use vault credentials', {
    component: 'Configuration',
    environment: 'production',
    credentialsSource: process.env.RODIT_NEAR_CREDENTIALS_SOURCE || 'not-set'
  });
}

// Configure session storage before initializing client
if (isProduction) {
  const SQLiteStore = require('connect-sqlite3')(require('express-session'));
  const sessionStore = new SQLiteStore({
    db: 'sessions.db',
    dir: config.get('API_DEFAULT_OPTIONS.DB_PATH', './data')
  });
  setExpressSessionStore(sessionStore);
}

6. Graceful Shutdown

Implement proper shutdown handling:

// ✅ Good - Graceful shutdown
const shutdown = async (signal) => {
  const logger = roditClient.getLogger();
  
  logger.info('Shutting down gracefully', {
    component: 'AppLifecycle',
    signal: signal || 'unknown',
    time: new Date().toISOString()
  });
  
  if (server) {
    server.close(async () => {
      logger.info('HTTP server closed');
      
      // Close database connections
      if (db && typeof db.close === 'function') {
        await db.close();
        logger.info('Database connections closed');
      }
      
      // Close session store
      if (sessionStore && typeof sessionStore.close === 'function') {
        await new Promise((resolve) => sessionStore.close(resolve));
        logger.info('Session store closed');
      }
      
      process.exit(0);
    });
    
    // Force shutdown after timeout
    setTimeout(() => {
      logger.error('Forced shutdown after timeout');
      process.exit(1);
    }, 10000);
  }
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

7. Request Context and Performance Tracking

Always include request context and track performance:

// ✅ Good - Request context and performance tracking
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || ulid();
  req.startTime = Date.now();
  next();
});

// Performance monitoring
app.use((req, res, next) => {
  const performanceService = roditClient.getPerformanceService();
  
  if (performanceService) {
    performanceService.recordRequest(req);
  }
  
  res.on('finish', () => {
    const duration = Date.now() - req.startTime;
    
    if (performanceService) {
      performanceService.recordMetric('request_duration_ms', duration, {
        method: req.method,
        path: req.path,
        status: res.statusCode
      });
      
      if (res.statusCode >= 400) {
        performanceService.recordMetric('error_count', 1, {
          method: req.method,
          path: req.path,
          status: res.statusCode
        });
      }
    }
  });
  
  next();
});

8. Login Endpoint Protection

CRITICAL: Never protect the login endpoint with authentication middleware:

// ✅ Good - Login endpoint without authentication
app.post('/api/login', (req, res) => {
  req.logAction = 'login-attempt';
  return roditClient.login_client(req, res);
});

// ❌ Bad - Login endpoint with authentication (creates circular dependency)
app.post('/api/login', authenticate, (req, res) => {  // DON'T DO THIS
  return roditClient.login_client(req, res);
});

// ✅ Good - Logout endpoint with authentication
app.post('/api/logout', authenticate, (req, res) => {
  req.logAction = 'logout-attempt';
  return roditClient.logout_client(req, res);
});

9. Route Mounting Order

Mount protected routes AFTER client initialization:

// ✅ Good - Correct order
async function startServer() {
  // 1. Configure session storage
  setExpressSessionStore(sessionStore);
  
  // 2. Initialize client
  roditClient = await RoditClient.create('server');
  app.locals.roditClient = roditClient;
  
  // 3. Create middleware
  const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
  const authorize = (req, res, next) => roditClient.authorize(req, res, next);
  
  // 4. Mount public routes
  app.post('/api/login', loginRoute);
  
  // 5. Mount protected routes
  app.use('/api/echo', authenticate, echoRoutes);
  app.use('/api/cruda', authenticate, authorize, crudaRoutes);
  app.post('/api/logout', authenticate, logoutRoute);
  
  // 6. Start server
  app.listen(port);
}

// ❌ Bad - Routes mounted before client initialization
app.use('/api/echo', authenticate, echoRoutes);  // authenticate is undefined!
roditClient = await RoditClient.create('server');

Security Considerations

1. Credential Storage

Production: Always use HashiCorp Vault for credential storage.

# ✅ Production - Vault-based credentials
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id

# ❌ Never commit credentials to version control
# ❌ Never use file-based credentials in production

Development: Use file-based credentials with proper permissions.

# Development only
export RODIT_NEAR_CREDENTIALS_SOURCE=file
export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json

# Set restrictive file permissions
chmod 600 ./credentials/rodit-credentials.json

2. JWT Token Security

Token Transmission:

  • ✅ Always use HTTPS in production
  • ✅ Send tokens in Authorization: Bearer <token> header
  • ❌ Never send tokens in URL query parameters
  • ❌ Never log JWT tokens in production

Token Duration:

// Balance security vs. usability
{
  "jwt_duration": 3600  // 1 hour recommended for most applications
}

Token Validation:

// SDK automatically validates:
// 1. JWT signature (cryptographic verification)
// 2. JWT expiration (time-based validation)
// 3. Session existence (state validation)
// 4. Session status (not closed/invalidated)

3. Session Management Security

Session Storage:

// ✅ Production - Use persistent storage with encryption
const sessionStore = new SQLiteStore({
  db: 'sessions.db',
  dir: './data',
  table: 'sessions'
});

// Ensure database file has restrictive permissions
// chmod 600 ./data/sessions.db

Session Invalidation:

// Always invalidate sessions on logout
app.post('/api/logout', authenticate, async (req, res) => {
  await roditClient.logout_client(req, res);
  // Session is now closed and token is invalidated
});

// Automatic cleanup of expired sessions
const sessionManager = roditClient.getSessionManager();
await sessionManager.runManualCleanup();

4. Rate Limiting

Implement rate limiting to prevent abuse:

const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;

const maxRequests = parseInt(metadata.max_requests);
const windowSeconds = parseInt(metadata.maxrq_window);

const rateLimiter = roditClient.getRateLimitMiddleware();
app.use(rateLimiter(maxRequests, windowSeconds));

Additional Protection:

// Add IP-based rate limiting for login endpoint
const loginRateLimiter = ratelimitmw(5, 900); // 5 attempts per 15 minutes
app.post('/api/login', loginRateLimiter, (req, res) => {
  return roditClient.login_client(req, res);
});

5. Input Validation

Always validate input data:

// ✅ Validate before processing
app.post('/api/cruda/create', authenticate, authorize, async (req, res) => {
  const { comment, author } = req.body;
  
  // Validate input
  if (!comment || typeof comment !== 'string') {
    return res.status(400).json({ error: 'Invalid comment' });
  }
  
  if (comment.length > 1000) {
    return res.status(400).json({ error: 'Comment too long' });
  }
  
  // Sanitize input
  const sanitizedComment = comment.trim();
  
  // Process request
  // ...
});

6. Error Handling

Never expose sensitive information in errors:

// ✅ Production - Generic error messages
const isProduction = process.env.NODE_ENV === 'production';

try {
  // Operation
} catch (error) {
  logger.errorWithContext('Operation failed', {
    component: 'API',
    error: error.message,
    stack: error.stack,
    requestId: req.requestId
  }, error);
  
  res.status(500).json({
    error: isProduction ? 'Internal server error' : error.message,
    requestId: req.requestId
  });
}

7. HTTPS/TLS Configuration

Always use HTTPS in production:

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('/path/to/private-key.pem'),
  cert: fs.readFileSync('/path/to/certificate.pem'),
  // Use strong TLS settings
  minVersion: 'TLSv1.2',
  ciphers: 'HIGH:!aNULL:!MD5'
};

const server = https.createServer(options, app);
server.listen(443);

8. CORS Configuration

Configure CORS restrictively:

const cors = require('cors');

// ✅ Production - Whitelist specific origins
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com'
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// ❌ Never use in production
app.use(cors({ origin: '*' }));

9. Logging Security

Avoid logging sensitive data:

// ✅ Safe logging
logger.info('User authenticated', {
  component: 'Authentication',
  userId: user.id,
  requestId: req.requestId
});

// ❌ Never log sensitive data
logger.info('Authentication attempt', {
  password: req.body.password,  // DON'T DO THIS
  privateKey: credentials.private_key,  // DON'T DO THIS
  jwtToken: token  // DON'T DO THIS
});

10. Webhook Security

Validate webhook endpoints:

// Webhooks are sent to URLs configured in RODiT token
// Ensure webhook_url uses HTTPS
{
  "webhook_url": "https://webhook.example.com:3444",  // ✅ HTTPS
  "webhook_cidr": "10.0.0.0/8"  // Restrict to specific IP ranges
}

// Implement webhook signature verification
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

11. Permission Validation

Always validate permissions before operations:

// ✅ Check permissions explicitly
app.post('/api/admin/users', authenticate, authorize, async (req, res) => {
  const client = req.app.locals.roditClient;
  
  // Double-check permission
  if (!client.isOperationPermitted('POST', '/api/admin/users')) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  // Proceed with operation
});

12. Security Headers

Add security headers:

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// Additional security headers
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  next();
});

13. Audit Logging

Log security-relevant events:

const logger = roditClient.getLogger();

// Log authentication events
logger.info('Login attempt', {
  component: 'Security',
  event: 'login_attempt',
  userId: req.body.roditid,
  ip: req.ip,
  userAgent: req.headers['user-agent'],
  requestId: req.requestId
});

// Log authorization failures
logger.warn('Authorization failed', {
  component: 'Security',
  event: 'authorization_failure',
  userId: req.user.id,
  path: req.path,
  method: req.method,
  ip: req.ip,
  requestId: req.requestId
});

// Log suspicious activity
if (failedAttempts > 5) {
  logger.error('Multiple failed login attempts', {
    component: 'Security',
    event: 'brute_force_attempt',
    ip: req.ip,
    attempts: failedAttempts,
    requestId: req.requestId
  });
}

14. Environment Separation

Maintain strict environment separation:

# Development
export NODE_ENV=development
export LOG_LEVEL=debug
export RODIT_NEAR_CREDENTIALS_SOURCE=file

# Staging
export NODE_ENV=production
export LOG_LEVEL=info
export RODIT_NEAR_CREDENTIALS_SOURCE=vault

# Production
export NODE_ENV=production
export LOG_LEVEL=warn
export RODIT_NEAR_CREDENTIALS_SOURCE=vault

Security Checklist

Before deploying to production:

  • All credentials stored in Vault
  • HTTPS/TLS enabled with strong ciphers
  • JWT duration set appropriately (≤ 2 hours)
  • Rate limiting configured
  • CORS configured with whitelist
  • Security headers enabled (helmet)
  • Input validation on all endpoints
  • Error messages don't expose sensitive data
  • Logging doesn't include sensitive data
  • Session storage uses persistent backend
  • Automatic session cleanup enabled
  • Audit logging for security events
  • File permissions restrictive (600 for sensitive files)
  • Environment variables properly configured
  • NODE_ENV set to 'production'

Troubleshooting

Common Issues

1. Authentication Middleware Errors

Problem: roditClient.authenticate is not a function or Cannot read properties of undefined

Solution: Ensure client is initialized and stored in app.locals:

// ✅ Correct - Check client availability
const authenticate = (req, res, next) => {
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ error: 'Authentication service unavailable' });
  }
  return client.authenticate(req, res, next);
};

// ❌ Wrong - Direct access without checking
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
// This fails if roditClient is not initialized yet

2. Configuration Not Found

Problem: Failed to initialize RODiT configuration

Solutions:

# Check environment variables
echo $RODIT_NEAR_CREDENTIALS_SOURCE  # Should be 'vault' or 'file'
echo $VAULT_ENDPOINT
echo $NEAR_CONTRACT_ID
echo $SERVICE_NAME

# For vault-based credentials
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id

# For file-based credentials (development)
export RODIT_NEAR_CREDENTIALS_SOURCE=file
export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json

Verify configuration:

const config = roditClient.getConfig();
console.log('NEAR_CONTRACT_ID:', config.get('NEAR_CONTRACT_ID'));
console.log('SERVICE_NAME:', config.get('SERVICE_NAME'));

3. Missing App.locals Client

Problem: RoditClient not available in app.locals or Cannot read properties of undefined (reading 'roditClient')

Solution: Ensure client is stored during initialization:

async function startServer() {
  try {
    // Initialize client
    const roditClient = await RoditClient.create('server');
    
    // Store in app.locals BEFORE mounting routes
    app.locals.roditClient = roditClient;
    
    // Verify it's stored
    if (!app.locals.roditClient) {
      throw new Error('Failed to store roditClient in app.locals');
    }
    
    // Now mount routes
    const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
    app.use('/api/protected', authenticate, protectedRoutes);
    
    app.listen(port);
  } catch (error) {
    console.error('Server initialization failed:', error);
    process.exit(1);
  }
}

4. Permission Denied Errors

Problem: Routes return 403 Forbidden

Debug steps:

// Check token permissions
const configObject = await roditClient.getConfigOwnRodit();
const permissionedRoutes = JSON.parse(
  configObject.own_rodit.metadata.permissioned_routes || '{}'
);
console.log('Configured permissions:', permissionedRoutes);

// Check specific operation
const hasPermission = roditClient.isOperationPermitted('POST', '/api/cruda/create');
console.log('Has permission:', hasPermission);

// Verify route path matches exactly
console.log('Requested path:', req.path);  // Must match permission key exactly

Common issues:

  • Route path doesn't match permission key exactly (e.g., /api/cruda/create vs /cruda/create)
  • HTTP method not allowed in permission configuration
  • Permission format incorrect (should be "+0" for all methods)
  • Client token has different permissions than server token

5. Session Not Found Errors

Problem: 401 Unauthorized - session_not_found

Cause: JWT token contains session ID that doesn't exist in session storage

Solutions:

// Verify session storage is configured
const sessionManager = roditClient.getSessionManager();
const storageInfo = await sessionManager.getStorageInfo();
console.log('Storage type:', storageInfo.storageType);
console.log('Active sessions:', storageInfo.sessionCount);

// Check if token is invalidated
const isInvalidated = await sessionManager.isTokenInvalidated(jwtToken);
console.log('Token invalidated:', isInvalidated);

// Enumerate sessions via storage for debugging
const allSessions = await sessionManager.storage.getAll();
console.log('Active sessions:', allSessions.filter(s => s.status === 'active').length);

Common causes:

  • Server restarted with in-memory storage (sessions lost)
  • Session expired
  • Token was invalidated by logout
  • Session storage not configured properly

Solution: Use persistent storage (SQLite or Redis) for production

6. Logging Issues

Problem: Logs not appearing in Loki or console

Solutions:

# Check logging configuration
export LOG_LEVEL=debug  # Enable debug logging
export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:password
// Test logger directly
const logger = roditClient.getLogger();
logger.info('Test message', { component: 'Test' });
logger.error('Test error', { component: 'Test' });

// Check if Loki transport is configured
const transports = logger.transports;
console.log('Logger transports:', transports.map(t => t.name));

Debug Mode

Enable debug logging for troubleshooting:

export LOG_LEVEL=debug  # Use 'debug' or 'trace' for development mode

This will provide detailed information about:

  • Authentication flows and token validation
  • Configuration loading from Vault/files
  • Permission checks and route matching
  • Session creation and validation
  • Network requests to portal/blockchain
  • Internal SDK operations
  • Request/response details

Example debug output:

const logger = roditClient.getLogger();

// Enable debug logging programmatically
logger.level = 'debug';

// Debug authentication
logger.debug('Authenticating request', {
  component: 'Authentication',
  hasAuthHeader: !!req.headers.authorization,
  path: req.path,
  method: req.method
});

Health Checks

Implement comprehensive health check endpoints:

app.get('/health', async (req, res) => {
  try {
    const client = req.app.locals.roditClient;
    if (!client) {
      return res.status(503).json({ 
        status: 'error', 
        message: 'RoditClient not available' 
      });
    }
    
    const configObject = await client.getConfigOwnRodit();
    const sessionManager = client.getSessionManager();
    const performanceService = client.getPerformanceService();
    
    const health = {
      status: 'healthy',
      timestamp: new Date().toISOString(),
      logLevel: config.get('LOG_LEVEL', 'info'),
      components: {
        roditClient: !!client,
        configuration: !!(configObject && configObject.own_rodit),
        sessionManager: !!sessionManager,
        performanceService: !!performanceService
      },
      metrics: {
        activeSessions: await sessionManager.getActiveSessionCount(),
        totalRequests: performanceService.getRequestCount(),
        errorCount: performanceService.getErrorCount()
      },
      roditToken: {
        tokenId: configObject?.own_rodit?.token_id,
        apiUrl: configObject?.own_rodit?.metadata?.subjectuniqueidentifier_url,
        jwtDuration: configObject?.own_rodit?.metadata?.jwt_duration
      }
    };
    
    res.json(health);
  } catch (error) {
    res.status(503).json({
      status: 'error',
      message: error.message,
      timestamp: new Date().toISOString()
    });
  }
});

// Readiness check (for Kubernetes)
app.get('/ready', async (req, res) => {
  const client = req.app.locals.roditClient;
  if (!client) {
    return res.status(503).json({ ready: false });
  }
  
  try {
    const configObject = await client.getConfigOwnRodit();
    const ready = !!(configObject && configObject.own_rodit);
    
    res.status(ready ? 200 : 503).json({ ready });
  } catch (error) {
    res.status(503).json({ ready: false, error: error.message });
  }
});

// Liveness check (for Kubernetes)
app.get('/live', (req, res) => {
  res.json({ alive: true });
});

Support

For additional support:

  1. Check the debug logs with LOG_LEVEL=debug
  2. Verify your RODiT token configuration
  3. Test with the health check endpoint
  4. Review the authentication flow in the logs
  5. Ensure all required environment variables are set

Version History

Version 4.0.2 (Current)

Features:

  • ✅ Session storage configuration exports (setExpressSessionStore, configureStorageFromConfig, etc.)
  • ✅ Test instance support with RoditClient.createTestInstance() for concurrent testing
  • ✅ Comprehensive session management with pluggable storage backends
  • ✅ Direct Loki logging integration via winston-loki
  • ✅ Performance tracking and metrics collection
  • ✅ Webhook support with graceful error handling
  • ✅ Rate limiting middleware with RODiT token configuration
  • ✅ Authorization middleware with route-based permissions
  • ✅ Graceful shutdown support

Improvements:

  • Enhanced error messages with request context
  • Better logging with structured context
  • Improved session cleanup and token invalidation
  • Support for SQLite, Redis, and in-memory session storage
  • Environment-specific configuration (NODE_ENV vs LOG_LEVEL separation)

Bug Fixes:

  • Fixed session storage initialization order
  • Corrected API endpoint resolution from RODiT metadata
  • Fixed JWT token validation and session state checking
  • Resolved circular dependency issues in imports

Version 2.8.x

Features:

  • Initial session management implementation
  • Basic authentication and authorization
  • RODiT token validation
  • Portal authentication support

Migration Guide

Migrating to 4.0.2

Session Storage Configuration:

// Old (not available):
// Had to import from deep path

// New (available from main SDK):
const { RoditClient, setExpressSessionStore } = require('@rodit/rodit-auth-be');

const sessionStore = new SQLiteStore({ db: 'sessions.db', dir: './data' });
setExpressSessionStore(sessionStore);

Test Instance Creation:

// Old:
// Had to manually create with testMode option

// New (simplified):
const testClient = await RoditClient.createTestInstance();

License

Copyright (c) 2025 Discernible Inc. All rights reserved.