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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rodit/rodit-auth-be

v9.12.0

Published

RODiT-based authentication system for Express.js applications

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: 1.1.0
License: Proprietary
Author: Discernible IO

Login POST /api/login: Use accountid, timestamp, and base64url_signature. Sign UTF-8 bytes of accountid + timestamp_iso, and reject deprecated keys such as signature and account_id. See CHANGELOG.md.

Table of Contents

Quick Start

Installation

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - RUN COMMAND: npm install @rodit/rodit-auth-be
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Basic Server Setup

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET express TO require('express')
  - DO: const { RoditClient } = require('@rodit/rodit-auth-be')
  - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
  - DO: const { ulid } = require('ulid')
  - SET session TO require('express-session')
  - SET SQLiteStore TO require('connect-sqlite3')(session)
  - SET app TO express()
  - DO: let roditClient
  - NOTE: Configure session storage BEFORE initializing RoditClient
  - SET sessionStore TO new SQLiteStore({
  - FIELD: db: 'sessions.db',
  - FIELD: dir: './data',
  - FIELD: table: 'sessions'
  - DO: })
  - DO: setExpressSessionStore(sessionStore)
  - NOTE: Configure Express middleware
  - DO: app.use(express.json())
  - FIELD: app.use(express.urlencoded({ extended: false }))
  - NOTE: Request context middleware
  - DO: app.use((req, res, next) => {
  - DO: req.requestId = req.headers['x-request-id'] || ulid()
  - DO: req.startTime = Date.now()
  - DO: next()
  - DO: })
  - NOTE: Server startup with SDK initialization
  - DO: async function startServer() {
  - DO: try {
  - NOTE: Initialize RODiT client (use 'server' for server applications)
  - SET roditClient TO await RoditClient.create('server')
  - NOTE: Store client in app.locals for route access
  - DO: app.locals.roditClient = roditClient
  - NOTE: Get logger and other services from client
  - SET logger TO roditClient.getLogger()
  - SET config TO roditClient.getConfig()
  - SET loggingmw TO roditClient.getLoggingMiddleware()
  - NOTE: Apply logging middleware
  - DO: app.use(loggingmw)
  - NOTE: Create authentication middleware
  - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
  - NOTE: Logout-specific auth allows signature-valid expired tokens for clean session closure
  - SET authenticateLogout TO (req, res, next) => roditClient.authenticateForLogout(req, res, next)
  - SET authorize TO (req, res, next) => roditClient.authorize(req, res, next)
  - NOTE: Public routes
  - DO: app.post('/api/login', (req, res) => {
  - DO: req.logAction = 'login-attempt'
  - RETURN roditClient.login_client(req, res)
  - DO: })
  - NOTE: Protected routes
  - DO: app.post('/api/logout', authenticateLogout, (req, res) => {
  - DO: req.logAction = 'logout-attempt'
  - RETURN roditClient.logout_client(req, res)
  - DO: })
  - DO: app.get('/api/protected', authenticate, (req, res) => {
  - FIELD: res.json({ message: 'Protected data', user: req.user })
  - DO: })
  - NOTE: Protected + authorized routes
  - DO: app.use('/api/admin', authenticate, authorize, adminRoutes)
  - SET port TO 3000
  - DO: app.listen(port, () => {
  - DO: logger.info(`RODiT Authentication Server running on port ${port}`)
  - DO: })
  - DO: } catch (error) {
  - FIELD: console.error('Server initialization failed:', error)
  - DO: process.exit(1)
  - }
  - }
  - DO: startServer()
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: In main app.js
  - SET roditClient TO await RoditClient.create('server')
  - DO: app.locals.roditClient = roditClient
  - NOTE: In route modules
  - SET router TO express.Router()
  - DO: router.get('/data', (req, res) => {
  - SET client TO req.app.locals.roditClient
  - SET logger TO client.getLogger()
  - DO: logger.info('Processing request', {
  - FIELD: component: 'DataRoute',
  - FIELD: userId: req.user?.id
  - DO: })
  - FIELD: res.json({ data: 'example' })
  - DO: })
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Authentication Middleware Pattern

Create middleware functions that delegate to the RoditClient:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Create reusable middleware
  - SET authenticate TO (req, res, next) => {
  - SET client TO req.app.locals.roditClient
  - CHECK CONDITION: if (!client) {
  - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
  - }
  - RETURN client.authenticate(req, res, next)
  - DO: }
  - SET authorize TO (req, res, next) => {
  - SET client TO req.app.locals.roditClient
  - CHECK CONDITION: if (!client) {
  - RETURN res.status(503).json({ error: 'Authorization service unavailable' })
  - }
  - RETURN client.authorize(req, res, next)
  - DO: }
  - NOTE: Use in routes
  - DO: app.get('/api/protected', authenticate, handler)
  - DO: app.post('/api/admin', authenticate, authorize, adminHandler)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Installation & Setup

Dependencies

Required:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - RUN COMMAND: npm install @rodit/rodit-auth-be express config winston
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Recommended for main:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - RUN COMMAND: npm install express-session connect-sqlite3
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Optional:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - RUN COMMAND: npm install node-vault  # For Vault-based credentials
  - RUN COMMAND: npm install winston-loki  # For Grafana Loki logging
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Environment Variables

Vault Configuration (main):

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault
  - FIELD: export VAULT_ENDPOINT=https://vault.example.com
  - DO: export VAULT_ROLE_ID=your-role-id
  - DO: export VAULT_SECRET_ID=your-secret-id
  - DO: export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
  - DO: export SERVICE_NAME=your-service-name
  - DO: export NEAR_CONTRACT_ID=discernible-io.near
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Application Configuration:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - FIELD: export NODE_ENV=main  # Environment: main, development, test
  - FIELD: export LOG_LEVEL=info       # Logging: error, warn, info, debug, trace
  - DO: export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Session Configuration:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - FIELD: export SESSION_STORAGE_TYPE=express-session  # Storage: memory, express, express-session
  - DO: export SESSION_CLEANUP_INTERVAL=3600000      # Cleanup interval in milliseconds (1 hour)
  - DO: export SESSION_TOKEN_RETENTION_PERIOD=604800 # Token retention in seconds (7 days)
  - DO: export SESSION_VALIDATION_CACHE_TTL=5000     # Cache TTL in milliseconds (5 seconds)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Logging Configuration:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - FIELD: export LOKI_URL=https://loki.example.com:3100
  - FIELD: export LOKI_BASIC_AUTH=username:password
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Configuration Files

Create config/default.json:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - {
  - FIELD: "NEAR_CONTRACT_ID": "discernible-io.near",
  - FIELD: "SERVICE_NAME": "your-service",
  - FIELD: "SECURITY_OPTIONS": {
  - FIELD: "SILENT_LOGIN_FAILURES": false,
  - FIELD: "SESSION_TTL_SECONDS": 5200  // server session lifetime from login (default in SDK)
  - FIELD: "FALLBACK_JWT_DURATION": 3600  // access-token fallback when passport jwt_duration is invalid
  - }
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Authentication

RODiT-Based Authentication

RODiT provides cryptographic mutual authentication using blockchain-verified identities.

Client Login Request

For API login documentation, use accountid with HTTP POST /api/login. The signed payload is accountid + timestamp_iso (no separator).

| Field | Description | |-------|-------------| | timestamp | Recommended; Unix seconds from GET /api/login/timestamp | | base64url_signature | Ed25519 detached signature (base64url) over accountid + timestamp_iso | | accountid | 64-hex implicit NEAR account login identifier |

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Implicit account login
  - {
  - FIELD: "accountid": "<64-char-hex>",
  - FIELD: "timestamp": 1640995200,
  - FIELD: "base64url_signature": "base64url-encoded-signature"
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Use base64url_signature in login payloads for API login examples.

Rejected keys (HTTP 400, LOGIN_PAYLOAD_DEPRECATED): signature and account_id.

Server Response

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Success (200)
  - {
  - FIELD: "jwt_token": "<jwt-token>",
  - FIELD: "requestId": "01HQXYZ123ABC"
  - }
  - NOTE: Headers:
  - NOTE: New-Token: <jwt>   (same token echoed for header-based clients)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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

Security hardening in current implementation:

  • JWT compact parts must be canonical base64url (non-canonical encodings are rejected).
  • Session registration is enforced during JWT validation (unknown/inactive/expired sessions are rejected).
  • Server session length defaults to SECURITY_OPTIONS.SESSION_TTL_SECONDS (5200 s); see Session lifetime and TTL.
  • Token renewal uses sessionManager for session checks and updates (no stateManager session mutations).

Login Implementation

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: routes/login.js
  - SET express TO require('express')
  - SET router TO express.Router()
  - DO: router.post('/login', async (req, res) => {
  - DO: req.logAction = 'login-attempt'
  - SET client TO req.app.locals.roditClient
  - CHECK CONDITION: if (!client) {
  - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
  - }
  - NOTE: Delegate to SDK's login_client method
  - WAIT FOR: client.login_client(req, res)
  - DO: })
  - DO: module.exports = router
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Logout Implementation

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Logout invalidates the JWT token and closes the session
  - NOTE: Use logout-specific auth so signature-valid expired tokens can still logout.
  - DO: router.post('/logout', authenticateLogout, async (req, res) => {
  - DO: req.logAction = 'logout-attempt'
  - SET client TO req.app.locals.roditClient
  - CHECK CONDITION: if (!client) {
  - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
  - }
  - NOTE: Delegate to SDK's logout_client method
  - WAIT FOR: client.logout_client(req, res)
  - DO: })
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Protected Routes

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Require authentication for access
  - DO: app.get('/api/data', authenticate, (req, res) => {
  - NOTE: req.user contains authenticated user information
  - SET logger TO req.app.locals.roditClient.getLogger()
  - DO: logger.info('Protected route accessed', {
  - FIELD: component: 'API',
  - FIELD: userId: req.user.id,
  - FIELD: roditId: req.user.roditId,
  - FIELD: requestId: req.requestId
  - DO: })
  - DO: res.json({
  - FIELD: message: 'Authenticated data',
  - FIELD: user: req.user,
  - FIELD: requestId: req.requestId
  - DO: })
  - DO: })
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Authentication Middleware

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

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET authenticate TO (req, res, next) => {
  - SET client TO req.app.locals.roditClient
  - RETURN client.authenticate(req, res, next)
  - DO: }
  - NOTE: After successful authentication, req.user contains:
  - NOTE: {
  - NOTE: id: 'user-unique-id',
  - NOTE: roditId: '01K4G3D95QF6NR0RSJK9WEK6KA',
  - NOTE: aud: 'audience',
  - NOTE: iss: 'issuer',
  - NOTE: exp: 1640999999,
  - NOTE: iat: 1640995200,
  - NOTE: session_id: '01HQXYZ123ABC'
  - NOTE: }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: No configuration needed - this is the default
  - NOTE: Only client-server authentication is accepted
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Accept All Logins:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export SECURITY_OPTIONS_LOGIN_MODE=promiscuous
  - NOTE: Both Partner and Peer logins are accepted
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Peer-to-Peer Only:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export SECURITY_OPTIONS_LOGIN_MODE=p2p
  - NOTE: Only peer-to-peer authentication is accepted
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Docker/Podman:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: podman run -e SECURITY_OPTIONS_LOGIN_MODE=partner ...
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

GitHub Actions: Add repository variable:

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

Logging and Monitoring

Successful Login:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - {
  - FIELD: "level": "info",
  - FIELD: "message": "PARTNER login verified successfully",
  - FIELD: "verificationType": "PARTNER",
  - FIELD: "loginMode": "partner",
  - FIELD: "duration": 1234
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Rejected Login:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - {
  - FIELD: "level": "warn",
  - FIELD: "message": "PEER login rejected by LOGIN_MODE policy",
  - FIELD: "verificationType": "PEER",
  - FIELD: "loginMode": "partner",
  - FIELD: "policyReason": "LOGIN_MODE=partner does not accept PEER logins"
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - {
  - FIELD: "permissioned_routes": {
  - FIELD: "entities": {
  - FIELD: "/": {
  - FIELD: "methods": "+0"
  - DO: },
  - FIELD: "/api/echo": {
  - FIELD: "methods": "+0"
  - DO: },
  - FIELD: "/api/cruda/create": {
  - FIELD: "methods": "+0"
  - DO: },
  - FIELD: "/api/cruda/list": {
  - FIELD: "methods": "+0"
  - DO: },
  - FIELD: "/api/admin": {
  - FIELD: "methods": "+0"
  - }
  - }
  - }
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET authenticate TO (req, res, next) => {
  - RETURN req.app.locals.roditClient.authenticate(req, res, next)
  - DO: }
  - SET authorize TO (req, res, next) => {
  - RETURN req.app.locals.roditClient.authorize(req, res, next)
  - DO: }
  - NOTE: Apply both authentication and authorization
  - DO: app.use('/api/admin', authenticate, authorize, adminRoutes)
  - NOTE: CRUDA endpoints with full protection
  - DO: app.use('/api/cruda', authenticate, authorize, crudaRoutes)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Permission Enforcement

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Example: CRUDA routes with permission checking
  - SET router TO express.Router()
  - NOTE: All routes require authentication + authorization
  - DO: router.post('/create', async (req, res) => {
  - NOTE: User must have permission for POST /api/cruda/create
  - DO: const { comment, author } = req.body
  - NOTE: Create record in database
  - SET result TO await db.run(
  - DO: 'INSERT INTO comments (comment, author) VALUES (?, ?)',
  - DO: [comment, author || req.user.roditId]
  - DO: )
  - FIELD: res.json({ id: result.lastID, requestId: req.requestId })
  - DO: })
  - DO: router.post('/list', async (req, res) => {
  - NOTE: User must have permission for POST /api/cruda/list
  - SET records TO await db.all('SELECT * FROM comments ORDER BY created_at DESC')
  - FIELD: res.json({ records, requestId: req.requestId })
  - DO: })
  - DO: module.exports = router
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Dynamic Permission Checking

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Check permissions programmatically
  - SET client TO req.app.locals.roditClient
  - SET hasPermission TO client.isOperationPermitted('POST', '/api/admin/users')
  - CHECK CONDITION: if (!hasPermission) {
  - RETURN res.status(403).json({
  - FIELD: error: 'Forbidden',
  - FIELD: message: 'You do not have permission to access this resource',
  - FIELD: requestId: req.requestId
  - DO: })
  - }
  - NOTE: Proceed with operation
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Client requests these permissions:
  - SET requestedPermissions TO {
  - FIELD: "/": "+0",
  - FIELD: "/api/echo": "+0",
  - FIELD: "/api/cruda/create": "+0"
  - DO: }
  - NOTE: Server validates against its own permissioned_routes
  - NOTE: If any requested route is not in server's config, request is rejected with HTTP 400
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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 lifetime and TTL

Server sessions and JWT access credentials use different clocks:

| Concept | Controlled by | Stored / carried as | |---------|----------------|---------------------| | Server session | SECURITY_OPTIONS.SESSION_TTL_SECONDS (host config) | sessionManager record expiresAt; JWT claim session_exp | | Access credential (JWT exp) | Passport jwt_duration on peer/own RODiT metadata (+ renewal) | JWT exp; renewed until session_exp |

You do not need to change on-chain jwt_duration on the server RODiT token to control how long a session lasts. Set session length in application config instead.

SECURITY_OPTIONS.SESSION_TTL_SECONDS

| Property | Value | |----------|--------| | SDK default | 5200 (~87 minutes) | | Valid range | 6031536000 (365 days), or 0 to disable | | Config path | SECURITY_OPTIONS.SESSION_TTL_SECONDS | | Env example | SECURITY_OPTIONS_SESSION_TTL_SECONDS=2592000 (30 days, with node-config style mapping) |

At login the SDK computes:

session_expiresAt = login_time + SESSION_TTL_SECONDS

Then applies passport caps: if either peer or own RODiT has a bounded not_after, the session cannot end later than the earlier of those dates.

Set SESSION_TTL_SECONDS to 0 to fall back to passport-derived session end (bounded not_after when present, otherwise max(peer, own) jwt_duration).

Examples

Default (5200 seconds):

// config/default.json — omit SESSION_TTL_SECONDS to use SDK default 5200
{
  "SECURITY_OPTIONS": {
    "FALLBACK_JWT_DURATION": 3600
  }
}

30-day sessions:

{
  "SECURITY_OPTIONS": {
    "SESSION_TTL_SECONDS": 2592000
  }
}

Passport-derived session length (legacy):

{
  "SECURITY_OPTIONS": {
    "SESSION_TTL_SECONDS": 0
  }
}

Enforcement on each API request

For normal API authentication (authenticate_apicall), the SDK:

  1. Checks stored session: exists, status === 'active', expiresAt not in the past.
  2. Validates JWT signature and exp (with renewal when eligible).
  3. Requires JWT session_exp to match stored expiresAt when session registration is enforced.

Portal/outbound login token validation can skip session registration when SECURITY_OPTIONS.RELAXED_SESSION_VALIDATION is true (default).

Related options

| Option | Purpose | |--------|---------| | FALLBACK_JWT_DURATION | Access-token lifetime when passport jwt_duration is missing or invalid (default 3600; max 7 days in validator) | | JWT_MAX_DURATION_SECONDS_RODIT_UNBOUNDED | Cap on JWT exp when peer not_after is unbounded | | RELAXED_SESSION_VALIDATION | Portal/outbound flows may skip server session lookup | | SESSION_VALIDATION_CACHE_TTL | Cache TTL for session invalidation checks after logout |

Session Storage Backends

1. In-Memory Storage (Default)

No configuration needed - works out of the box:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET client TO await RoditClient.create('server')
  - NOTE: Uses InMemorySessionStorage by default
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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

2. SQLite Storage (Recommended for main)

Persistent storage using SQLite database:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET express TO require('express')
  - SET session TO require('express-session')
  - SET SQLiteStore TO require('connect-sqlite3')(session)
  - DO: const { RoditClient } = require('@rodit/rodit-auth-be')
  - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
  - NOTE: Configure BEFORE initializing RoditClient
  - SET sessionStore TO new SQLiteStore({
  - FIELD: db: 'sessions.db',
  - FIELD: dir: './data',
  - FIELD: table: 'sessions'
  - DO: })
  - DO: setExpressSessionStore(sessionStore)
  - NOTE: Now initialize client
  - SET client TO await RoditClient.create('server')
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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

3. Redis Storage (For Multi-Server)

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - RUN COMMAND: npm install express-session connect-redis redis
OUTPUTS:
  - Produces the section's intended result using equivalent logic.
PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET session TO require('express-session')
  - SET RedisStore TO require('connect-redis').default
  - DO: const { createClient } = require('redis')
  - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
  - NOTE: Create Redis client
  - SET redisClient TO createClient({
  - FIELD: url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
  - DO: })
  - WAIT FOR: redisClient.connect()
  - NOTE: Create Redis store
  - SET redisStore TO new RedisStore({
  - FIELD: client: redisClient,
  - FIELD: prefix: 'rodit:sess:',
  - FIELD: ttl: 86400 // 24 hours
  - DO: })
  - DO: setExpressSessionStore(redisStore)
  - SET client TO await RoditClient.create('server')
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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
PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export SESSION_STORAGE_TYPE=memory
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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 main with persistent storage
PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export SESSION_STORAGE_TYPE=express-session
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Configuring Persistent Storage

SQLite Example:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET session TO require('express-session')
  - SET SQLiteStore TO require('connect-sqlite3')(session)
  - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
  - NOTE: Configure BEFORE initializing RoditClient
  - SET sessionStore TO new SQLiteStore({
  - FIELD: db: 'sessions.db',
  - FIELD: dir: './data',
  - FIELD: table: 'sessions'
  - DO: })
  - DO: setExpressSessionStore(sessionStore)
  - NOTE: Now initialize client
  - SET client TO await RoditClient.create('server')
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Redis Example:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET session TO require('express-session')
  - SET RedisStore TO require('connect-redis').default
  - DO: const { createClient } = require('redis')
  - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
  - SET redisClient TO createClient({
  - FIELD: url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
  - DO: })
  - WAIT FOR: redisClient.connect()
  - SET redisStore TO new RedisStore({
  - FIELD: client: redisClient,
  - FIELD: prefix: 'rodit:sess:',
  - FIELD: ttl: 86400
  - DO: })
  - DO: setExpressSessionStore(redisStore)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Session Configuration Variables

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Storage backend type
  - DO: export SESSION_STORAGE_TYPE=express-session
  - NOTE: Cleanup interval (milliseconds) - how often to remove expired sessions
  - DO: export SESSION_CLEANUP_INTERVAL=3600000  # 1 hour
  - NOTE: Token retention period (seconds) - how long to keep closed sessions
  - DO: export SESSION_TOKEN_RETENTION_PERIOD=604800  # 7 days
  - NOTE: Validation cache TTL (milliseconds) - trades security for performance
  - NOTE: Lower = more secure but more storage lookups
  - NOTE: Higher = faster but longer window after logout where token may still work
  - NOTE: Set to 0 to disable caching (always check session state)
  - DO: export SESSION_VALIDATION_CACHE_TTL=5000  # 5 seconds
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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
PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Get cache statistics
  - SET sessionManager TO roditClient.getSessionManager()
  - SET cacheStats TO sessionManager.getValidationCacheStats()
  - FIELD: console.log('Cache stats:', cacheStats)
  - NOTE: Output: { totalEntries: 10, validEntries: 8, expiredEntries: 2, cacheTTL: 5000, cacheEnabled: true }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Session Operations

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Get session manager
  - SET sessionManager TO roditClient.getSessionManager()
  - NOTE: Get active session count
  - SET activeCount TO await sessionManager.getActiveSessionCount()
  - NOTE: Get storage information
  - SET storageInfo TO await sessionManager.getStorageInfo()
  - FIELD: console.log('Storage type:', storageInfo.type)
  - FIELD: console.log('Session count:', storageInfo.sessionCount)
  - NOTE: Enumerate sessions via storage
  - SET allSessions TO await sessionManager.storage.getAll()
  - NOTE: Or fallback using keys() + get()
  - SET sessionIds TO await sessionManager.storage.keys()
  - SET sessions TO []
  - REPEAT: for (const id of sessionIds) {
  - SET session TO await sessionManager.storage.get(id)
  - CHECK CONDITION: if (session) sessions.push(session)
  - }
  - NOTE: Check if token is invalidated
  - SET isInvalidated TO await sessionManager.isTokenInvalidated(jwtToken)
  - NOTE: Get detailed invalidation info
  - SET invalidationInfo TO await sessionManager.getTokenInvalidationInfo(jwtToken)
  - CHECK CONDITION: if (invalidationInfo) {
  - FIELD: console.log('Invalidation reason:', invalidationInfo.reason)
  - FIELD: console.log('Invalidated at:', invalidationInfo.invalidatedAt)
  - }
  - NOTE: Manually close a session
  - WAIT FOR: sessionManager.closeSession(sessionId, 'admin_action')
  - NOTE: Run manual cleanup (removes expired sessions)
  - SET cleanup TO await sessionManager.runManualCleanup()
  - DO: console.log(`Removed ${cleanup.removedSessionsCount} expired sessions`)
  - NOTE: Get validation cache statistics
  - SET cacheStats TO sessionManager.getValidationCacheStats()
  - FIELD: console.log('Cache entries:', cacheStats.totalEntries)
  - FIELD: console.log('Cache TTL:', cacheStats.cacheTTL)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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 expire when stored expiresAt is reached (SESSION_TTL_SECONDS from login, capped by passport not_after)
  5. Cleanup - Expired sessions removed by automatic cleanup process

Token Invalidation

The SDK validates tokens by checking session state:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Authentication middleware checks:
  - NOTE: 1. JWT signature validity
  - NOTE: 2. JWT expiration
  - NOTE: 3. Session exists and is active
  - NOTE: 4. Session not expired
  - NOTE: After logout, tokens are invalidated because:
  - NOTE: - Session status set to 'closed'
  - NOTE: - Subsequent requests fail authentication
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET config TO roditClient.getConfig()
  - NOTE: Priority 1: Checks process.env.SESSION_STORAGE_TYPE
  - NOTE: Priority 2: Checks host config.get('SESSION_STORAGE_TYPE')
  - NOTE: Priority 3: Uses SDK default 'memory'
  - NOTE: Priority 4: Falls back to 'memory' if provided
  - SET storageType TO config.get('SESSION_STORAGE_TYPE', 'memory')
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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/main.json, config/development.json
  3. Vault Credentials - Main 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:

  • main - Main branch deploy (strict security, no error details)
  • development - Development branch deploy (relaxed security, detailed errors)
  • test - Testing environment (allows bypasses for automated testing)

Default: development

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 main)
  • 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

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Environment detection (security)
  - SET isMain TO process.env.NODE_ENV === 'main'
  - SET isDevelopment TO process.env.NODE_ENV === 'development'
  - SET isTest TO process.env.NODE_ENV === 'test'
  - NOTE: Logging verbosity (independent)
  - SET config TO roditClient.getConfig()
  - SET logLevel TO config.get('LOG_LEVEL', 'info')
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Configuration Examples

Main (normal):

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export NODE_ENV=main
  - DO: export LOG_LEVEL=info
  - NOTE: Results in:
  - NOTE: - Strict security enforcement
  - NOTE: - No error details in responses
  - NOTE: - Minimal logging output
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Main (troubleshooting):

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export NODE_ENV=main
  - DO: export LOG_LEVEL=debug
  - NOTE: Results in:
  - NOTE: - Strict security enforcement (still main)
  - NOTE: - No error details in responses (still secure)
  - NOTE: - Verbose logging for debugging
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Development:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export NODE_ENV=development
  - DO: export LOG_LEVEL=debug
  - NOTE: Results in:
  - NOTE: - Relaxed security for development
  - NOTE: - Detailed error messages in responses
  - NOTE: - Verbose logging
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Testing:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export NODE_ENV=test
  - DO: export LOG_LEVEL=error
  - NOTE: Results in:
  - NOTE: - Test mode (allows bypasses)
  - NOTE: - Detailed error messages
  - NOTE: - Only errors logged (cleaner test output)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Behavior Matrix

| Scenario | NODE_ENV | LOG_LEVEL | Security | Error Details | Logging | |----------|----------|-----------|----------|---------------|---------| | Main | main | info | ✅ Strict | ❌ Hidden | Minimal | | Main Debug | main | debug | ✅ Strict | ❌ Hidden | Verbose | | Development | development | debug | ⚠️ Relaxed | ✅ Shown | Verbose | | Testing | test | error | ⚠️ Bypass OK | ✅ Shown | Errors only |

Vault-Based Configuration (main)

For main deployments, credentials are loaded from HashiCorp Vault:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Environment variables for vault
  - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault
  - FIELD: export VAULT_ENDPOINT=https://vault.example.com
  - DO: export VAULT_ROLE_ID=your-role-id
  - DO: export VAULT_SECRET_ID=your-secret-id
  - DO: export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
  - DO: export SERVICE_NAME=your-service-name
  - DO: export NEAR_CONTRACT_ID=discernible-io.near
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

File-Based Configuration (Development)

For development, credentials can be loaded from files:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=file
  - DO: export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Accessing Configuration

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Get complete RODiT configuration
  - SET configObject TO await roditClient.getConfigOwnRodit()
  - SET metadata TO configObject.own_rodit.metadata
  - NOTE: Access RODiT token metadata
  - SET jwtDuration TO metadata.jwt_duration;  // JWT expiration time
  - SET maxRequests TO metadata.max_requests;  // Rate limit
  - SET maxRqWindow TO metadata.maxrq_window;  // Rate limit window
  - SET apiEndpoint TO metadata.subjectuniqueidentifier_url;  // API URL
  - SET webhookUrl TO metadata.webhook_url;  // Webhook endpoint
  - NOTE: Parse permissioned routes
  - SET permissionedRoutes TO JSON.parse(metadata.permissioned_routes || '{}')
  - NOTE: Use SDK config for application settings
  - SET config TO roditClient.getConfig()
  - SET logLevel TO config.get('LOG_LEVEL', 'info')
  - SET dbPath TO config.get('API_DEFAULT_OPTIONS.DB_PATH')
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Dynamic Rate Limiting

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Configure rate limiting from RODiT token
  - SET configObject TO await roditClient.getConfigOwnRodit()
  - SET metadata TO configObject.own_rodit.metadata
  - CHECK CONDITION: if (metadata.max_requests && metadata.maxrq_window) {
  - SET maxRequests TO parseInt(metadata.max_requests)
  - SET windowSeconds TO parseInt(metadata.maxrq_window)
  - SET rateLimiter TO roditClient.getRateLimitMiddleware()
  - DO: app.use(rateLimiter(maxRequests, windowSeconds))
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Environment Variables

Complete list of SDK environment variables:

Core Configuration

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Service identification
  - DO: export SERVICE_NAME=your-service-name
  - DO: export API_VERSION=1.0.0
  - NOTE: Environment and logging
  - DO: export NODE_ENV=main               # main, development, test
  - DO: export LOG_LEVEL=info                # error, warn, info, debug, trace
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Credentials and Authentication

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Credential source
  - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault  # vault, file, env
  - NOTE: Vault configuration (main)
  - FIELD: export VAULT_ENDPOINT=https://vault.example.com
  - DO: export VAULT_ROLE_ID=your-role-id
  - DO: export VAULT_SECRET_ID=your-secret-id
  - DO: export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
  - DO: export VAULT_TOKEN_TTL=3600
  - NOTE: File-based credentials (development)
  - DO: export CREDENTIALS_FILEPATH=./credentials/rodit.json
  - NOTE: NEAR blockchain
  - DO: export NEAR_CONTRACT_ID=discernible-io.near
  - FIELD: export NEAR_RPC_URL=https://rpc.mainnet.fastnear.com
  - DO: export NEAR_RPC_CACHE_TTL=5000       # milliseconds
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Session Management

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Session storage configuration
  - DO: export SESSION_STORAGE_TYPE=express-session     # memory, express, express-session
  - DO: export SESSION_CLEANUP_INTERVAL=3600000         # milliseconds (1 hour)
  - DO: export SESSION_TOKEN_RETENTION_PERIOD=604800    # seconds (7 days)
  - DO: export SESSION_VALIDATION_CACHE_TTL=5000        # milliseconds (5 seconds)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Logging and Monitoring

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Loki logging
  - FIELD: export LOKI_URL=https://loki.example.com:3100
  - FIELD: export LOKI_BASIC_AUTH=username:password
  - DO: export LOKI_TLS_SKIP_VERIFY=false    # true to skip TLS verification
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Security Options

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Webhook configuration
  - DO: export WEBHOOK_TLS_SKIP_VERIFY=false  # true to skip TLS verification
  - NOTE: Login mode control (see Login Mode section below)
  - DO: export SECURITY_OPTIONS_LOGIN_MODE=partner  # partner, promiscuous, or p2p
  - NOTE: Security thresholds
  - DO: export SECURITY_OPTIONS_LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY=0.80
  - DO: export SECURITY_OPTIONS_THRESHOLD_VALIDATION_TYPE=0.10
  - DO: export SECURITY_OPTIONS_DURATIONRAMP=0.85
  - DO: export SECURITY_OPTIONS_SERVERORCLIENT=SERVER-INITIATED
  - DO: export SECURITY_OPTIONS_SILENT_LOGIN_FAILURES=false
  - NOTE: Server session lifetime (seconds from login; SDK default 5200)
  - DO: export SECURITY_OPTIONS_SESSION_TTL_SECONDS=5200
  - NOTE: Access-token fallback when passport jwt_duration is invalid
  - DO: export SECURITY_OPTIONS_FALLBACK_JWT_DURATION=3600
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Database Configuration

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Logging & Monitoring

Structured Logging

The SDK provides comprehensive structured logging:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: const { logger } = require('@rodit/rodit-auth-be')
  - NOTE: Basic logging
  - DO: logger.info('Operation completed', {
  - FIELD: component: 'UserService',
  - FIELD: operation: 'createUser',
  - FIELD: userId: '123',
  - FIELD: duration: 150
  - DO: })
  - NOTE: Context-aware logging
  - DO: logger.infoWithContext('Request processed', {
  - FIELD: component: 'API',
  - FIELD: method: 'POST',
  - FIELD: path: '/api/users',
  - FIELD: requestId: req.requestId,
  - FIELD: userId: req.user?.id,
  - FIELD: duration: Date.now() - req.startTime
  - DO: })
  - NOTE: Error logging with metrics
  - DO: logger.errorWithContext('Operation failed', {
  - FIELD: component: 'UserService',
  - FIELD: operation: 'createUser',
  - FIELD: requestId: req.requestId,
  - FIELD: error: error.message,
  - FIELD: stack: error.stack
  - DO: }, error)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Loki with the SDK (canonical)

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

Environment variables

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - FIELD: export LOKI_URL=https://<your-loki-host>:3100
  - FIELD: export LOKI_BASIC_AUTH="username:password"   # store in secrets
  - DO: export LOKI_TLS_SKIP_VERIFY=true              # only for self-signed/test
  - DO: export LOG_LEVEL=info
  - DO: export SERVICE_NAME=clienttest-idc
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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.
  • Main: 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)

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - DO: const { logger } = require('@rodit/rodit-auth-be')
  - SET winston TO require('winston')
  - SET LokiTransport TO require('winston-loki')
  - SET transports TO [new winston.transports.Console({ format: winston.format.json() })]
  - CHECK CONDITION: if (process.env.LOKI_URL) {
  - SET lokiOptions TO {
  - FIELD: host: process.env.LOKI_URL,
  - FIELD: basicAuth: process.env.LOKI_BASIC_AUTH, // Basic Auth for Loki
  - FIELD: labels: { app: process.env.SERVICE_NAME || 'clienttest-idc', component: 'rodit-sdk' },
  - FIELD: json: true,
  - FIELD: batching: true
  - DO: }
  - CHECK CONDITION: if ((process.env.LOKI_TLS_SKIP_VERIFY || '').toLowerCase() === 'true') {
  - FIELD: lokiOptions.ssl = { rejectUnauthorized: false }
  - }
  - DO: transports.push(new LokiTransport(lokiOptions))
  - }
  - SET customLogger TO winston.createLogger({
  - FIELD: level: process.env.LOG_LEVEL || 'info',
  - FIELD: format: winston.format.json(),
  - DO: transports
  - DO: })
  - DO: logger.setLogger(customLogger)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

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="clienttest-idc"} and confirm logs.

Performance Tracking

The SDK includes comprehensive performance tracking and metrics collection.

Performance Service

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET performanceService TO roditClient.getPerformanceService()
  - NOTE: Record incoming request
  - DO: performanceService.recordRequest(req)
  - NOTE: Record custom metrics with labels
  - DO: performanceService.recordMetric('operation_duration', 150, {
  - FIELD: operation: 'db_query',
  - FIELD: table: 'users',
  - FIELD: status: 'success'
  - DO: })
  - NOTE: Record errors
  - DO: performanceService.recordMetric('error_count', 1, {
  - FIELD: method: req.method,
  - FIELD: path: req.path,
  - FIELD: status: res.statusCode
  - DO: })
  - NOTE: Get aggregated metrics
  - SET metrics TO performanceService.getMetrics()
  - FIELD: console.log('Total requests:', metrics.totalRequests)
  - FIELD: console.log('Error count:', metrics.errorCount)
  - FIELD: console.log('Average response time:', metrics.avgResponseTime)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Automatic Request Tracking

Integrate performance tracking into your middleware:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Performance monitoring middleware
  - DO: app.use((req, res, next) => {
  - DO: req.startTime = Date.now()
  - SET performanceService TO roditClient.getPerformanceService()
  - CHECK CONDITION: if (performanceService) {
  - DO: performanceService.recordRequest(req)
  - }
  - DO: res.on('finish', () => {
  - SET duration TO Date.now() - req.startTime
  - CHECK CONDITION: if (performanceService) {
  - NOTE: Record request duration
  - DO: performanceService.recordMetric('request_duration_ms', duration, {
  - FIELD: method: req.method,
  - FIELD: path: req.path,
  - FIELD: status: res.statusCode
  - DO: })
  - NOTE: Record errors
  - CHECK CONDITION: if (res.statusCode >= 400) {
  - DO: performanceService.recordMetric('error_count', 1, {
  - FIELD: method: req.method,
  - FIELD: path: req.path,
  - FIELD: status: res.statusCode
  - DO: })
  - }
  - }
  - DO: })
  - DO: next()
  - DO: })
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Session Performance Metrics

Track session-related performance:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET sessionManager TO roditClient.getSessionManager()
  - NOTE: Get validation cache statistics
  - SET cacheStats TO sessionManager.getValidationCacheStats()
  - DO: logger.info('Session cache performance', {
  - FIELD: component: 'SessionManager',
  - FIELD: totalEntries: cacheStats.totalEntries,
  - FIELD: validEntries: cacheStats.validEntries,
  - FIELD: expiredEntries: cacheStats.expiredEntries,
  - FIELD: cacheTTL: cacheStats.cacheTTL,
  - FIELD: cacheEnabled: cacheStats.cacheEnabled
  - DO: })
  - NOTE: Get storage information
  - SET storageInfo TO await sessionManager.getStorageInfo()
  - DO: logger.info('Session storage status', {
  - FIELD: component: 'SessionManager',
  - FIELD: storageType: storageInfo.type,
  - FIELD: sessionCount: storageInfo.sessionCount,
  - FIELD: timestamp: storageInfo.timestamp
  - DO: })
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Custom Metrics

Record application-specific metrics:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET performanceService TO roditClient.getPerformanceService()
  - NOTE: Database operation timing
  - SET dbStart TO Date.now()
  - SET result TO await db.query('SELECT * FROM users')
  - SET dbDuration TO Date.now() - dbStart
  - DO: performanceService.recordMetric('db_query_duration', dbDuration, {
  - FIELD: operation: 'select',
  - FIELD: table: 'users',
  - FIELD: rowCount: result.length
  - DO: })
  - NOTE: External API call timing
  - SET apiStart TO Date.now()
  - SET apiResponse TO await fetch('https://api.example.com/data')
  - SET apiDuration TO Date.now() - apiStart
  - DO: performanceService.recordMetric('external_api_duration', apiDuration, {
  - FIELD: endpoint: 'api.example.com',
  - FIELD: status: apiResponse.status,
  - FIELD: success: apiResponse.ok
  - DO: })
  - NOTE: Business metrics
  - DO: performanceService.recordMetric('user_action', 1, {
  - FIELD: action: 'comment_created',
  - FIELD: userId: req.user.id,
  - FIELD: timestamp: new Date().toISOString()
  - DO: })
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Webhooks

Overview

The SDK supports sending webhooks to multiple endpoints for important events. Webhook URLs are configured in the RODiT token metadata.

Key Features:

  • Custom Endpoints - Send webhooks to any endpoint path (e.g., /hooks/wake, /hooks/agent, /webhook)
  • Non-blocking - Webhooks sent asynchronously without blocking the main response
  • Error Resilient - Webhook failures don't affect the main operation

Webhooks are configured in your RODiT token:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - {
  - FIELD: "webhook_url": "https://webhook.example.com:7443",
  - FIELD: "webhook_cidr": "0.0.0.0/0"
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Sending Webhooks to Default Endpoint

Send webhooks to the default /webhook endpoint:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Get webhook handler from client
  - SET roditClient TO req.app.locals.roditClient
  - NOTE: Send webhook for an event
  - SET webhookPayload TO {
  - FIELD: event: 'comment_created',
  - FIELD: data: {
  - FIELD: id: comment.id,
  - FIELD: author: comment.author,
  - FIELD: timestamp: new Date().toISOString()
  - DO: },
  - FIELD: isError: false
  - DO: }
  - DO: try {
  - SET result TO await roditClient.sendWebhook(webhookPayload, req)
  - CHECK CONDITION: if (result.success) {
  - DO: logger.info('Webhook sent successfully', {
  - FIELD: component: 'CRUDA',
  - FIELD: event: webhookPayload.event,
  - FIELD: requestId: req.requestId
  - DO: })
  - }
  - DO: } catch (error) {
  - NOTE: Webhook failures don't crash the application
  - DO: logger.warn('Webhook delivery failed', {
  - FIELD: component: 'CRUDA',
  - FIELD: event: webhookPayload.event,
  - FIELD: error: error.message,
  - FIELD: requestId: req.requestId
  - DO: })
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Sending Webhooks to Custom Endpoints

Send webhooks to specific endpoints like /hooks/wake or /hooks/agent:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET roditClient TO req.app.locals.roditClient
  - SET webhookPayload TO {
  - FIELD: event: 'heartbeat_request',
  - FIELD: data: {
  - FIELD: timestamp: new Date().toISOString(),
  - FIELD: source: '/api/testhola'
  - }
  - DO: }
  - NOTE: Send to /hooks/wake endpoint (trigger immediate heartbeat)
  - WAIT FOR: roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/wake', req)
  - NOTE: Send to /hooks/agent endpoint (run isolated agent task)
  - WAIT FOR: roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/agent', req)
  - NOTE: Send to custom endpoint
  - WAIT FOR: roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/custom', req)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Convenience Methods for Common Endpoints

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - SET roditClient TO req.app.locals.roditClient
  - SET payload TO {
  - FIELD: event: 'test_event',
  - FIELD: data: { timestamp: new Date().toISOString() }
  - DO: }
  - NOTE: Send to /hooks/wake (heartbeat confirmation)
  - WAIT FOR: roditClient.sendWakeHook(payload, req)
  - NOTE: Send to /hooks/agent (agent task confirmation)
  - WAIT FOR: roditClient.sendAgentHook(payload, req)
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Webhook Endpoint Purposes

| Endpoint | Purpose | Use Case | |----------|---------|----------| | /webhook | Default webhook endpoint | General event notifications | | /hooks/wake | Trigger immediate heartbeat | Enqueue system event for main session | | /hooks/agent | Run isolated agent task | Execute background tasks with optional reply to messaging channels |

Webhook Error Handling

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Graceful webhook handling in CRUDA operations
  - SET logAndSendWebhook TO async (payload, req = null) => {
  - DO: try {
  - SET roditClient TO req?.app?.locals?.roditClient
  - CHECK CONDITION: if (!roditClient) {
  - DO: logger.warn('RoditClient not available, skipping webhook', {
  - FIELD: component: 'CRUDA',
  - FIELD: event: payload?.event
  - DO: })
  - RETURN { success: false, error: 'RoditClient not available' }
  - }
  - RETURN await roditClient.sendWebhook(payload, req)
  - DO: } catch (error) {
  - NOTE: Log but don't throw - webhook failures shouldn't crash the app
  - DO: logger.error('Webhook delivery failed', {
  - FIELD: component: 'CRUDA',
  - FIELD: event: payload?.event,
  - FIELD: error: error.message
  - DO: })
  - RETURN { success: false, error: error.message }
  - }
  - DO: }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Development/Testing Webhooks

The /api/testhola endpoint sends test webhooks in development mode (NODE_ENV === 'development'):

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: Event: testhola_validation_success
  - NOTE: Sent to: /hooks/wake and /hooks/agent (development only)
  - {
  - FIELD: "event": "testhola_validation_success",
  - FIELD: "data": {
  - FIELD: "peerTokenId": "bcdfhjkmnpqr",
  - FIELD: "serverTokenId": "bcdfhjkmnpqr",
  - FIELD: "recipient": "MUNDO",
  - FIELD: "timestamp": "2026-04-24T14:30:00.000Z",
  - FIELD: "endpoint": "/api/testhola"
  - }
  - }
OUTPUTS:
  - Produces the section's intended result using equivalent logic.

Use Case: Test webhook delivery and signature validation during development without needing a main deployment.

Advanced Usage

Route Module Pattern

Create reusable route modules that access the shared RoditClient:

PSEUDOCODE
INPUTS:
  - Use values defined by the surrounding section/context.
STEPS:
  - NOTE: routes/protected.js
  - SET express TO require('express')
  - DO: const { logger } = require('@rodit/rodit-auth-be')
  - SET router TO express.Router()
  - NOTE: Middleware that uses the shared client
  - SET authenticate TO (req, res, next) => {
  - SET client TO req.app.locals.roditClient
  - CHECK CONDITION: if (!client) {
  - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
  - }
  - RETURN client.authenticate(req, res, next)
  - DO: }
  - SET authorize TO (req, res, next) => {
  - SET client TO req.app.locals.roditClient
  - CHECK CONDITION: if (!client) {
  - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
  - }
  - RETURN client.authorize(req, res