npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@rodit/rodit-auth-be

v3.1.0

Published

RODiT-based authentication system for Express.js applications

Downloads

504

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: 2.9.0
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 = 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 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

Session Configuration:

export SESSION_STORAGE_TYPE=express-session  # Storage: memory, express, express-session
export SESSION_CLEANUP_INTERVAL=3600000      # Cleanup interval in milliseconds (1 hour)
export SESSION_TOKEN_RETENTION_PERIOD=604800 # Token retention in seconds (7 days)
export SESSION_VALIDATION_CACHE_TTL=5000     # Cache TTL in milliseconds (5 seconds)

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",
  "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'
// }

Login Mode Control

The SDK provides configurable access control for RODiT authentication, allowing you to restrict which types of logins are accepted by your server.

Login Types

Partner Login (Client-Server)

  • Definition: Authentication where the peer's service provider ID is different from the server's service provider ID
  • Use Case: Traditional client-server authentication where a client authenticates to a service provider
  • Example: A mobile app (client) authenticating to your API server

Peer Login (Peer-to-Peer)

  • Definition: Authentication where the peer's service provider ID is the same as the server's service provider ID
  • Use Case: Peer-to-peer authentication between entities with the same service provider
  • Example: Two servers in the same organization authenticating to each other

Configuration Options

| Mode | Partner Logins | Peer Logins | Description | |------|---------------|-------------|-------------| | partner | ✅ Accepted | ❌ Rejected | Default - Only accept client-server authentication | | promiscuous | ✅ Accepted | ✅ Accepted | Accept all valid logins regardless of type | | p2p | ❌ Rejected | ✅ Accepted | Only accept peer-to-peer authentication |

Usage Examples

Default (Partner Only):

# No configuration needed - this is the default
# Only client-server authentication is accepted

Accept All Logins:

export SECURITY_OPTIONS_LOGIN_MODE=promiscuous
# Both Partner and Peer logins are accepted

Peer-to-Peer Only:

export SECURITY_OPTIONS_LOGIN_MODE=p2p
# Only peer-to-peer authentication is accepted

Docker/Podman:

podman run -e SECURITY_OPTIONS_LOGIN_MODE=partner ...

GitHub Actions: Add repository variable:

  • Name: SECURITY_OPTIONS_LOGIN_MODE
  • Value: partner | promiscuous | p2p

Logging and Monitoring

Successful Login:

{
  "level": "info",
  "message": "PARTNER login verified successfully",
  "verificationType": "PARTNER",
  "loginMode": "partner",
  "duration": 1234
}

Rejected Login:

{
  "level": "warn",
  "message": "PEER login rejected by LOGIN_MODE policy",
  "verificationType": "PEER",
  "loginMode": "partner",
  "policyReason": "LOGIN_MODE=partner does not accept PEER logins"
}

Metrics:

  • rodit_match_verification with result: "success" - Successful authentication
  • rodit_match_verification with result: "policy_rejected" - Rejected by policy

Security Considerations

  1. Default is Secure: The default partner mode provides the most restrictive access control
  2. Promiscuous Mode: Use only when you need to accept both types of authentication
  3. P2P Mode: Use when building peer-to-peer systems where only same-provider authentication is needed
  4. Policy Enforcement: Rejections are logged with clear reasons for audit trails

Troubleshooting

Login Rejected with "policy_rejected":

  • If you see "PEER login rejected" and need to accept peer logins, set mode to promiscuous or p2p
  • If you see "PARTNER login rejected" and need to accept partner logins, set mode to promiscuous or partner

Check Current Mode: Look for the log message during authentication:

"Starting RODiT match verification" with "loginMode": "partner"

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

2. SQLite Storage (Recommended for Production)

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 Storage Configuration

The SDK supports configurable session storage via the SESSION_STORAGE_TYPE environment variable.

Storage Type Options

1. "memory" (Default)

  • Uses SDK's standalone InMemorySessionStorage
  • No external dependencies required
  • Sessions stored in JavaScript Map
  • Sessions lost on server restart
  • Suitable for development or single-instance deployments
export SESSION_STORAGE_TYPE=memory

2. "express" or "express-session"

  • Uses express-session compatible stores
  • Requires express-session to be installed
  • Defaults to express-session MemoryStore
  • Can be overridden with setExpressSessionStore() for Redis, SQLite, etc.
  • Suitable for production with persistent storage
export SESSION_STORAGE_TYPE=express-session

Configuring Persistent Storage

SQLite Example:

const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const { 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');

Redis Example:

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

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

const redisStore = new RedisStore({
  client: redisClient,
  prefix: 'rodit:sess:',
  ttl: 86400
});

setExpressSessionStore(redisStore);

Session Configuration Variables

# Storage backend type
export SESSION_STORAGE_TYPE=express-session

# Cleanup interval (milliseconds) - how often to remove expired sessions
export SESSION_CLEANUP_INTERVAL=3600000  # 1 hour

# Token retention period (seconds) - how long to keep closed sessions
export SESSION_TOKEN_RETENTION_PERIOD=604800  # 7 days

# Validation cache TTL (milliseconds) - trades security for performance
# Lower = more secure but more storage lookups
# Higher = faster but longer window after logout where token may still work
# Set to 0 to disable caching (always check session state)
export SESSION_VALIDATION_CACHE_TTL=5000  # 5 seconds

Session Validation Cache:

The SDK caches token validation results to reduce storage lookups:

  • Enabled by default with 5-second TTL
  • Trade-off: Performance vs. security
  • After logout: Cache is immediately invalidated for that session
  • Recommendation: Keep default (5s) for most use cases
  • High security: Set to 0 to disable caching
// Get cache statistics
const sessionManager = roditClient.getSessionManager();
const cacheStats = sessionManager.getValidationCacheStats();
console.log('Cache stats:', cacheStats);
// Output: { totalEntries: 10, validEntries: 8, expiredEntries: 2, cacheTTL: 5000, cacheEnabled: true }

Session Operations

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

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

// Get storage information
const storageInfo = await sessionManager.getStorageInfo();
console.log('Storage type:', storageInfo.type);
console.log('Session count:', storageInfo.sessionCount);

// 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);

// Get detailed invalidation info
const invalidationInfo = await sessionManager.getTokenInvalidationInfo(jwtToken);
if (invalidationInfo) {
  console.log('Invalidation reason:', invalidationInfo.reason);
  console.log('Invalidated at:', invalidationInfo.invalidatedAt);
}

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

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

// Get validation cache statistics
const cacheStats = sessionManager.getValidationCacheStats();
console.log('Cache entries:', cacheStats.totalEntries);
console.log('Cache TTL:', cacheStats.cacheTTL);

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

Configuration Priority

The SDK automatically configures itself from multiple sources with a clear priority hierarchy:

  1. Environment Variables (Highest priority) - Direct process.env access
  2. Host Application Config - Values from config package (with env mappings)
  3. SDK Fallback Defaults - Built-in defaults from configsdk.js
  4. Provided Default Value - Optional parameter to config.get()

Example:

const config = roditClient.getConfig();

// Priority 1: Checks process.env.SESSION_STORAGE_TYPE
// Priority 2: Checks host config.get('SESSION_STORAGE_TYPE')
// Priority 3: Uses SDK default 'memory'
// Priority 4: Falls back to 'memory' if provided
const storageType = config.get('SESSION_STORAGE_TYPE', 'memory');

This ensures that:

  • CI/CD environment variables always take precedence
  • Host applications can override SDK defaults
  • SDK provides sensible defaults for all settings
  • Configuration is predictable and debuggable

Automatic Configuration Loading

The SDK loads configuration from multiple sources:

  1. Environment Variables - Direct environment access
  2. Configuration Files - config/default.json, config/production.json
  3. Vault Credentials - Production credential storage
  4. SDK Defaults - Fallback values

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 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));
}

Environment Variables

Complete list of SDK environment variables:

Core Configuration

# Service identification
export SERVICE_NAME=your-service-name
export API_VERSION=1.0.0

# Environment and logging
export NODE_ENV=production           # production, development, test, staging
export LOG_LEVEL=info                # error, warn, info, debug, trace

Credentials and Authentication

# Credential source
export RODIT_NEAR_CREDENTIALS_SOURCE=vault  # vault, file, env

# Vault configuration (production)
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 VAULT_TOKEN_TTL=3600

# File-based credentials (development)
export CREDENTIALS_FILEPATH=./credentials/rodit.json

# NEAR blockchain
export NEAR_CONTRACT_ID=your-contract.testnet
export NEAR_RPC_URL=https://rpc.testnet.fastnear.com
export NEAR_RPC_CACHE_TTL=5000       # milliseconds

Session Management

# Session storage configuration
export SESSION_STORAGE_TYPE=express-session     # memory, express, express-session
export SESSION_CLEANUP_INTERVAL=3600000         # milliseconds (1 hour)
export SESSION_TOKEN_RETENTION_PERIOD=604800    # seconds (7 days)
export SESSION_VALIDATION_CACHE_TTL=5000        # milliseconds (5 seconds)

Logging and Monitoring

# Loki logging
export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:password
export LOKI_TLS_SKIP_VERIFY=false    # true to skip TLS verification

Security Options

# Webhook configuration
export WEBHOOK_TLS_SKIP_VERIFY=false  # true to skip TLS verification

# Login mode control (see Login Mode section below)
export SECURITY_OPTIONS_LOGIN_MODE=partner  # partner, promiscuous, or p2p

# Security thresholds
export SECURITY_OPTIONS_LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY=0.80
export SECURITY_OPTIONS_THRESHOLD_VALIDATION_TYPE=0.10
export SECURITY_OPTIONS_DURATIONRAMP=0.85
export SECURITY_OPTIONS_SERVERORCLIENT=SERVER-INITIATED
export SECURITY_OPTIONS_SILENT_LOGIN_FAILURES=false

Database Configuration

export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite

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.

Direct-to-Loki via winston-loki (recommended)

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);

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.
  • 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 Tracking

The SDK includes comprehensive performance tracking and metrics collection.

Performance Service

const performanceService = roditClient.getPerformanceService();

// Record incoming request
performanceService.recordRequest(req);

// Record custom metrics with labels
performanceService.recordMetric('operation_duration', 150, {
  operation: 'db_query',
  table: 'users',
  status: 'success'
});

// Record errors
performanceService.recordMetric('error_count', 1, {
  method: req.method,
  path: req.path,
  status: res.statusCode
});

// Get aggregated metrics
const metrics = performanceService.getMetrics();
console.log('Total requests:', metrics.totalRequests);
console.log('Error count:', metrics.errorCount);
console.log('Average response time:', metrics.avgResponseTime);

Automatic Request Tracking

Integrate performance tracking into your middleware:

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

Session Performance Metrics

Track session-related performance:

const sessionManager = roditClient.getSessionManager();

// Get validation cache statistics
const cacheStats = sessionManager.getValidationCacheStats();
logger.info('Session cache performance', {
  component: 'SessionManager',
  totalEntries: cacheStats.totalEntries,
  validEntries: cacheStats.validEntries,
  expiredEntries: cacheStats.expiredEntries,
  cacheTTL: cacheStats.cacheTTL,
  cacheEnabled: cacheStats.cacheEnabled
});

// Get storage information
const storageInfo = await sessionManager.getStorageInfo();
logger.info('Session storage status', {
  component: 'SessionManager',
  storageType: storageInfo.type,
  sessionCount: storageInfo.sessionCount,
  timestamp: storageInfo.timestamp
});

Custom Metrics

Record application-specific metrics:

const performanceService = roditClient.getPerformanceService();

// Database operation timing
const dbStart = Date.now();
const result = await db.query('SELECT * FROM users');
const dbDuration = Date.now() - dbStart;

performanceService.recordMetric('db_query_duration', dbDuration, {
  operation: 'select',
  table: 'users',
  rowCount: result.length
});

// External API call timing
const apiStart = Date.now();
const apiResponse = await fetch('https://api.example.com/data');
const apiDuration = Date.now() - apiStart;

performanceService.recordMetric('external_api_duration', apiDuration, {
  endpoint: 'api.example.com',
  status: apiResponse.status,
  success: apiResponse.ok
});

// Business metrics
performanceService.recordMetric('user_action', 1, {
  action: 'comment_created',
  userId: req.user.id,
  timestamp: new Date().toISOString()
});

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)

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 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',