@rodit/rodit-auth-be 9.11.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3543 @@
1
+ # RODiT Authentication SDK
2
+
3
+ A comprehensive Node.js SDK for implementing RODiT-based mutual authentication, authorization, self-configuration, and session management in Express.js applications.
4
+
5
+ **Version:** 1.1.0
6
+ **License:** Proprietary
7
+ **Author:** Discernible IO
8
+
9
+ **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](./CHANGELOG.md).
10
+
11
+ ## Table of Contents
12
+
13
+ - [Quick Start](#quick-start)
14
+ - [Core Concepts](#core-concepts)
15
+ - [Installation & Setup](#installation--setup)
16
+ - [Authentication](#authentication)
17
+ - [Login Mode Control](#login-mode-control)
18
+ - [Authorization & Permissions](#authorization--permissions)
19
+ - [Session Management](#session-management)
20
+ - [Session lifetime and TTL](#session-lifetime-and-ttl)
21
+ - [Configuration](#configuration)
22
+ - [Environment Variables](#environment-variables)
23
+ - [Session Storage Configuration](#session-storage-configuration)
24
+ - [Configuration Priority](#configuration-priority)
25
+ - [Logging & Monitoring](#logging--monitoring)
26
+ - [Performance Tracking](#performance-tracking)
27
+ - [Webhooks](#webhooks)
28
+ - [Advanced Usage](#advanced-usage)
29
+ - [Portal Authentication](#portal-authentication-server-to-server)
30
+ - [SignPortal URL Configuration](#signportal-url-configuration)
31
+ - [CRUDA Operations Example](#cruda-operations-example)
32
+ - [API Reference](#api-reference)
33
+ - [Best Practices](#best-practices)
34
+ - [Troubleshooting](#troubleshooting)
35
+
36
+ ## Quick Start
37
+
38
+ ### Installation
39
+
40
+ ```text
41
+ PSEUDOCODE
42
+ INPUTS:
43
+ - Use values defined by the surrounding section/context.
44
+ STEPS:
45
+ - RUN COMMAND: npm install @rodit/rodit-auth-be
46
+ OUTPUTS:
47
+ - Produces the section's intended result using equivalent logic.
48
+ ```
49
+
50
+ ### Basic Server Setup
51
+
52
+ ```text
53
+ PSEUDOCODE
54
+ INPUTS:
55
+ - Use values defined by the surrounding section/context.
56
+ STEPS:
57
+ - SET express TO require('express')
58
+ - DO: const { RoditClient } = require('@rodit/rodit-auth-be')
59
+ - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
60
+ - DO: const { ulid } = require('ulid')
61
+ - SET session TO require('express-session')
62
+ - SET SQLiteStore TO require('connect-sqlite3')(session)
63
+ - SET app TO express()
64
+ - DO: let roditClient
65
+ - NOTE: Configure session storage BEFORE initializing RoditClient
66
+ - SET sessionStore TO new SQLiteStore({
67
+ - FIELD: db: 'sessions.db',
68
+ - FIELD: dir: './data',
69
+ - FIELD: table: 'sessions'
70
+ - DO: })
71
+ - DO: setExpressSessionStore(sessionStore)
72
+ - NOTE: Configure Express middleware
73
+ - DO: app.use(express.json())
74
+ - FIELD: app.use(express.urlencoded({ extended: false }))
75
+ - NOTE: Request context middleware
76
+ - DO: app.use((req, res, next) => {
77
+ - DO: req.requestId = req.headers['x-request-id'] || ulid()
78
+ - DO: req.startTime = Date.now()
79
+ - DO: next()
80
+ - DO: })
81
+ - NOTE: Server startup with SDK initialization
82
+ - DO: async function startServer() {
83
+ - DO: try {
84
+ - NOTE: Initialize RODiT client (use 'server' for server applications)
85
+ - SET roditClient TO await RoditClient.create('server')
86
+ - NOTE: Store client in app.locals for route access
87
+ - DO: app.locals.roditClient = roditClient
88
+ - NOTE: Get logger and other services from client
89
+ - SET logger TO roditClient.getLogger()
90
+ - SET config TO roditClient.getConfig()
91
+ - SET loggingmw TO roditClient.getLoggingMiddleware()
92
+ - NOTE: Apply logging middleware
93
+ - DO: app.use(loggingmw)
94
+ - NOTE: Create authentication middleware
95
+ - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
96
+ - NOTE: Logout-specific auth allows signature-valid expired tokens for clean session closure
97
+ - SET authenticateLogout TO (req, res, next) => roditClient.authenticateForLogout(req, res, next)
98
+ - SET authorize TO (req, res, next) => roditClient.authorize(req, res, next)
99
+ - NOTE: Public routes
100
+ - DO: app.post('/api/login', (req, res) => {
101
+ - DO: req.logAction = 'login-attempt'
102
+ - RETURN roditClient.login_client(req, res)
103
+ - DO: })
104
+ - NOTE: Protected routes
105
+ - DO: app.post('/api/logout', authenticateLogout, (req, res) => {
106
+ - DO: req.logAction = 'logout-attempt'
107
+ - RETURN roditClient.logout_client(req, res)
108
+ - DO: })
109
+ - DO: app.get('/api/protected', authenticate, (req, res) => {
110
+ - FIELD: res.json({ message: 'Protected data', user: req.user })
111
+ - DO: })
112
+ - NOTE: Protected + authorized routes
113
+ - DO: app.use('/api/admin', authenticate, authorize, adminRoutes)
114
+ - SET port TO 3000
115
+ - DO: app.listen(port, () => {
116
+ - DO: logger.info(`RODiT Authentication Server running on port ${port}`)
117
+ - DO: })
118
+ - DO: } catch (error) {
119
+ - FIELD: console.error('Server initialization failed:', error)
120
+ - DO: process.exit(1)
121
+ - }
122
+ - }
123
+ - DO: startServer()
124
+ OUTPUTS:
125
+ - Produces the section's intended result using equivalent logic.
126
+ ```
127
+
128
+ ## Core Concepts
129
+
130
+ ### The RoditClient Pattern
131
+
132
+ The SDK centers around the `RoditClient` class, which provides a unified interface for all RODiT operations:
133
+
134
+ - **Single Initialization**: Create once with `RoditClient.create(role)` where role is `'server'`, `'client'`, or `'portal'`
135
+ - **Shared Instance**: Store in `app.locals` for access across routes and middleware
136
+ - **Self-Configuring**: Automatically loads configuration from Vault, files, or environment variables
137
+ - **Encapsulated**: All SDK functionality accessed through the client instance
138
+ - **Session Management**: Built-in session tracking with pluggable storage backends
139
+ - **Performance Monitoring**: Integrated request tracking and metrics collection
140
+
141
+ ### App.locals Pattern
142
+
143
+ Store the initialized client in `app.locals` for consistent access across your application:
144
+
145
+ ```text
146
+ PSEUDOCODE
147
+ INPUTS:
148
+ - Use values defined by the surrounding section/context.
149
+ STEPS:
150
+ - NOTE: In main app.js
151
+ - SET roditClient TO await RoditClient.create('server')
152
+ - DO: app.locals.roditClient = roditClient
153
+ - NOTE: In route modules
154
+ - SET router TO express.Router()
155
+ - DO: router.get('/data', (req, res) => {
156
+ - SET client TO req.app.locals.roditClient
157
+ - SET logger TO client.getLogger()
158
+ - DO: logger.info('Processing request', {
159
+ - FIELD: component: 'DataRoute',
160
+ - FIELD: userId: req.user?.id
161
+ - DO: })
162
+ - FIELD: res.json({ data: 'example' })
163
+ - DO: })
164
+ OUTPUTS:
165
+ - Produces the section's intended result using equivalent logic.
166
+ ```
167
+
168
+ ### Authentication Middleware Pattern
169
+
170
+ Create middleware functions that delegate to the RoditClient:
171
+
172
+ ```text
173
+ PSEUDOCODE
174
+ INPUTS:
175
+ - Use values defined by the surrounding section/context.
176
+ STEPS:
177
+ - NOTE: Create reusable middleware
178
+ - SET authenticate TO (req, res, next) => {
179
+ - SET client TO req.app.locals.roditClient
180
+ - CHECK CONDITION: if (!client) {
181
+ - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
182
+ - }
183
+ - RETURN client.authenticate(req, res, next)
184
+ - DO: }
185
+ - SET authorize TO (req, res, next) => {
186
+ - SET client TO req.app.locals.roditClient
187
+ - CHECK CONDITION: if (!client) {
188
+ - RETURN res.status(503).json({ error: 'Authorization service unavailable' })
189
+ - }
190
+ - RETURN client.authorize(req, res, next)
191
+ - DO: }
192
+ - NOTE: Use in routes
193
+ - DO: app.get('/api/protected', authenticate, handler)
194
+ - DO: app.post('/api/admin', authenticate, authorize, adminHandler)
195
+ OUTPUTS:
196
+ - Produces the section's intended result using equivalent logic.
197
+ ```
198
+
199
+ ## Installation & Setup
200
+
201
+ ### Dependencies
202
+
203
+ **Required:**
204
+ ```text
205
+ PSEUDOCODE
206
+ INPUTS:
207
+ - Use values defined by the surrounding section/context.
208
+ STEPS:
209
+ - RUN COMMAND: npm install @rodit/rodit-auth-be express config winston
210
+ OUTPUTS:
211
+ - Produces the section's intended result using equivalent logic.
212
+ ```
213
+
214
+ **Recommended for main:**
215
+ ```text
216
+ PSEUDOCODE
217
+ INPUTS:
218
+ - Use values defined by the surrounding section/context.
219
+ STEPS:
220
+ - RUN COMMAND: npm install express-session connect-sqlite3
221
+ OUTPUTS:
222
+ - Produces the section's intended result using equivalent logic.
223
+ ```
224
+
225
+ **Optional:**
226
+ ```text
227
+ PSEUDOCODE
228
+ INPUTS:
229
+ - Use values defined by the surrounding section/context.
230
+ STEPS:
231
+ - RUN COMMAND: npm install node-vault # For Vault-based credentials
232
+ - RUN COMMAND: npm install winston-loki # For Grafana Loki logging
233
+ OUTPUTS:
234
+ - Produces the section's intended result using equivalent logic.
235
+ ```
236
+
237
+ ### Environment Variables
238
+
239
+ **Vault Configuration (main):**
240
+ ```text
241
+ PSEUDOCODE
242
+ INPUTS:
243
+ - Use values defined by the surrounding section/context.
244
+ STEPS:
245
+ - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault
246
+ - FIELD: export VAULT_ENDPOINT=https://vault.example.com
247
+ - DO: export VAULT_ROLE_ID=your-role-id
248
+ - DO: export VAULT_SECRET_ID=your-secret-id
249
+ - DO: export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
250
+ - DO: export SERVICE_NAME=your-service-name
251
+ - DO: export NEAR_CONTRACT_ID=discernible-io.near
252
+ OUTPUTS:
253
+ - Produces the section's intended result using equivalent logic.
254
+ ```
255
+
256
+ **Application Configuration:**
257
+ ```text
258
+ PSEUDOCODE
259
+ INPUTS:
260
+ - Use values defined by the surrounding section/context.
261
+ STEPS:
262
+ - FIELD: export NODE_ENV=main # Environment: main, development, test
263
+ - FIELD: export LOG_LEVEL=info # Logging: error, warn, info, debug, trace
264
+ - DO: export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite
265
+ OUTPUTS:
266
+ - Produces the section's intended result using equivalent logic.
267
+ ```
268
+
269
+ **Session Configuration:**
270
+ ```text
271
+ PSEUDOCODE
272
+ INPUTS:
273
+ - Use values defined by the surrounding section/context.
274
+ STEPS:
275
+ - FIELD: export SESSION_STORAGE_TYPE=express-session # Storage: memory, express, express-session
276
+ - DO: export SESSION_CLEANUP_INTERVAL=3600000 # Cleanup interval in milliseconds (1 hour)
277
+ - DO: export SESSION_TOKEN_RETENTION_PERIOD=604800 # Token retention in seconds (7 days)
278
+ - DO: export SESSION_VALIDATION_CACHE_TTL=5000 # Cache TTL in milliseconds (5 seconds)
279
+ OUTPUTS:
280
+ - Produces the section's intended result using equivalent logic.
281
+ ```
282
+
283
+ **Logging Configuration:**
284
+ ```text
285
+ PSEUDOCODE
286
+ INPUTS:
287
+ - Use values defined by the surrounding section/context.
288
+ STEPS:
289
+ - FIELD: export LOKI_URL=https://loki.example.com:3100
290
+ - FIELD: export LOKI_BASIC_AUTH=username:password
291
+ OUTPUTS:
292
+ - Produces the section's intended result using equivalent logic.
293
+ ```
294
+
295
+ ### Configuration Files
296
+
297
+ Create `config/default.json`:
298
+
299
+ ```text
300
+ PSEUDOCODE
301
+ INPUTS:
302
+ - Use values defined by the surrounding section/context.
303
+ STEPS:
304
+ - {
305
+ - FIELD: "NEAR_CONTRACT_ID": "discernible-io.near",
306
+ - FIELD: "SERVICE_NAME": "your-service",
307
+ - FIELD: "SECURITY_OPTIONS": {
308
+ - FIELD: "SILENT_LOGIN_FAILURES": false,
309
+ - FIELD: "SESSION_TTL_SECONDS": 5200 // server session lifetime from login (default in SDK)
310
+ - FIELD: "FALLBACK_JWT_DURATION": 3600 // access-token fallback when passport jwt_duration is invalid
311
+ - }
312
+ - }
313
+ OUTPUTS:
314
+ - Produces the section's intended result using equivalent logic.
315
+ ```
316
+
317
+ ## Authentication
318
+
319
+ ### RODiT-Based Authentication
320
+
321
+ RODiT provides cryptographic mutual authentication using blockchain-verified identities.
322
+
323
+ #### Client Login Request
324
+
325
+ For API login documentation, use **`accountid`** with HTTP `POST /api/login`. The signed payload is **`accountid + timestamp_iso`** (no separator).
326
+
327
+ | Field | Description |
328
+ |-------|-------------|
329
+ | `timestamp` | Recommended; Unix seconds from `GET /api/login/timestamp` |
330
+ | `base64url_signature` | Ed25519 detached signature (base64url) over `accountid + timestamp_iso` |
331
+ | `accountid` | 64-hex implicit NEAR account login identifier |
332
+
333
+ ```text
334
+ PSEUDOCODE
335
+ INPUTS:
336
+ - Use values defined by the surrounding section/context.
337
+ STEPS:
338
+ - NOTE: Implicit account login
339
+ - {
340
+ - FIELD: "accountid": "<64-char-hex>",
341
+ - FIELD: "timestamp": 1640995200,
342
+ - FIELD: "base64url_signature": "base64url-encoded-signature"
343
+ - }
344
+ OUTPUTS:
345
+ - Produces the section's intended result using equivalent logic.
346
+ ```
347
+
348
+ Use **`base64url_signature`** in login payloads for API login examples.
349
+
350
+ Rejected keys (HTTP 400, `LOGIN_PAYLOAD_DEPRECATED`): **`signature`** and **`account_id`**.
351
+
352
+ #### Server Response
353
+
354
+ ```text
355
+ PSEUDOCODE
356
+ INPUTS:
357
+ - Use values defined by the surrounding section/context.
358
+ STEPS:
359
+ - NOTE: Success (200)
360
+ - {
361
+ - FIELD: "jwt_token": "<jwt-token>",
362
+ - FIELD: "requestId": "01HQXYZ123ABC"
363
+ - }
364
+ - NOTE: Headers:
365
+ - NOTE: New-Token: <jwt> (same token echoed for header-based clients)
366
+ OUTPUTS:
367
+ - Produces the section's intended result using equivalent logic.
368
+ ```
369
+
370
+ #### Authentication Flow
371
+
372
+ 1. **Client sends RODiT credentials** - RODiT ID, timestamp, and cryptographic signature
373
+ 2. **SDK verifies signature** - Validates against blockchain records (NEAR Protocol)
374
+ 3. **Session created** - New session stored in session manager
375
+ 4. **JWT token issued** - Token contains session ID and user claims
376
+ 5. **Subsequent requests** - Client sends JWT in `Authorization: Bearer <token>` header
377
+ 6. **Token validation** - SDK validates JWT and checks session status
378
+
379
+ Security hardening in current implementation:
380
+ - JWT compact parts must be canonical base64url (non-canonical encodings are rejected).
381
+ - Session registration is enforced during JWT validation (unknown/inactive/expired sessions are rejected).
382
+ - Server session length defaults to `SECURITY_OPTIONS.SESSION_TTL_SECONDS` (5200 s); see [Session lifetime and TTL](#session-lifetime-and-ttl).
383
+ - Token renewal uses `sessionManager` for session checks and updates (no `stateManager` session mutations).
384
+
385
+ ### Login Implementation
386
+
387
+ ```text
388
+ PSEUDOCODE
389
+ INPUTS:
390
+ - Use values defined by the surrounding section/context.
391
+ STEPS:
392
+ - NOTE: routes/login.js
393
+ - SET express TO require('express')
394
+ - SET router TO express.Router()
395
+ - DO: router.post('/login', async (req, res) => {
396
+ - DO: req.logAction = 'login-attempt'
397
+ - SET client TO req.app.locals.roditClient
398
+ - CHECK CONDITION: if (!client) {
399
+ - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
400
+ - }
401
+ - NOTE: Delegate to SDK's login_client method
402
+ - WAIT FOR: client.login_client(req, res)
403
+ - DO: })
404
+ - DO: module.exports = router
405
+ OUTPUTS:
406
+ - Produces the section's intended result using equivalent logic.
407
+ ```
408
+
409
+ ### Logout Implementation
410
+
411
+ ```text
412
+ PSEUDOCODE
413
+ INPUTS:
414
+ - Use values defined by the surrounding section/context.
415
+ STEPS:
416
+ - NOTE: Logout invalidates the JWT token and closes the session
417
+ - NOTE: Use logout-specific auth so signature-valid expired tokens can still logout.
418
+ - DO: router.post('/logout', authenticateLogout, async (req, res) => {
419
+ - DO: req.logAction = 'logout-attempt'
420
+ - SET client TO req.app.locals.roditClient
421
+ - CHECK CONDITION: if (!client) {
422
+ - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
423
+ - }
424
+ - NOTE: Delegate to SDK's logout_client method
425
+ - WAIT FOR: client.logout_client(req, res)
426
+ - DO: })
427
+ OUTPUTS:
428
+ - Produces the section's intended result using equivalent logic.
429
+ ```
430
+
431
+ ### Protected Routes
432
+
433
+ ```text
434
+ PSEUDOCODE
435
+ INPUTS:
436
+ - Use values defined by the surrounding section/context.
437
+ STEPS:
438
+ - NOTE: Require authentication for access
439
+ - DO: app.get('/api/data', authenticate, (req, res) => {
440
+ - NOTE: req.user contains authenticated user information
441
+ - SET logger TO req.app.locals.roditClient.getLogger()
442
+ - DO: logger.info('Protected route accessed', {
443
+ - FIELD: component: 'API',
444
+ - FIELD: userId: req.user.id,
445
+ - FIELD: roditId: req.user.roditId,
446
+ - FIELD: requestId: req.requestId
447
+ - DO: })
448
+ - DO: res.json({
449
+ - FIELD: message: 'Authenticated data',
450
+ - FIELD: user: req.user,
451
+ - FIELD: requestId: req.requestId
452
+ - DO: })
453
+ - DO: })
454
+ OUTPUTS:
455
+ - Produces the section's intended result using equivalent logic.
456
+ ```
457
+
458
+ ### Authentication Middleware
459
+
460
+ The `authenticate` middleware validates JWT tokens and populates `req.user`:
461
+
462
+ ```text
463
+ PSEUDOCODE
464
+ INPUTS:
465
+ - Use values defined by the surrounding section/context.
466
+ STEPS:
467
+ - SET authenticate TO (req, res, next) => {
468
+ - SET client TO req.app.locals.roditClient
469
+ - RETURN client.authenticate(req, res, next)
470
+ - DO: }
471
+ - NOTE: After successful authentication, req.user contains:
472
+ - NOTE: {
473
+ - NOTE: id: 'user-unique-id',
474
+ - NOTE: roditId: '01K4G3D95QF6NR0RSJK9WEK6KA',
475
+ - NOTE: aud: 'audience',
476
+ - NOTE: iss: 'issuer',
477
+ - NOTE: exp: 1640999999,
478
+ - NOTE: iat: 1640995200,
479
+ - NOTE: session_id: '01HQXYZ123ABC'
480
+ - NOTE: }
481
+ OUTPUTS:
482
+ - Produces the section's intended result using equivalent logic.
483
+ ```
484
+
485
+ ### Login Mode Control
486
+
487
+ The SDK provides configurable access control for RODiT authentication, allowing you to restrict which types of logins are accepted by your server.
488
+
489
+ #### Login Types
490
+
491
+ **Partner Login (Client-Server)**
492
+ - **Definition**: Authentication where the peer's service provider ID is **different** from the server's service provider ID
493
+ - **Use Case**: Traditional client-server authentication where a client authenticates to a service provider
494
+ - **Example**: A mobile app (client) authenticating to your API server
495
+
496
+ **Peer Login (Peer-to-Peer)**
497
+ - **Definition**: Authentication where the peer's service provider ID is **the same** as the server's service provider ID
498
+ - **Use Case**: Peer-to-peer authentication between entities with the same service provider
499
+ - **Example**: Two servers in the same organization authenticating to each other
500
+
501
+ #### Configuration Options
502
+
503
+ | Mode | Partner Logins | Peer Logins | Description |
504
+ |------|---------------|-------------|-------------|
505
+ | `partner` | ✅ Accepted | ❌ Rejected | **Default** - Only accept client-server authentication |
506
+ | `promiscuous` | ✅ Accepted | ✅ Accepted | Accept all valid logins regardless of type |
507
+ | `p2p` | ❌ Rejected | ✅ Accepted | Only accept peer-to-peer authentication |
508
+
509
+ #### Usage Examples
510
+
511
+ **Default (Partner Only):**
512
+ ```text
513
+ PSEUDOCODE
514
+ INPUTS:
515
+ - Use values defined by the surrounding section/context.
516
+ STEPS:
517
+ - NOTE: No configuration needed - this is the default
518
+ - NOTE: Only client-server authentication is accepted
519
+ OUTPUTS:
520
+ - Produces the section's intended result using equivalent logic.
521
+ ```
522
+
523
+ **Accept All Logins:**
524
+ ```text
525
+ PSEUDOCODE
526
+ INPUTS:
527
+ - Use values defined by the surrounding section/context.
528
+ STEPS:
529
+ - DO: export SECURITY_OPTIONS_LOGIN_MODE=promiscuous
530
+ - NOTE: Both Partner and Peer logins are accepted
531
+ OUTPUTS:
532
+ - Produces the section's intended result using equivalent logic.
533
+ ```
534
+
535
+ **Peer-to-Peer Only:**
536
+ ```text
537
+ PSEUDOCODE
538
+ INPUTS:
539
+ - Use values defined by the surrounding section/context.
540
+ STEPS:
541
+ - DO: export SECURITY_OPTIONS_LOGIN_MODE=p2p
542
+ - NOTE: Only peer-to-peer authentication is accepted
543
+ OUTPUTS:
544
+ - Produces the section's intended result using equivalent logic.
545
+ ```
546
+
547
+ **Docker/Podman:**
548
+ ```text
549
+ PSEUDOCODE
550
+ INPUTS:
551
+ - Use values defined by the surrounding section/context.
552
+ STEPS:
553
+ - DO: podman run -e SECURITY_OPTIONS_LOGIN_MODE=partner ...
554
+ OUTPUTS:
555
+ - Produces the section's intended result using equivalent logic.
556
+ ```
557
+
558
+ **GitHub Actions:**
559
+ Add repository variable:
560
+ - **Name**: `SECURITY_OPTIONS_LOGIN_MODE`
561
+ - **Value**: `partner` | `promiscuous` | `p2p`
562
+
563
+ #### Logging and Monitoring
564
+
565
+ **Successful Login:**
566
+ ```text
567
+ PSEUDOCODE
568
+ INPUTS:
569
+ - Use values defined by the surrounding section/context.
570
+ STEPS:
571
+ - {
572
+ - FIELD: "level": "info",
573
+ - FIELD: "message": "PARTNER login verified successfully",
574
+ - FIELD: "verificationType": "PARTNER",
575
+ - FIELD: "loginMode": "partner",
576
+ - FIELD: "duration": 1234
577
+ - }
578
+ OUTPUTS:
579
+ - Produces the section's intended result using equivalent logic.
580
+ ```
581
+
582
+ **Rejected Login:**
583
+ ```text
584
+ PSEUDOCODE
585
+ INPUTS:
586
+ - Use values defined by the surrounding section/context.
587
+ STEPS:
588
+ - {
589
+ - FIELD: "level": "warn",
590
+ - FIELD: "message": "PEER login rejected by LOGIN_MODE policy",
591
+ - FIELD: "verificationType": "PEER",
592
+ - FIELD: "loginMode": "partner",
593
+ - FIELD: "policyReason": "LOGIN_MODE=partner does not accept PEER logins"
594
+ - }
595
+ OUTPUTS:
596
+ - Produces the section's intended result using equivalent logic.
597
+ ```
598
+
599
+ **Metrics:**
600
+ - `rodit_match_verification` with `result: "success"` - Successful authentication
601
+ - `rodit_match_verification` with `result: "policy_rejected"` - Rejected by policy
602
+
603
+ #### Security Considerations
604
+
605
+ 1. **Default is Secure**: The default `partner` mode provides the most restrictive access control
606
+ 2. **Promiscuous Mode**: Use only when you need to accept both types of authentication
607
+ 3. **P2P Mode**: Use when building peer-to-peer systems where only same-provider authentication is needed
608
+ 4. **Policy Enforcement**: Rejections are logged with clear reasons for audit trails
609
+
610
+ #### Troubleshooting
611
+
612
+ **Login Rejected with "policy_rejected":**
613
+ - If you see "PEER login rejected" and need to accept peer logins, set mode to `promiscuous` or `p2p`
614
+ - If you see "PARTNER login rejected" and need to accept partner logins, set mode to `promiscuous` or `partner`
615
+
616
+ **Check Current Mode:**
617
+ Look for the log message during authentication:
618
+ ```
619
+ "Starting RODiT match verification" with "loginMode": "partner"
620
+ ```
621
+
622
+ ## Authorization & Permissions
623
+
624
+ ### Route-Based Permissions
625
+
626
+ Permissions are configured in your RODiT token metadata using the `permissioned_routes` field:
627
+
628
+ ```text
629
+ PSEUDOCODE
630
+ INPUTS:
631
+ - Use values defined by the surrounding section/context.
632
+ STEPS:
633
+ - {
634
+ - FIELD: "permissioned_routes": {
635
+ - FIELD: "entities": {
636
+ - FIELD: "/": {
637
+ - FIELD: "methods": "+0"
638
+ - DO: },
639
+ - FIELD: "/api/echo": {
640
+ - FIELD: "methods": "+0"
641
+ - DO: },
642
+ - FIELD: "/api/cruda/create": {
643
+ - FIELD: "methods": "+0"
644
+ - DO: },
645
+ - FIELD: "/api/cruda/list": {
646
+ - FIELD: "methods": "+0"
647
+ - DO: },
648
+ - FIELD: "/api/admin": {
649
+ - FIELD: "methods": "+0"
650
+ - }
651
+ - }
652
+ - }
653
+ - }
654
+ OUTPUTS:
655
+ - Produces the section's intended result using equivalent logic.
656
+ ```
657
+
658
+ **Permission Format:**
659
+ - `"+0"` = All methods allowed (GET, POST, PUT, DELETE, etc.)
660
+ - `"+1"` = GET only
661
+ - `"+2"` = POST only
662
+ - Custom combinations can be defined
663
+
664
+ ### Permission Validation Middleware
665
+
666
+ The `authorize` middleware validates that the authenticated user has permission to access the requested route:
667
+
668
+ ```text
669
+ PSEUDOCODE
670
+ INPUTS:
671
+ - Use values defined by the surrounding section/context.
672
+ STEPS:
673
+ - SET authenticate TO (req, res, next) => {
674
+ - RETURN req.app.locals.roditClient.authenticate(req, res, next)
675
+ - DO: }
676
+ - SET authorize TO (req, res, next) => {
677
+ - RETURN req.app.locals.roditClient.authorize(req, res, next)
678
+ - DO: }
679
+ - NOTE: Apply both authentication and authorization
680
+ - DO: app.use('/api/admin', authenticate, authorize, adminRoutes)
681
+ - NOTE: CRUDA endpoints with full protection
682
+ - DO: app.use('/api/cruda', authenticate, authorize, crudaRoutes)
683
+ OUTPUTS:
684
+ - Produces the section's intended result using equivalent logic.
685
+ ```
686
+
687
+ ### Permission Enforcement
688
+
689
+ ```text
690
+ PSEUDOCODE
691
+ INPUTS:
692
+ - Use values defined by the surrounding section/context.
693
+ STEPS:
694
+ - NOTE: Example: CRUDA routes with permission checking
695
+ - SET router TO express.Router()
696
+ - NOTE: All routes require authentication + authorization
697
+ - DO: router.post('/create', async (req, res) => {
698
+ - NOTE: User must have permission for POST /api/cruda/create
699
+ - DO: const { comment, author } = req.body
700
+ - NOTE: Create record in database
701
+ - SET result TO await db.run(
702
+ - DO: 'INSERT INTO comments (comment, author) VALUES (?, ?)',
703
+ - DO: [comment, author || req.user.roditId]
704
+ - DO: )
705
+ - FIELD: res.json({ id: result.lastID, requestId: req.requestId })
706
+ - DO: })
707
+ - DO: router.post('/list', async (req, res) => {
708
+ - NOTE: User must have permission for POST /api/cruda/list
709
+ - SET records TO await db.all('SELECT * FROM comments ORDER BY created_at DESC')
710
+ - FIELD: res.json({ records, requestId: req.requestId })
711
+ - DO: })
712
+ - DO: module.exports = router
713
+ OUTPUTS:
714
+ - Produces the section's intended result using equivalent logic.
715
+ ```
716
+
717
+ ### Dynamic Permission Checking
718
+
719
+ ```text
720
+ PSEUDOCODE
721
+ INPUTS:
722
+ - Use values defined by the surrounding section/context.
723
+ STEPS:
724
+ - NOTE: Check permissions programmatically
725
+ - SET client TO req.app.locals.roditClient
726
+ - SET hasPermission TO client.isOperationPermitted('POST', '/api/admin/users')
727
+ - CHECK CONDITION: if (!hasPermission) {
728
+ - RETURN res.status(403).json({
729
+ - FIELD: error: 'Forbidden',
730
+ - FIELD: message: 'You do not have permission to access this resource',
731
+ - FIELD: requestId: req.requestId
732
+ - DO: })
733
+ - }
734
+ - NOTE: Proceed with operation
735
+ OUTPUTS:
736
+ - Produces the section's intended result using equivalent logic.
737
+ ```
738
+
739
+ ### Permission Validation in Client Token Minting
740
+
741
+ When minting client tokens via `/api/signclient`, the server validates that requested permissions are a subset of the server's own permissions:
742
+
743
+ ```text
744
+ PSEUDOCODE
745
+ INPUTS:
746
+ - Use values defined by the surrounding section/context.
747
+ STEPS:
748
+ - NOTE: Client requests these permissions:
749
+ - SET requestedPermissions TO {
750
+ - FIELD: "/": "+0",
751
+ - FIELD: "/api/echo": "+0",
752
+ - FIELD: "/api/cruda/create": "+0"
753
+ - DO: }
754
+ - NOTE: Server validates against its own permissioned_routes
755
+ - NOTE: If any requested route is not in server's config, request is rejected with HTTP 400
756
+ OUTPUTS:
757
+ - Produces the section's intended result using equivalent logic.
758
+ ```
759
+
760
+ ## Session Management
761
+
762
+ ### Overview
763
+
764
+ The SDK includes a comprehensive session management system that:
765
+ - Tracks active user sessions
766
+ - Validates JWT tokens against session state
767
+ - Supports pluggable storage backends
768
+ - Automatically cleans up expired sessions
769
+ - Integrates with performance metrics
770
+
771
+ ### Session lifetime and TTL
772
+
773
+ Server sessions and JWT access credentials use **different** clocks:
774
+
775
+ | Concept | Controlled by | Stored / carried as |
776
+ |---------|----------------|---------------------|
777
+ | **Server session** | `SECURITY_OPTIONS.SESSION_TTL_SECONDS` (host config) | `sessionManager` record `expiresAt`; JWT claim `session_exp` |
778
+ | **Access credential (JWT `exp`)** | Passport `jwt_duration` on peer/own RODiT metadata (+ renewal) | JWT `exp`; renewed until `session_exp` |
779
+
780
+ 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.
781
+
782
+ #### `SECURITY_OPTIONS.SESSION_TTL_SECONDS`
783
+
784
+ | Property | Value |
785
+ |----------|--------|
786
+ | **SDK default** | `5200` (~87 minutes) |
787
+ | **Valid range** | `60` – `31536000` (365 days), or `0` to disable |
788
+ | **Config path** | `SECURITY_OPTIONS.SESSION_TTL_SECONDS` |
789
+ | **Env example** | `SECURITY_OPTIONS_SESSION_TTL_SECONDS=2592000` (30 days, with node-config style mapping) |
790
+
791
+ At login the SDK computes:
792
+
793
+ ```text
794
+ session_expiresAt = login_time + SESSION_TTL_SECONDS
795
+ ```
796
+
797
+ 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.
798
+
799
+ 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`).
800
+
801
+ #### Examples
802
+
803
+ **Default (5200 seconds):**
804
+
805
+ ```javascript
806
+ // config/default.json — omit SESSION_TTL_SECONDS to use SDK default 5200
807
+ {
808
+ "SECURITY_OPTIONS": {
809
+ "FALLBACK_JWT_DURATION": 3600
810
+ }
811
+ }
812
+ ```
813
+
814
+ **30-day sessions:**
815
+
816
+ ```javascript
817
+ {
818
+ "SECURITY_OPTIONS": {
819
+ "SESSION_TTL_SECONDS": 2592000
820
+ }
821
+ }
822
+ ```
823
+
824
+ **Passport-derived session length (legacy):**
825
+
826
+ ```javascript
827
+ {
828
+ "SECURITY_OPTIONS": {
829
+ "SESSION_TTL_SECONDS": 0
830
+ }
831
+ }
832
+ ```
833
+
834
+ #### Enforcement on each API request
835
+
836
+ For normal API authentication (`authenticate_apicall`), the SDK:
837
+
838
+ 1. Checks stored session: exists, `status === 'active'`, `expiresAt` not in the past.
839
+ 2. Validates JWT signature and `exp` (with renewal when eligible).
840
+ 3. Requires JWT `session_exp` to match stored `expiresAt` when session registration is enforced.
841
+
842
+ Portal/outbound login token validation can skip session registration when `SECURITY_OPTIONS.RELAXED_SESSION_VALIDATION` is `true` (default).
843
+
844
+ #### Related options
845
+
846
+ | Option | Purpose |
847
+ |--------|---------|
848
+ | `FALLBACK_JWT_DURATION` | Access-token lifetime when passport `jwt_duration` is missing or invalid (default `3600`; max 7 days in validator) |
849
+ | `JWT_MAX_DURATION_SECONDS_RODIT_UNBOUNDED` | Cap on JWT `exp` when peer `not_after` is unbounded |
850
+ | `RELAXED_SESSION_VALIDATION` | Portal/outbound flows may skip server session lookup |
851
+ | `SESSION_VALIDATION_CACHE_TTL` | Cache TTL for session invalidation checks after logout |
852
+
853
+ ### Session Storage Backends
854
+
855
+ #### 1. In-Memory Storage (Default)
856
+
857
+ No configuration needed - works out of the box:
858
+
859
+ ```text
860
+ PSEUDOCODE
861
+ INPUTS:
862
+ - Use values defined by the surrounding section/context.
863
+ STEPS:
864
+ - SET client TO await RoditClient.create('server')
865
+ - NOTE: Uses InMemorySessionStorage by default
866
+ OUTPUTS:
867
+ - Produces the section's intended result using equivalent logic.
868
+ ```
869
+
870
+ **Pros:** Fast, zero configuration
871
+ **Cons:** Sessions lost on server restart, not suitable for multi-server deployments
872
+
873
+ #### 2. SQLite Storage (Recommended for main)
874
+
875
+ Persistent storage using SQLite database:
876
+
877
+ ```text
878
+ PSEUDOCODE
879
+ INPUTS:
880
+ - Use values defined by the surrounding section/context.
881
+ STEPS:
882
+ - SET express TO require('express')
883
+ - SET session TO require('express-session')
884
+ - SET SQLiteStore TO require('connect-sqlite3')(session)
885
+ - DO: const { RoditClient } = require('@rodit/rodit-auth-be')
886
+ - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
887
+ - NOTE: Configure BEFORE initializing RoditClient
888
+ - SET sessionStore TO new SQLiteStore({
889
+ - FIELD: db: 'sessions.db',
890
+ - FIELD: dir: './data',
891
+ - FIELD: table: 'sessions'
892
+ - DO: })
893
+ - DO: setExpressSessionStore(sessionStore)
894
+ - NOTE: Now initialize client
895
+ - SET client TO await RoditClient.create('server')
896
+ OUTPUTS:
897
+ - Produces the section's intended result using equivalent logic.
898
+ ```
899
+
900
+ **Pros:** Persistent across restarts, simple setup, uses existing database infrastructure
901
+ **Cons:** Not suitable for multi-server deployments
902
+
903
+ #### 3. Redis Storage (For Multi-Server)
904
+
905
+ ```text
906
+ PSEUDOCODE
907
+ INPUTS:
908
+ - Use values defined by the surrounding section/context.
909
+ STEPS:
910
+ - RUN COMMAND: npm install express-session connect-redis redis
911
+ OUTPUTS:
912
+ - Produces the section's intended result using equivalent logic.
913
+ ```
914
+
915
+ ```text
916
+ PSEUDOCODE
917
+ INPUTS:
918
+ - Use values defined by the surrounding section/context.
919
+ STEPS:
920
+ - SET session TO require('express-session')
921
+ - SET RedisStore TO require('connect-redis').default
922
+ - DO: const { createClient } = require('redis')
923
+ - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
924
+ - NOTE: Create Redis client
925
+ - SET redisClient TO createClient({
926
+ - FIELD: url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
927
+ - DO: })
928
+ - WAIT FOR: redisClient.connect()
929
+ - NOTE: Create Redis store
930
+ - SET redisStore TO new RedisStore({
931
+ - FIELD: client: redisClient,
932
+ - FIELD: prefix: 'rodit:sess:',
933
+ - FIELD: ttl: 86400 // 24 hours
934
+ - DO: })
935
+ - DO: setExpressSessionStore(redisStore)
936
+ - SET client TO await RoditClient.create('server')
937
+ OUTPUTS:
938
+ - Produces the section's intended result using equivalent logic.
939
+ ```
940
+
941
+ **Pros:** Shared sessions across multiple servers, high performance
942
+ **Cons:** Requires Redis infrastructure
943
+
944
+ ### Session Storage Configuration
945
+
946
+ The SDK supports configurable session storage via the `SESSION_STORAGE_TYPE` environment variable.
947
+
948
+ #### Storage Type Options
949
+
950
+ **1. `"memory"` (Default)**
951
+ - Uses SDK's standalone `InMemorySessionStorage`
952
+ - No external dependencies required
953
+ - Sessions stored in JavaScript `Map`
954
+ - Sessions lost on server restart
955
+ - Suitable for development or single-instance deployments
956
+
957
+ ```text
958
+ PSEUDOCODE
959
+ INPUTS:
960
+ - Use values defined by the surrounding section/context.
961
+ STEPS:
962
+ - DO: export SESSION_STORAGE_TYPE=memory
963
+ OUTPUTS:
964
+ - Produces the section's intended result using equivalent logic.
965
+ ```
966
+
967
+ **2. `"express"` or `"express-session"`**
968
+ - Uses `express-session` compatible stores
969
+ - Requires `express-session` to be installed
970
+ - Defaults to `express-session` MemoryStore
971
+ - Can be overridden with `setExpressSessionStore()` for Redis, SQLite, etc.
972
+ - Suitable for main with persistent storage
973
+
974
+ ```text
975
+ PSEUDOCODE
976
+ INPUTS:
977
+ - Use values defined by the surrounding section/context.
978
+ STEPS:
979
+ - DO: export SESSION_STORAGE_TYPE=express-session
980
+ OUTPUTS:
981
+ - Produces the section's intended result using equivalent logic.
982
+ ```
983
+
984
+ #### Configuring Persistent Storage
985
+
986
+ **SQLite Example:**
987
+ ```text
988
+ PSEUDOCODE
989
+ INPUTS:
990
+ - Use values defined by the surrounding section/context.
991
+ STEPS:
992
+ - SET session TO require('express-session')
993
+ - SET SQLiteStore TO require('connect-sqlite3')(session)
994
+ - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
995
+ - NOTE: Configure BEFORE initializing RoditClient
996
+ - SET sessionStore TO new SQLiteStore({
997
+ - FIELD: db: 'sessions.db',
998
+ - FIELD: dir: './data',
999
+ - FIELD: table: 'sessions'
1000
+ - DO: })
1001
+ - DO: setExpressSessionStore(sessionStore)
1002
+ - NOTE: Now initialize client
1003
+ - SET client TO await RoditClient.create('server')
1004
+ OUTPUTS:
1005
+ - Produces the section's intended result using equivalent logic.
1006
+ ```
1007
+
1008
+ **Redis Example:**
1009
+ ```text
1010
+ PSEUDOCODE
1011
+ INPUTS:
1012
+ - Use values defined by the surrounding section/context.
1013
+ STEPS:
1014
+ - SET session TO require('express-session')
1015
+ - SET RedisStore TO require('connect-redis').default
1016
+ - DO: const { createClient } = require('redis')
1017
+ - DO: const { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')
1018
+ - SET redisClient TO createClient({
1019
+ - FIELD: url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
1020
+ - DO: })
1021
+ - WAIT FOR: redisClient.connect()
1022
+ - SET redisStore TO new RedisStore({
1023
+ - FIELD: client: redisClient,
1024
+ - FIELD: prefix: 'rodit:sess:',
1025
+ - FIELD: ttl: 86400
1026
+ - DO: })
1027
+ - DO: setExpressSessionStore(redisStore)
1028
+ OUTPUTS:
1029
+ - Produces the section's intended result using equivalent logic.
1030
+ ```
1031
+
1032
+ #### Session Configuration Variables
1033
+
1034
+ ```text
1035
+ PSEUDOCODE
1036
+ INPUTS:
1037
+ - Use values defined by the surrounding section/context.
1038
+ STEPS:
1039
+ - NOTE: Storage backend type
1040
+ - DO: export SESSION_STORAGE_TYPE=express-session
1041
+ - NOTE: Cleanup interval (milliseconds) - how often to remove expired sessions
1042
+ - DO: export SESSION_CLEANUP_INTERVAL=3600000 # 1 hour
1043
+ - NOTE: Token retention period (seconds) - how long to keep closed sessions
1044
+ - DO: export SESSION_TOKEN_RETENTION_PERIOD=604800 # 7 days
1045
+ - NOTE: Validation cache TTL (milliseconds) - trades security for performance
1046
+ - NOTE: Lower = more secure but more storage lookups
1047
+ - NOTE: Higher = faster but longer window after logout where token may still work
1048
+ - NOTE: Set to 0 to disable caching (always check session state)
1049
+ - DO: export SESSION_VALIDATION_CACHE_TTL=5000 # 5 seconds
1050
+ OUTPUTS:
1051
+ - Produces the section's intended result using equivalent logic.
1052
+ ```
1053
+
1054
+ **Session Validation Cache:**
1055
+
1056
+ The SDK caches token validation results to reduce storage lookups:
1057
+
1058
+ - **Enabled by default** with 5-second TTL
1059
+ - **Trade-off**: Performance vs. security
1060
+ - **After logout**: Cache is immediately invalidated for that session
1061
+ - **Recommendation**: Keep default (5s) for most use cases
1062
+ - **High security**: Set to `0` to disable caching
1063
+
1064
+ ```text
1065
+ PSEUDOCODE
1066
+ INPUTS:
1067
+ - Use values defined by the surrounding section/context.
1068
+ STEPS:
1069
+ - NOTE: Get cache statistics
1070
+ - SET sessionManager TO roditClient.getSessionManager()
1071
+ - SET cacheStats TO sessionManager.getValidationCacheStats()
1072
+ - FIELD: console.log('Cache stats:', cacheStats)
1073
+ - NOTE: Output: { totalEntries: 10, validEntries: 8, expiredEntries: 2, cacheTTL: 5000, cacheEnabled: true }
1074
+ OUTPUTS:
1075
+ - Produces the section's intended result using equivalent logic.
1076
+ ```
1077
+
1078
+ ### Session Operations
1079
+
1080
+ ```text
1081
+ PSEUDOCODE
1082
+ INPUTS:
1083
+ - Use values defined by the surrounding section/context.
1084
+ STEPS:
1085
+ - NOTE: Get session manager
1086
+ - SET sessionManager TO roditClient.getSessionManager()
1087
+ - NOTE: Get active session count
1088
+ - SET activeCount TO await sessionManager.getActiveSessionCount()
1089
+ - NOTE: Get storage information
1090
+ - SET storageInfo TO await sessionManager.getStorageInfo()
1091
+ - FIELD: console.log('Storage type:', storageInfo.type)
1092
+ - FIELD: console.log('Session count:', storageInfo.sessionCount)
1093
+ - NOTE: Enumerate sessions via storage
1094
+ - SET allSessions TO await sessionManager.storage.getAll()
1095
+ - NOTE: Or fallback using keys() + get()
1096
+ - SET sessionIds TO await sessionManager.storage.keys()
1097
+ - SET sessions TO []
1098
+ - REPEAT: for (const id of sessionIds) {
1099
+ - SET session TO await sessionManager.storage.get(id)
1100
+ - CHECK CONDITION: if (session) sessions.push(session)
1101
+ - }
1102
+ - NOTE: Check if token is invalidated
1103
+ - SET isInvalidated TO await sessionManager.isTokenInvalidated(jwtToken)
1104
+ - NOTE: Get detailed invalidation info
1105
+ - SET invalidationInfo TO await sessionManager.getTokenInvalidationInfo(jwtToken)
1106
+ - CHECK CONDITION: if (invalidationInfo) {
1107
+ - FIELD: console.log('Invalidation reason:', invalidationInfo.reason)
1108
+ - FIELD: console.log('Invalidated at:', invalidationInfo.invalidatedAt)
1109
+ - }
1110
+ - NOTE: Manually close a session
1111
+ - WAIT FOR: sessionManager.closeSession(sessionId, 'admin_action')
1112
+ - NOTE: Run manual cleanup (removes expired sessions)
1113
+ - SET cleanup TO await sessionManager.runManualCleanup()
1114
+ - DO: console.log(`Removed ${cleanup.removedSessionsCount} expired sessions`)
1115
+ - NOTE: Get validation cache statistics
1116
+ - SET cacheStats TO sessionManager.getValidationCacheStats()
1117
+ - FIELD: console.log('Cache entries:', cacheStats.totalEntries)
1118
+ - FIELD: console.log('Cache TTL:', cacheStats.cacheTTL)
1119
+ OUTPUTS:
1120
+ - Produces the section's intended result using equivalent logic.
1121
+ ```
1122
+
1123
+ ### Session Lifecycle
1124
+
1125
+ 1. **Login** - Session created, JWT token issued with session ID
1126
+ 2. **Active** - Token validated on each request, session last_accessed updated
1127
+ 3. **Logout** - Session closed, token invalidated, termination token issued
1128
+ 4. **Expiration** - Sessions expire when stored `expiresAt` is reached (`SESSION_TTL_SECONDS` from login, capped by passport `not_after`)
1129
+ 5. **Cleanup** - Expired sessions removed by automatic cleanup process
1130
+
1131
+ ### Token Invalidation
1132
+
1133
+ The SDK validates tokens by checking session state:
1134
+
1135
+ ```text
1136
+ PSEUDOCODE
1137
+ INPUTS:
1138
+ - Use values defined by the surrounding section/context.
1139
+ STEPS:
1140
+ - NOTE: Authentication middleware checks:
1141
+ - NOTE: 1. JWT signature validity
1142
+ - NOTE: 2. JWT expiration
1143
+ - NOTE: 3. Session exists and is active
1144
+ - NOTE: 4. Session not expired
1145
+ - NOTE: After logout, tokens are invalidated because:
1146
+ - NOTE: - Session status set to 'closed'
1147
+ - NOTE: - Subsequent requests fail authentication
1148
+ OUTPUTS:
1149
+ - Produces the section's intended result using equivalent logic.
1150
+ ```
1151
+
1152
+ ## Configuration
1153
+
1154
+ ### Configuration Priority
1155
+
1156
+ The SDK automatically configures itself from multiple sources with a clear priority hierarchy:
1157
+
1158
+ 1. **Environment Variables** (Highest priority) - Direct `process.env` access
1159
+ 2. **Host Application Config** - Values from `config` package (with env mappings)
1160
+ 3. **SDK Fallback Defaults** - Built-in defaults from `configsdk.js`
1161
+ 4. **Provided Default Value** - Optional parameter to `config.get()`
1162
+
1163
+ **Example:**
1164
+ ```text
1165
+ PSEUDOCODE
1166
+ INPUTS:
1167
+ - Use values defined by the surrounding section/context.
1168
+ STEPS:
1169
+ - SET config TO roditClient.getConfig()
1170
+ - NOTE: Priority 1: Checks process.env.SESSION_STORAGE_TYPE
1171
+ - NOTE: Priority 2: Checks host config.get('SESSION_STORAGE_TYPE')
1172
+ - NOTE: Priority 3: Uses SDK default 'memory'
1173
+ - NOTE: Priority 4: Falls back to 'memory' if provided
1174
+ - SET storageType TO config.get('SESSION_STORAGE_TYPE', 'memory')
1175
+ OUTPUTS:
1176
+ - Produces the section's intended result using equivalent logic.
1177
+ ```
1178
+
1179
+ This ensures that:
1180
+ - CI/CD environment variables always take precedence
1181
+ - Host applications can override SDK defaults
1182
+ - SDK provides sensible defaults for all settings
1183
+ - Configuration is predictable and debuggable
1184
+
1185
+ ### Automatic Configuration Loading
1186
+
1187
+ The SDK loads configuration from multiple sources:
1188
+
1189
+ 1. **Environment Variables** - Direct environment access
1190
+ 2. **Configuration Files** - config/default.json, config/main.json, config/development.json
1191
+ 3. **Vault Credentials** - Main credential storage
1192
+ 4. **SDK Defaults** - Fallback values
1193
+
1194
+ ### Environment Configuration: NODE_ENV and LOG_LEVEL
1195
+
1196
+ The SDK uses **two separate environment variables** for configuration, following Node.js ecosystem standards:
1197
+
1198
+ #### NODE_ENV - Environment Type & Security Behavior
1199
+
1200
+ Controls environment-specific behavior and security settings:
1201
+
1202
+ **Values:**
1203
+ - `main` - Main branch deploy (strict security, no error details)
1204
+ - `development` - Development branch deploy (relaxed security, detailed errors)
1205
+ - `test` - Testing environment (allows bypasses for automated testing)
1206
+
1207
+ **Default:** `development`
1208
+
1209
+ **Controls:**
1210
+ - ✅ Error detail exposure in API responses
1211
+ - ✅ Peer public key requirement enforcement
1212
+ - ✅ Webhook verification bypass (test mode only)
1213
+ - ✅ Security-critical behavior
1214
+
1215
+ #### LOG_LEVEL - Logging Verbosity
1216
+
1217
+ Controls Winston logger verbosity independently from environment:
1218
+
1219
+ **Values:**
1220
+ - `error` - Only errors
1221
+ - `warn` - Warnings and errors
1222
+ - `info` - Informational messages, warnings, and errors (recommended for main)
1223
+ - `debug` - Detailed debugging information
1224
+ - `trace` - Maximum verbosity with full traces
1225
+
1226
+ **Default:** `info`
1227
+
1228
+ **Controls:**
1229
+ - ✅ Winston logger output level
1230
+ - ✅ Debug payload logging
1231
+ - ✅ Log verbosity only (not security)
1232
+
1233
+ #### Separation of Concerns
1234
+
1235
+ ```text
1236
+ PSEUDOCODE
1237
+ INPUTS:
1238
+ - Use values defined by the surrounding section/context.
1239
+ STEPS:
1240
+ - NOTE: Environment detection (security)
1241
+ - SET isMain TO process.env.NODE_ENV === 'main'
1242
+ - SET isDevelopment TO process.env.NODE_ENV === 'development'
1243
+ - SET isTest TO process.env.NODE_ENV === 'test'
1244
+ - NOTE: Logging verbosity (independent)
1245
+ - SET config TO roditClient.getConfig()
1246
+ - SET logLevel TO config.get('LOG_LEVEL', 'info')
1247
+ OUTPUTS:
1248
+ - Produces the section's intended result using equivalent logic.
1249
+ ```
1250
+
1251
+ #### Configuration Examples
1252
+
1253
+ **Main (normal):**
1254
+ ```text
1255
+ PSEUDOCODE
1256
+ INPUTS:
1257
+ - Use values defined by the surrounding section/context.
1258
+ STEPS:
1259
+ - DO: export NODE_ENV=main
1260
+ - DO: export LOG_LEVEL=info
1261
+ - NOTE: Results in:
1262
+ - NOTE: - Strict security enforcement
1263
+ - NOTE: - No error details in responses
1264
+ - NOTE: - Minimal logging output
1265
+ OUTPUTS:
1266
+ - Produces the section's intended result using equivalent logic.
1267
+ ```
1268
+
1269
+ **Main (troubleshooting):**
1270
+ ```text
1271
+ PSEUDOCODE
1272
+ INPUTS:
1273
+ - Use values defined by the surrounding section/context.
1274
+ STEPS:
1275
+ - DO: export NODE_ENV=main
1276
+ - DO: export LOG_LEVEL=debug
1277
+ - NOTE: Results in:
1278
+ - NOTE: - Strict security enforcement (still main)
1279
+ - NOTE: - No error details in responses (still secure)
1280
+ - NOTE: - Verbose logging for debugging
1281
+ OUTPUTS:
1282
+ - Produces the section's intended result using equivalent logic.
1283
+ ```
1284
+
1285
+ **Development:**
1286
+ ```text
1287
+ PSEUDOCODE
1288
+ INPUTS:
1289
+ - Use values defined by the surrounding section/context.
1290
+ STEPS:
1291
+ - DO: export NODE_ENV=development
1292
+ - DO: export LOG_LEVEL=debug
1293
+ - NOTE: Results in:
1294
+ - NOTE: - Relaxed security for development
1295
+ - NOTE: - Detailed error messages in responses
1296
+ - NOTE: - Verbose logging
1297
+ OUTPUTS:
1298
+ - Produces the section's intended result using equivalent logic.
1299
+ ```
1300
+
1301
+ **Testing:**
1302
+ ```text
1303
+ PSEUDOCODE
1304
+ INPUTS:
1305
+ - Use values defined by the surrounding section/context.
1306
+ STEPS:
1307
+ - DO: export NODE_ENV=test
1308
+ - DO: export LOG_LEVEL=error
1309
+ - NOTE: Results in:
1310
+ - NOTE: - Test mode (allows bypasses)
1311
+ - NOTE: - Detailed error messages
1312
+ - NOTE: - Only errors logged (cleaner test output)
1313
+ OUTPUTS:
1314
+ - Produces the section's intended result using equivalent logic.
1315
+ ```
1316
+
1317
+ #### Behavior Matrix
1318
+
1319
+ | Scenario | NODE_ENV | LOG_LEVEL | Security | Error Details | Logging |
1320
+ |----------|----------|-----------|----------|---------------|---------|
1321
+ | Main | `main` | `info` | ✅ Strict | ❌ Hidden | Minimal |
1322
+ | Main Debug | `main` | `debug` | ✅ Strict | ❌ Hidden | Verbose |
1323
+ | Development | `development` | `debug` | ⚠️ Relaxed | ✅ Shown | Verbose |
1324
+ | Testing | `test` | `error` | ⚠️ Bypass OK | ✅ Shown | Errors only |
1325
+
1326
+ ### Vault-Based Configuration (main)
1327
+
1328
+ For main deployments, credentials are loaded from HashiCorp Vault:
1329
+
1330
+ ```text
1331
+ PSEUDOCODE
1332
+ INPUTS:
1333
+ - Use values defined by the surrounding section/context.
1334
+ STEPS:
1335
+ - NOTE: Environment variables for vault
1336
+ - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault
1337
+ - FIELD: export VAULT_ENDPOINT=https://vault.example.com
1338
+ - DO: export VAULT_ROLE_ID=your-role-id
1339
+ - DO: export VAULT_SECRET_ID=your-secret-id
1340
+ - DO: export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
1341
+ - DO: export SERVICE_NAME=your-service-name
1342
+ - DO: export NEAR_CONTRACT_ID=discernible-io.near
1343
+ OUTPUTS:
1344
+ - Produces the section's intended result using equivalent logic.
1345
+ ```
1346
+
1347
+ ### File-Based Configuration (Development)
1348
+
1349
+ For development, credentials can be loaded from files:
1350
+
1351
+ ```text
1352
+ PSEUDOCODE
1353
+ INPUTS:
1354
+ - Use values defined by the surrounding section/context.
1355
+ STEPS:
1356
+ - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=file
1357
+ - DO: export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json
1358
+ OUTPUTS:
1359
+ - Produces the section's intended result using equivalent logic.
1360
+ ```
1361
+
1362
+ ### Accessing Configuration
1363
+
1364
+ ```text
1365
+ PSEUDOCODE
1366
+ INPUTS:
1367
+ - Use values defined by the surrounding section/context.
1368
+ STEPS:
1369
+ - NOTE: Get complete RODiT configuration
1370
+ - SET configObject TO await roditClient.getConfigOwnRodit()
1371
+ - SET metadata TO configObject.own_rodit.metadata
1372
+ - NOTE: Access RODiT token metadata
1373
+ - SET jwtDuration TO metadata.jwt_duration; // JWT expiration time
1374
+ - SET maxRequests TO metadata.max_requests; // Rate limit
1375
+ - SET maxRqWindow TO metadata.maxrq_window; // Rate limit window
1376
+ - SET apiEndpoint TO metadata.subjectuniqueidentifier_url; // API URL
1377
+ - SET webhookUrl TO metadata.webhook_url; // Webhook endpoint
1378
+ - NOTE: Parse permissioned routes
1379
+ - SET permissionedRoutes TO JSON.parse(metadata.permissioned_routes || '{}')
1380
+ - NOTE: Use SDK config for application settings
1381
+ - SET config TO roditClient.getConfig()
1382
+ - SET logLevel TO config.get('LOG_LEVEL', 'info')
1383
+ - SET dbPath TO config.get('API_DEFAULT_OPTIONS.DB_PATH')
1384
+ OUTPUTS:
1385
+ - Produces the section's intended result using equivalent logic.
1386
+ ```
1387
+
1388
+ ### Dynamic Rate Limiting
1389
+
1390
+ ```text
1391
+ PSEUDOCODE
1392
+ INPUTS:
1393
+ - Use values defined by the surrounding section/context.
1394
+ STEPS:
1395
+ - NOTE: Configure rate limiting from RODiT token
1396
+ - SET configObject TO await roditClient.getConfigOwnRodit()
1397
+ - SET metadata TO configObject.own_rodit.metadata
1398
+ - CHECK CONDITION: if (metadata.max_requests && metadata.maxrq_window) {
1399
+ - SET maxRequests TO parseInt(metadata.max_requests)
1400
+ - SET windowSeconds TO parseInt(metadata.maxrq_window)
1401
+ - SET rateLimiter TO roditClient.getRateLimitMiddleware()
1402
+ - DO: app.use(rateLimiter(maxRequests, windowSeconds))
1403
+ - }
1404
+ OUTPUTS:
1405
+ - Produces the section's intended result using equivalent logic.
1406
+ ```
1407
+
1408
+ ### Environment Variables
1409
+
1410
+ Complete list of SDK environment variables:
1411
+
1412
+ #### Core Configuration
1413
+ ```text
1414
+ PSEUDOCODE
1415
+ INPUTS:
1416
+ - Use values defined by the surrounding section/context.
1417
+ STEPS:
1418
+ - NOTE: Service identification
1419
+ - DO: export SERVICE_NAME=your-service-name
1420
+ - DO: export API_VERSION=1.0.0
1421
+ - NOTE: Environment and logging
1422
+ - DO: export NODE_ENV=main # main, development, test
1423
+ - DO: export LOG_LEVEL=info # error, warn, info, debug, trace
1424
+ OUTPUTS:
1425
+ - Produces the section's intended result using equivalent logic.
1426
+ ```
1427
+
1428
+ #### Credentials and Authentication
1429
+ ```text
1430
+ PSEUDOCODE
1431
+ INPUTS:
1432
+ - Use values defined by the surrounding section/context.
1433
+ STEPS:
1434
+ - NOTE: Credential source
1435
+ - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault # vault, file, env
1436
+ - NOTE: Vault configuration (main)
1437
+ - FIELD: export VAULT_ENDPOINT=https://vault.example.com
1438
+ - DO: export VAULT_ROLE_ID=your-role-id
1439
+ - DO: export VAULT_SECRET_ID=your-secret-id
1440
+ - DO: export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
1441
+ - DO: export VAULT_TOKEN_TTL=3600
1442
+ - NOTE: File-based credentials (development)
1443
+ - DO: export CREDENTIALS_FILEPATH=./credentials/rodit.json
1444
+ - NOTE: NEAR blockchain
1445
+ - DO: export NEAR_CONTRACT_ID=discernible-io.near
1446
+ - FIELD: export NEAR_RPC_URL=https://rpc.mainnet.fastnear.com
1447
+ - DO: export NEAR_RPC_CACHE_TTL=5000 # milliseconds
1448
+ OUTPUTS:
1449
+ - Produces the section's intended result using equivalent logic.
1450
+ ```
1451
+
1452
+ #### Session Management
1453
+ ```text
1454
+ PSEUDOCODE
1455
+ INPUTS:
1456
+ - Use values defined by the surrounding section/context.
1457
+ STEPS:
1458
+ - NOTE: Session storage configuration
1459
+ - DO: export SESSION_STORAGE_TYPE=express-session # memory, express, express-session
1460
+ - DO: export SESSION_CLEANUP_INTERVAL=3600000 # milliseconds (1 hour)
1461
+ - DO: export SESSION_TOKEN_RETENTION_PERIOD=604800 # seconds (7 days)
1462
+ - DO: export SESSION_VALIDATION_CACHE_TTL=5000 # milliseconds (5 seconds)
1463
+ OUTPUTS:
1464
+ - Produces the section's intended result using equivalent logic.
1465
+ ```
1466
+
1467
+ #### Logging and Monitoring
1468
+ ```text
1469
+ PSEUDOCODE
1470
+ INPUTS:
1471
+ - Use values defined by the surrounding section/context.
1472
+ STEPS:
1473
+ - NOTE: Loki logging
1474
+ - FIELD: export LOKI_URL=https://loki.example.com:3100
1475
+ - FIELD: export LOKI_BASIC_AUTH=username:password
1476
+ - DO: export LOKI_TLS_SKIP_VERIFY=false # true to skip TLS verification
1477
+ OUTPUTS:
1478
+ - Produces the section's intended result using equivalent logic.
1479
+ ```
1480
+
1481
+ #### Security Options
1482
+ ```text
1483
+ PSEUDOCODE
1484
+ INPUTS:
1485
+ - Use values defined by the surrounding section/context.
1486
+ STEPS:
1487
+ - NOTE: Webhook configuration
1488
+ - DO: export WEBHOOK_TLS_SKIP_VERIFY=false # true to skip TLS verification
1489
+ - NOTE: Login mode control (see Login Mode section below)
1490
+ - DO: export SECURITY_OPTIONS_LOGIN_MODE=partner # partner, promiscuous, or p2p
1491
+ - NOTE: Security thresholds
1492
+ - DO: export SECURITY_OPTIONS_LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY=0.80
1493
+ - DO: export SECURITY_OPTIONS_THRESHOLD_VALIDATION_TYPE=0.10
1494
+ - DO: export SECURITY_OPTIONS_DURATIONRAMP=0.85
1495
+ - DO: export SECURITY_OPTIONS_SERVERORCLIENT=SERVER-INITIATED
1496
+ - DO: export SECURITY_OPTIONS_SILENT_LOGIN_FAILURES=false
1497
+ - NOTE: Server session lifetime (seconds from login; SDK default 5200)
1498
+ - DO: export SECURITY_OPTIONS_SESSION_TTL_SECONDS=5200
1499
+ - NOTE: Access-token fallback when passport jwt_duration is invalid
1500
+ - DO: export SECURITY_OPTIONS_FALLBACK_JWT_DURATION=3600
1501
+ OUTPUTS:
1502
+ - Produces the section's intended result using equivalent logic.
1503
+ ```
1504
+
1505
+ #### Database Configuration
1506
+ ```text
1507
+ PSEUDOCODE
1508
+ INPUTS:
1509
+ - Use values defined by the surrounding section/context.
1510
+ STEPS:
1511
+ - DO: export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite
1512
+ OUTPUTS:
1513
+ - Produces the section's intended result using equivalent logic.
1514
+ ```
1515
+
1516
+ ## Logging & Monitoring
1517
+
1518
+ ### Structured Logging
1519
+
1520
+ The SDK provides comprehensive structured logging:
1521
+
1522
+ ```text
1523
+ PSEUDOCODE
1524
+ INPUTS:
1525
+ - Use values defined by the surrounding section/context.
1526
+ STEPS:
1527
+ - DO: const { logger } = require('@rodit/rodit-auth-be')
1528
+ - NOTE: Basic logging
1529
+ - DO: logger.info('Operation completed', {
1530
+ - FIELD: component: 'UserService',
1531
+ - FIELD: operation: 'createUser',
1532
+ - FIELD: userId: '123',
1533
+ - FIELD: duration: 150
1534
+ - DO: })
1535
+ - NOTE: Context-aware logging
1536
+ - DO: logger.infoWithContext('Request processed', {
1537
+ - FIELD: component: 'API',
1538
+ - FIELD: method: 'POST',
1539
+ - FIELD: path: '/api/users',
1540
+ - FIELD: requestId: req.requestId,
1541
+ - FIELD: userId: req.user?.id,
1542
+ - FIELD: duration: Date.now() - req.startTime
1543
+ - DO: })
1544
+ - NOTE: Error logging with metrics
1545
+ - DO: logger.errorWithContext('Operation failed', {
1546
+ - FIELD: component: 'UserService',
1547
+ - FIELD: operation: 'createUser',
1548
+ - FIELD: requestId: req.requestId,
1549
+ - FIELD: error: error.message,
1550
+ - FIELD: stack: error.stack
1551
+ - DO: }, error)
1552
+ OUTPUTS:
1553
+ - Produces the section's intended result using equivalent logic.
1554
+ ```
1555
+
1556
+ ### Loki with the SDK (canonical)
1557
+
1558
+ Use this as the authoritative guide for configuring logging with the SDK.
1559
+
1560
+ #### Environment variables
1561
+
1562
+ ```text
1563
+ PSEUDOCODE
1564
+ INPUTS:
1565
+ - Use values defined by the surrounding section/context.
1566
+ STEPS:
1567
+ - FIELD: export LOKI_URL=https://<your-loki-host>:3100
1568
+ - FIELD: export LOKI_BASIC_AUTH="username:password" # store in secrets
1569
+ - DO: export LOKI_TLS_SKIP_VERIFY=true # only for self-signed/test
1570
+ - DO: export LOG_LEVEL=info
1571
+ - DO: export SERVICE_NAME=clienttest-idc
1572
+ OUTPUTS:
1573
+ - Produces the section's intended result using equivalent logic.
1574
+ ```
1575
+
1576
+ These are already mapped in `config/custom-environment-variables.json`, so container/CI env vars will flow into the app.
1577
+
1578
+ #### How the SDK selects/configures the logger
1579
+
1580
+ - Default: JSON to stdout only (no Loki). Honors `LOG_LEVEL`, adds `service_name`.
1581
+ - Main: Create a Winston logger with a `winston-loki` transport and inject it once: `logger.setLogger(customLogger)`.
1582
+ - Access: `const { logger } = require('@rodit/rodit-auth-be')` or `roditClient.getLogger()` both delegate to the same facade.
1583
+
1584
+ #### Direct-to-Loki via winston-loki (recommended)
1585
+
1586
+ ```text
1587
+ PSEUDOCODE
1588
+ INPUTS:
1589
+ - Use values defined by the surrounding section/context.
1590
+ STEPS:
1591
+ - DO: const { logger } = require('@rodit/rodit-auth-be')
1592
+ - SET winston TO require('winston')
1593
+ - SET LokiTransport TO require('winston-loki')
1594
+ - SET transports TO [new winston.transports.Console({ format: winston.format.json() })]
1595
+ - CHECK CONDITION: if (process.env.LOKI_URL) {
1596
+ - SET lokiOptions TO {
1597
+ - FIELD: host: process.env.LOKI_URL,
1598
+ - FIELD: basicAuth: process.env.LOKI_BASIC_AUTH, // Basic Auth for Loki
1599
+ - FIELD: labels: { app: process.env.SERVICE_NAME || 'clienttest-idc', component: 'rodit-sdk' },
1600
+ - FIELD: json: true,
1601
+ - FIELD: batching: true
1602
+ - DO: }
1603
+ - CHECK CONDITION: if ((process.env.LOKI_TLS_SKIP_VERIFY || '').toLowerCase() === 'true') {
1604
+ - FIELD: lokiOptions.ssl = { rejectUnauthorized: false }
1605
+ - }
1606
+ - DO: transports.push(new LokiTransport(lokiOptions))
1607
+ - }
1608
+ - SET customLogger TO winston.createLogger({
1609
+ - FIELD: level: process.env.LOG_LEVEL || 'info',
1610
+ - FIELD: format: winston.format.json(),
1611
+ - DO: transports
1612
+ - DO: })
1613
+ - DO: logger.setLogger(customLogger)
1614
+ OUTPUTS:
1615
+ - Produces the section's intended result using equivalent logic.
1616
+ ```
1617
+
1618
+ #### CI/CD notes
1619
+
1620
+ - `.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.
1621
+ - Store `LOKI_BASIC_AUTH` in CI/CD secrets; never commit credentials.
1622
+
1623
+ #### Quick verification
1624
+
1625
+ 1) Start the app with `LOKI_URL` and `LOKI_BASIC_AUTH` set.
1626
+ 2) Emit a test log: `logger.info('Loki test', { component: 'SmokeTest' })`.
1627
+ 3) In Grafana Explore, query with `{app="clienttest-idc"}` and confirm logs.
1628
+
1629
+ ## Performance Tracking
1630
+
1631
+ The SDK includes comprehensive performance tracking and metrics collection.
1632
+
1633
+ ### Performance Service
1634
+
1635
+ ```text
1636
+ PSEUDOCODE
1637
+ INPUTS:
1638
+ - Use values defined by the surrounding section/context.
1639
+ STEPS:
1640
+ - SET performanceService TO roditClient.getPerformanceService()
1641
+ - NOTE: Record incoming request
1642
+ - DO: performanceService.recordRequest(req)
1643
+ - NOTE: Record custom metrics with labels
1644
+ - DO: performanceService.recordMetric('operation_duration', 150, {
1645
+ - FIELD: operation: 'db_query',
1646
+ - FIELD: table: 'users',
1647
+ - FIELD: status: 'success'
1648
+ - DO: })
1649
+ - NOTE: Record errors
1650
+ - DO: performanceService.recordMetric('error_count', 1, {
1651
+ - FIELD: method: req.method,
1652
+ - FIELD: path: req.path,
1653
+ - FIELD: status: res.statusCode
1654
+ - DO: })
1655
+ - NOTE: Get aggregated metrics
1656
+ - SET metrics TO performanceService.getMetrics()
1657
+ - FIELD: console.log('Total requests:', metrics.totalRequests)
1658
+ - FIELD: console.log('Error count:', metrics.errorCount)
1659
+ - FIELD: console.log('Average response time:', metrics.avgResponseTime)
1660
+ OUTPUTS:
1661
+ - Produces the section's intended result using equivalent logic.
1662
+ ```
1663
+
1664
+ ### Automatic Request Tracking
1665
+
1666
+ Integrate performance tracking into your middleware:
1667
+
1668
+ ```text
1669
+ PSEUDOCODE
1670
+ INPUTS:
1671
+ - Use values defined by the surrounding section/context.
1672
+ STEPS:
1673
+ - NOTE: Performance monitoring middleware
1674
+ - DO: app.use((req, res, next) => {
1675
+ - DO: req.startTime = Date.now()
1676
+ - SET performanceService TO roditClient.getPerformanceService()
1677
+ - CHECK CONDITION: if (performanceService) {
1678
+ - DO: performanceService.recordRequest(req)
1679
+ - }
1680
+ - DO: res.on('finish', () => {
1681
+ - SET duration TO Date.now() - req.startTime
1682
+ - CHECK CONDITION: if (performanceService) {
1683
+ - NOTE: Record request duration
1684
+ - DO: performanceService.recordMetric('request_duration_ms', duration, {
1685
+ - FIELD: method: req.method,
1686
+ - FIELD: path: req.path,
1687
+ - FIELD: status: res.statusCode
1688
+ - DO: })
1689
+ - NOTE: Record errors
1690
+ - CHECK CONDITION: if (res.statusCode >= 400) {
1691
+ - DO: performanceService.recordMetric('error_count', 1, {
1692
+ - FIELD: method: req.method,
1693
+ - FIELD: path: req.path,
1694
+ - FIELD: status: res.statusCode
1695
+ - DO: })
1696
+ - }
1697
+ - }
1698
+ - DO: })
1699
+ - DO: next()
1700
+ - DO: })
1701
+ OUTPUTS:
1702
+ - Produces the section's intended result using equivalent logic.
1703
+ ```
1704
+
1705
+ ### Session Performance Metrics
1706
+
1707
+ Track session-related performance:
1708
+
1709
+ ```text
1710
+ PSEUDOCODE
1711
+ INPUTS:
1712
+ - Use values defined by the surrounding section/context.
1713
+ STEPS:
1714
+ - SET sessionManager TO roditClient.getSessionManager()
1715
+ - NOTE: Get validation cache statistics
1716
+ - SET cacheStats TO sessionManager.getValidationCacheStats()
1717
+ - DO: logger.info('Session cache performance', {
1718
+ - FIELD: component: 'SessionManager',
1719
+ - FIELD: totalEntries: cacheStats.totalEntries,
1720
+ - FIELD: validEntries: cacheStats.validEntries,
1721
+ - FIELD: expiredEntries: cacheStats.expiredEntries,
1722
+ - FIELD: cacheTTL: cacheStats.cacheTTL,
1723
+ - FIELD: cacheEnabled: cacheStats.cacheEnabled
1724
+ - DO: })
1725
+ - NOTE: Get storage information
1726
+ - SET storageInfo TO await sessionManager.getStorageInfo()
1727
+ - DO: logger.info('Session storage status', {
1728
+ - FIELD: component: 'SessionManager',
1729
+ - FIELD: storageType: storageInfo.type,
1730
+ - FIELD: sessionCount: storageInfo.sessionCount,
1731
+ - FIELD: timestamp: storageInfo.timestamp
1732
+ - DO: })
1733
+ OUTPUTS:
1734
+ - Produces the section's intended result using equivalent logic.
1735
+ ```
1736
+
1737
+ ### Custom Metrics
1738
+
1739
+ Record application-specific metrics:
1740
+
1741
+ ```text
1742
+ PSEUDOCODE
1743
+ INPUTS:
1744
+ - Use values defined by the surrounding section/context.
1745
+ STEPS:
1746
+ - SET performanceService TO roditClient.getPerformanceService()
1747
+ - NOTE: Database operation timing
1748
+ - SET dbStart TO Date.now()
1749
+ - SET result TO await db.query('SELECT * FROM users')
1750
+ - SET dbDuration TO Date.now() - dbStart
1751
+ - DO: performanceService.recordMetric('db_query_duration', dbDuration, {
1752
+ - FIELD: operation: 'select',
1753
+ - FIELD: table: 'users',
1754
+ - FIELD: rowCount: result.length
1755
+ - DO: })
1756
+ - NOTE: External API call timing
1757
+ - SET apiStart TO Date.now()
1758
+ - SET apiResponse TO await fetch('https://api.example.com/data')
1759
+ - SET apiDuration TO Date.now() - apiStart
1760
+ - DO: performanceService.recordMetric('external_api_duration', apiDuration, {
1761
+ - FIELD: endpoint: 'api.example.com',
1762
+ - FIELD: status: apiResponse.status,
1763
+ - FIELD: success: apiResponse.ok
1764
+ - DO: })
1765
+ - NOTE: Business metrics
1766
+ - DO: performanceService.recordMetric('user_action', 1, {
1767
+ - FIELD: action: 'comment_created',
1768
+ - FIELD: userId: req.user.id,
1769
+ - FIELD: timestamp: new Date().toISOString()
1770
+ - DO: })
1771
+ OUTPUTS:
1772
+ - Produces the section's intended result using equivalent logic.
1773
+ ```
1774
+
1775
+ ## Webhooks
1776
+
1777
+ ### Overview
1778
+
1779
+ The SDK supports sending webhooks to multiple endpoints for important events. Webhook URLs are configured in the RODiT token metadata.
1780
+
1781
+ **Key Features:**
1782
+ - **Custom Endpoints** - Send webhooks to any endpoint path (e.g., `/hooks/wake`, `/hooks/agent`, `/webhook`)
1783
+ - **Non-blocking** - Webhooks sent asynchronously without blocking the main response
1784
+ - **Error Resilient** - Webhook failures don't affect the main operation
1785
+
1786
+ Webhooks are configured in your RODiT token:
1787
+
1788
+ ```text
1789
+ PSEUDOCODE
1790
+ INPUTS:
1791
+ - Use values defined by the surrounding section/context.
1792
+ STEPS:
1793
+ - {
1794
+ - FIELD: "webhook_url": "https://webhook.example.com:7443",
1795
+ - FIELD: "webhook_cidr": "0.0.0.0/0"
1796
+ - }
1797
+ OUTPUTS:
1798
+ - Produces the section's intended result using equivalent logic.
1799
+ ```
1800
+
1801
+ ### Sending Webhooks to Default Endpoint
1802
+
1803
+ Send webhooks to the default `/webhook` endpoint:
1804
+
1805
+ ```text
1806
+ PSEUDOCODE
1807
+ INPUTS:
1808
+ - Use values defined by the surrounding section/context.
1809
+ STEPS:
1810
+ - NOTE: Get webhook handler from client
1811
+ - SET roditClient TO req.app.locals.roditClient
1812
+ - NOTE: Send webhook for an event
1813
+ - SET webhookPayload TO {
1814
+ - FIELD: event: 'comment_created',
1815
+ - FIELD: data: {
1816
+ - FIELD: id: comment.id,
1817
+ - FIELD: author: comment.author,
1818
+ - FIELD: timestamp: new Date().toISOString()
1819
+ - DO: },
1820
+ - FIELD: isError: false
1821
+ - DO: }
1822
+ - DO: try {
1823
+ - SET result TO await roditClient.sendWebhook(webhookPayload, req)
1824
+ - CHECK CONDITION: if (result.success) {
1825
+ - DO: logger.info('Webhook sent successfully', {
1826
+ - FIELD: component: 'CRUDA',
1827
+ - FIELD: event: webhookPayload.event,
1828
+ - FIELD: requestId: req.requestId
1829
+ - DO: })
1830
+ - }
1831
+ - DO: } catch (error) {
1832
+ - NOTE: Webhook failures don't crash the application
1833
+ - DO: logger.warn('Webhook delivery failed', {
1834
+ - FIELD: component: 'CRUDA',
1835
+ - FIELD: event: webhookPayload.event,
1836
+ - FIELD: error: error.message,
1837
+ - FIELD: requestId: req.requestId
1838
+ - DO: })
1839
+ - }
1840
+ OUTPUTS:
1841
+ - Produces the section's intended result using equivalent logic.
1842
+ ```
1843
+
1844
+ ### Sending Webhooks to Custom Endpoints
1845
+
1846
+ Send webhooks to specific endpoints like `/hooks/wake` or `/hooks/agent`:
1847
+
1848
+ ```text
1849
+ PSEUDOCODE
1850
+ INPUTS:
1851
+ - Use values defined by the surrounding section/context.
1852
+ STEPS:
1853
+ - SET roditClient TO req.app.locals.roditClient
1854
+ - SET webhookPayload TO {
1855
+ - FIELD: event: 'heartbeat_request',
1856
+ - FIELD: data: {
1857
+ - FIELD: timestamp: new Date().toISOString(),
1858
+ - FIELD: source: '/api/testhola'
1859
+ - }
1860
+ - DO: }
1861
+ - NOTE: Send to /hooks/wake endpoint (trigger immediate heartbeat)
1862
+ - WAIT FOR: roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/wake', req)
1863
+ - NOTE: Send to /hooks/agent endpoint (run isolated agent task)
1864
+ - WAIT FOR: roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/agent', req)
1865
+ - NOTE: Send to custom endpoint
1866
+ - WAIT FOR: roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/custom', req)
1867
+ OUTPUTS:
1868
+ - Produces the section's intended result using equivalent logic.
1869
+ ```
1870
+
1871
+ ### Convenience Methods for Common Endpoints
1872
+
1873
+ ```text
1874
+ PSEUDOCODE
1875
+ INPUTS:
1876
+ - Use values defined by the surrounding section/context.
1877
+ STEPS:
1878
+ - SET roditClient TO req.app.locals.roditClient
1879
+ - SET payload TO {
1880
+ - FIELD: event: 'test_event',
1881
+ - FIELD: data: { timestamp: new Date().toISOString() }
1882
+ - DO: }
1883
+ - NOTE: Send to /hooks/wake (heartbeat confirmation)
1884
+ - WAIT FOR: roditClient.sendWakeHook(payload, req)
1885
+ - NOTE: Send to /hooks/agent (agent task confirmation)
1886
+ - WAIT FOR: roditClient.sendAgentHook(payload, req)
1887
+ OUTPUTS:
1888
+ - Produces the section's intended result using equivalent logic.
1889
+ ```
1890
+
1891
+ ### Webhook Endpoint Purposes
1892
+
1893
+ | Endpoint | Purpose | Use Case |
1894
+ |----------|---------|----------|
1895
+ | `/webhook` | Default webhook endpoint | General event notifications |
1896
+ | `/hooks/wake` | Trigger immediate heartbeat | Enqueue system event for main session |
1897
+ | `/hooks/agent` | Run isolated agent task | Execute background tasks with optional reply to messaging channels |
1898
+
1899
+ ### Webhook Error Handling
1900
+
1901
+ ```text
1902
+ PSEUDOCODE
1903
+ INPUTS:
1904
+ - Use values defined by the surrounding section/context.
1905
+ STEPS:
1906
+ - NOTE: Graceful webhook handling in CRUDA operations
1907
+ - SET logAndSendWebhook TO async (payload, req = null) => {
1908
+ - DO: try {
1909
+ - SET roditClient TO req?.app?.locals?.roditClient
1910
+ - CHECK CONDITION: if (!roditClient) {
1911
+ - DO: logger.warn('RoditClient not available, skipping webhook', {
1912
+ - FIELD: component: 'CRUDA',
1913
+ - FIELD: event: payload?.event
1914
+ - DO: })
1915
+ - RETURN { success: false, error: 'RoditClient not available' }
1916
+ - }
1917
+ - RETURN await roditClient.sendWebhook(payload, req)
1918
+ - DO: } catch (error) {
1919
+ - NOTE: Log but don't throw - webhook failures shouldn't crash the app
1920
+ - DO: logger.error('Webhook delivery failed', {
1921
+ - FIELD: component: 'CRUDA',
1922
+ - FIELD: event: payload?.event,
1923
+ - FIELD: error: error.message
1924
+ - DO: })
1925
+ - RETURN { success: false, error: error.message }
1926
+ - }
1927
+ - DO: }
1928
+ OUTPUTS:
1929
+ - Produces the section's intended result using equivalent logic.
1930
+ ```
1931
+
1932
+ ### Development/Testing Webhooks
1933
+
1934
+ The `/api/testhola` endpoint sends test webhooks in development mode (`NODE_ENV === 'development'`):
1935
+
1936
+ ```text
1937
+ PSEUDOCODE
1938
+ INPUTS:
1939
+ - Use values defined by the surrounding section/context.
1940
+ STEPS:
1941
+ - NOTE: Event: testhola_validation_success
1942
+ - NOTE: Sent to: /hooks/wake and /hooks/agent (development only)
1943
+ - {
1944
+ - FIELD: "event": "testhola_validation_success",
1945
+ - FIELD: "data": {
1946
+ - FIELD: "peerTokenId": "bcdfhjkmnpqr",
1947
+ - FIELD: "serverTokenId": "bcdfhjkmnpqr",
1948
+ - FIELD: "recipient": "MUNDO",
1949
+ - FIELD: "timestamp": "2026-04-24T14:30:00.000Z",
1950
+ - FIELD: "endpoint": "/api/testhola"
1951
+ - }
1952
+ - }
1953
+ OUTPUTS:
1954
+ - Produces the section's intended result using equivalent logic.
1955
+ ```
1956
+
1957
+ **Use Case:** Test webhook delivery and signature validation during development without needing a main deployment.
1958
+
1959
+ ## Advanced Usage
1960
+
1961
+ ### Route Module Pattern
1962
+
1963
+ Create reusable route modules that access the shared RoditClient:
1964
+
1965
+ ```text
1966
+ PSEUDOCODE
1967
+ INPUTS:
1968
+ - Use values defined by the surrounding section/context.
1969
+ STEPS:
1970
+ - NOTE: routes/protected.js
1971
+ - SET express TO require('express')
1972
+ - DO: const { logger } = require('@rodit/rodit-auth-be')
1973
+ - SET router TO express.Router()
1974
+ - NOTE: Middleware that uses the shared client
1975
+ - SET authenticate TO (req, res, next) => {
1976
+ - SET client TO req.app.locals.roditClient
1977
+ - CHECK CONDITION: if (!client) {
1978
+ - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
1979
+ - }
1980
+ - RETURN client.authenticate(req, res, next)
1981
+ - DO: }
1982
+ - SET authorize TO (req, res, next) => {
1983
+ - SET client TO req.app.locals.roditClient
1984
+ - CHECK CONDITION: if (!client) {
1985
+ - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
1986
+ - }
1987
+ - RETURN client.authorize(req, res, next)
1988
+ - DO: }
1989
+ - NOTE: Protected route with full authentication and authorization
1990
+ - DO: router.get('/data', authenticate, authorize, async (req, res) => {
1991
+ - SET startTime TO Date.now()
1992
+ - DO: try {
1993
+ - NOTE: Your business logic here
1994
+ - SET data TO await processUserData(req.user.id)
1995
+ - DO: logger.infoWithContext('Data retrieved successfully', {
1996
+ - FIELD: component: 'ProtectedRoutes',
1997
+ - FIELD: method: 'getData',
1998
+ - FIELD: userId: req.user.id,
1999
+ - FIELD: requestId: req.requestId,
2000
+ - FIELD: duration: Date.now() - startTime
2001
+ - DO: })
2002
+ - FIELD: res.json({ data, requestId: req.requestId })
2003
+ - DO: } catch (error) {
2004
+ - DO: logger.errorWithContext('Failed to retrieve data', {
2005
+ - FIELD: component: 'ProtectedRoutes',
2006
+ - FIELD: method: 'getData',
2007
+ - FIELD: userId: req.user.id,
2008
+ - FIELD: requestId: req.requestId,
2009
+ - FIELD: duration: Date.now() - startTime,
2010
+ - FIELD: error: error.message
2011
+ - DO: }, error)
2012
+ - DO: res.status(500).json({
2013
+ - FIELD: error: 'Internal server error',
2014
+ - FIELD: requestId: req.requestId
2015
+ - DO: })
2016
+ - }
2017
+ - DO: })
2018
+ - DO: module.exports = router
2019
+ OUTPUTS:
2020
+ - Produces the section's intended result using equivalent logic.
2021
+ ```
2022
+
2023
+ ### Portal Authentication (Server-to-Server)
2024
+
2025
+ For server-to-server authentication (e.g., minting client tokens):
2026
+
2027
+ ```text
2028
+ PSEUDOCODE
2029
+ INPUTS:
2030
+ - Use values defined by the surrounding section/context.
2031
+ STEPS:
2032
+ - NOTE: routes/signclient.js
2033
+ - SET router TO express.Router()
2034
+ - DO: router.post('/signclient', authenticate, authorize, async (req, res) => {
2035
+ - DO: const { tobesignedValues, mintingfee, mintingfeeaccount } = req.body
2036
+ - SET client TO req.app.locals.roditClient
2037
+ - SET logger TO client.getLogger()
2038
+ - DO: try {
2039
+ - NOTE: Validate requested permissions against server's permissions
2040
+ - SET configObject TO await client.getConfigOwnRodit()
2041
+ - SET serverPermissions TO JSON.parse(
2042
+ - DO: configObject.own_rodit.metadata.permissioned_routes || '{}'
2043
+ - DO: )
2044
+ - SET requestedPermissions TO JSON.parse(
2045
+ - DO: tobesignedValues.permissioned_routes || '{}'
2046
+ - DO: )
2047
+ - NOTE: Validate that all requested routes exist in server config
2048
+ - NOTE: (Implementation details in actual code)
2049
+ - NOTE: Authenticate to portal and mint client token
2050
+ - SET port TO configObject.port || 8443
2051
+ - SET result TO await client.login_portal(configObject, port)
2052
+ - CHECK CONDITION: if (result.error) {
2053
+ - RETURN res.status(500).json({
2054
+ - FIELD: error: 'Portal authentication failed',
2055
+ - FIELD: details: result.message,
2056
+ - FIELD: requestId: req.requestId
2057
+ - DO: })
2058
+ - }
2059
+ - NOTE: Sign the client token via portal
2060
+ - SET signedToken TO await signPortalRodit(
2061
+ - DO: port,
2062
+ - DO: tobesignedValues,
2063
+ - DO: mintingfee,
2064
+ - DO: mintingfeeaccount,
2065
+ - DO: client
2066
+ - DO: )
2067
+ - DO: res.json({
2068
+ - DO: signedToken,
2069
+ - FIELD: requestId: req.requestId
2070
+ - DO: })
2071
+ - DO: } catch (error) {
2072
+ - DO: logger.errorWithContext('Client token minting failed', {
2073
+ - FIELD: component: 'SignClient',
2074
+ - FIELD: requestId: req.requestId,
2075
+ - FIELD: error: error.message
2076
+ - DO: }, error)
2077
+ - DO: res.status(500).json({
2078
+ - FIELD: error: 'Token minting failed',
2079
+ - FIELD: requestId: req.requestId
2080
+ - DO: })
2081
+ - }
2082
+ - DO: })
2083
+ - DO: module.exports = router
2084
+ OUTPUTS:
2085
+ - Produces the section's intended result using equivalent logic.
2086
+ ```
2087
+
2088
+ ### SignPortal URL Configuration
2089
+
2090
+ #### Overview
2091
+
2092
+ 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.
2093
+
2094
+ #### Smart Contract Name Format
2095
+
2096
+ The SignPortal URL is derived from the smart contract component (`sc=`) in your `serviceprovider_id`. The SDK supports two formats:
2097
+
2098
+ **Standard Format (3+ components):**
2099
+ ```
2100
+ sc=<number>-<domain>-<tld>.near
2101
+ ```
2102
+
2103
+ Example:
2104
+ ```
2105
+ serviceprovider_id: "bc=near.org;sc=10975-discernible-org.near;id=..."
2106
+ ```
2107
+
2108
+ Parsing:
2109
+ - Split by `.`: `["10975-discernible-org", "near"]`
2110
+ - Take first part: `10975-discernible-org`
2111
+ - Split by `-`: `["10975", "discernible", "org"]`
2112
+ - Extract domain: `discernible` (index 1)
2113
+ - Extract TLD: `org` (index 2)
2114
+ - **Result**: `https://signportal.discernible.org:8443`
2115
+
2116
+ **Alternative Format (2 components):**
2117
+ ```
2118
+ sc=<domain>-<tld>.near
2119
+ ```
2120
+
2121
+ Example:
2122
+ ```
2123
+ serviceprovider_id: "bc=near.org;sc=roditcorp-com.near;id=..."
2124
+ ```
2125
+
2126
+ Parsing:
2127
+ - Split by `.`: `["roditcorp-com", "near"]`
2128
+ - Take first part: `roditcorp-com`
2129
+ - Split by `-`: `["roditcorp", "com"]`
2130
+ - Extract domain: `roditcorp` (index 0)
2131
+ - Extract TLD: `com` (index 1)
2132
+ - **Result**: `https://signportal.roditcorp.com:8443`
2133
+
2134
+ #### serviceprovider_id Structure
2135
+
2136
+ The complete `serviceprovider_id` format:
2137
+ ```
2138
+ bc=<blockchain>;sc=<smart-contract>;id=<identifier>[;id=<additional-id>]
2139
+ ```
2140
+
2141
+ Components:
2142
+ - `bc=` - Blockchain identifier (e.g., `near.org`)
2143
+ - `sc=` - Smart contract name (used to construct SignPortal URL)
2144
+ - `id=` - One or more identifier components
2145
+
2146
+ Example:
2147
+ ```text
2148
+ PSEUDOCODE
2149
+ INPUTS:
2150
+ - Use values defined by the surrounding section/context.
2151
+ STEPS:
2152
+ - {
2153
+ - FIELD: "serviceprovider_id": "bc=near.org;sc=roditcorp-com.near;id=01K8QECHMKFVNWQ54PJ2W2GMA7;id=01K8QECHMM1214VMDHSH7JM6H8"
2154
+ - }
2155
+ OUTPUTS:
2156
+ - Produces the section's intended result using equivalent logic.
2157
+ ```
2158
+
2159
+ #### URL Construction Method
2160
+
2161
+ The SDK uses `roditClient.getPortalUrl(serviceProviderId, port)` to construct the SignPortal URL:
2162
+
2163
+ ```text
2164
+ PSEUDOCODE
2165
+ INPUTS:
2166
+ - Use values defined by the surrounding section/context.
2167
+ STEPS:
2168
+ - SET client TO req.app.locals.roditClient
2169
+ - SET configObject TO await client.getConfigOwnRodit()
2170
+ - SET serviceProviderId TO configObject.own_rodit.metadata.serviceprovider_id
2171
+ - SET portalPort TO 8443
2172
+ - NOTE: Automatically constructs: https://signportal.<domain>.<tld>:8443
2173
+ - SET portalUrl TO client.getPortalUrl(serviceProviderId, portalPort)
2174
+ OUTPUTS:
2175
+ - Produces the section's intended result using equivalent logic.
2176
+ ```
2177
+
2178
+ #### Troubleshooting
2179
+
2180
+ **Error: "Failed to parse URL from " (empty string)**
2181
+ - **Cause**: `serviceprovider_id` is empty or undefined in your RODiT configuration
2182
+ - **Solution**: Verify your RODiT token has a valid `serviceprovider_id` field
2183
+ - **Check**: Run `./infra/roditwallet.sh <private-key> <token-id>` to view token metadata
2184
+
2185
+ **Error: "Invalid serviceprovider_id format: missing sc= component"**
2186
+ - **Cause**: The `serviceprovider_id` doesn't contain an `sc=` component
2187
+ - **Solution**: Ensure your token includes the smart contract identifier
2188
+ - **Format**: `bc=near.org;sc=<contract-name>.near;id=...`
2189
+
2190
+ **Error: "Invalid domain format in smart contract"**
2191
+ - **Cause**: Smart contract name has fewer than 2 components when split by `-`
2192
+ - **Solution**: Use format `<domain>-<tld>` or `<number>-<domain>-<tld>`
2193
+ - **Valid**: `roditcorp-com.near`, `10975-discernible-org.near`
2194
+ - **Invalid**: `roditcorp.near`, `mycontract.near`
2195
+
2196
+ #### Configuration Verification
2197
+
2198
+ To verify your SignPortal URL configuration:
2199
+
2200
+ ```text
2201
+ PSEUDOCODE
2202
+ INPUTS:
2203
+ - Use values defined by the surrounding section/context.
2204
+ STEPS:
2205
+ - SET client TO req.app.locals.roditClient
2206
+ - SET logger TO client.getLogger()
2207
+ - DO: try {
2208
+ - SET configObject TO await client.getConfigOwnRodit()
2209
+ - SET serviceProviderId TO configObject.own_rodit.metadata.serviceprovider_id
2210
+ - DO: logger.info('RODiT Configuration', {
2211
+ - FIELD: component: 'SignPortal',
2212
+ - DO: serviceProviderId,
2213
+ - FIELD: hasServiceProviderId: !!serviceProviderId
2214
+ - DO: })
2215
+ - CHECK CONDITION: if (serviceProviderId) {
2216
+ - SET portalUrl TO client.getPortalUrl(serviceProviderId, 8443)
2217
+ - DO: logger.info('SignPortal URL constructed', {
2218
+ - FIELD: component: 'SignPortal',
2219
+ - DO: portalUrl
2220
+ - DO: })
2221
+ - }
2222
+ - DO: } catch (error) {
2223
+ - DO: logger.error('SignPortal URL construction failed', {
2224
+ - FIELD: component: 'SignPortal',
2225
+ - FIELD: error: error.message
2226
+ - DO: })
2227
+ - }
2228
+ OUTPUTS:
2229
+ - Produces the section's intended result using equivalent logic.
2230
+ ```
2231
+
2232
+ ### CRUDA Operations Example
2233
+
2234
+ Complete CRUD implementation with authentication, authorization, webhooks, and performance tracking:
2235
+
2236
+ ```text
2237
+ PSEUDOCODE
2238
+ INPUTS:
2239
+ - Use values defined by the surrounding section/context.
2240
+ STEPS:
2241
+ - NOTE: protected/cruda.js
2242
+ - SET express TO require('express')
2243
+ - SET router TO express.Router()
2244
+ - DO: const { RoditClient } = require('@rodit/rodit-auth-be')
2245
+ - SET sqlite3 TO require('sqlite3')
2246
+ - DO: const { open } = require('sqlite')
2247
+ - DO: const { ulid } = require('ulid')
2248
+ - SET sdkClient TO new RoditClient()
2249
+ - SET logger TO sdkClient.getLogger()
2250
+ - DO: let db
2251
+ - NOTE: Initialize database
2252
+ - SET initializeDatabase TO async () => {
2253
+ - SET db TO await open({
2254
+ - FIELD: filename: '/app/data/database.sqlite',
2255
+ - FIELD: driver: sqlite3.Database
2256
+ - DO: })
2257
+ - WAIT FOR: db.run(`CREATE TABLE IF NOT EXISTS comments (
2258
+ - DO: id INTEGER PRIMARY KEY AUTOINCREMENT,
2259
+ - DO: comment TEXT NOT NULL,
2260
+ - DO: author TEXT,
2261
+ - DO: created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
2262
+ - DO: updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
2263
+ - DO: )`)
2264
+ - DO: }
2265
+ - NOTE: Webhook helper
2266
+ - SET logAndSendWebhook TO async (payload, req) => {
2267
+ - DO: try {
2268
+ - SET roditClient TO req?.app?.locals?.roditClient
2269
+ - CHECK CONDITION: if (!roditClient) return { success: false }
2270
+ - RETURN await roditClient.send_webhook(payload, req)
2271
+ - DO: } catch (error) {
2272
+ - FIELD: logger.error('Webhook failed', { error: error.message })
2273
+ - RETURN { success: false, error: error.message }
2274
+ - }
2275
+ - DO: }
2276
+ - NOTE: CREATE
2277
+ - DO: router.post('/create', async (req, res) => {
2278
+ - DO: const { comment, author } = req.body
2279
+ - SET requestId TO req.requestId || ulid()
2280
+ - DO: try {
2281
+ - SET result TO await db.run(
2282
+ - DO: 'INSERT INTO comments (comment, author) VALUES (?, ?)',
2283
+ - DO: [comment, author || req.user.roditId]
2284
+ - DO: )
2285
+ - NOTE: Send webhook
2286
+ - WAIT FOR: logAndSendWebhook({
2287
+ - FIELD: event: 'comment_created',
2288
+ - FIELD: data: { id: result.lastID, comment, author },
2289
+ - FIELD: isError: false
2290
+ - DO: }, req)
2291
+ - FIELD: res.json({ id: result.lastID, requestId })
2292
+ - DO: } catch (error) {
2293
+ - DO: logger.errorWithContext('Create failed', {
2294
+ - FIELD: component: 'CRUDA',
2295
+ - FIELD: error: error.message,
2296
+ - DO: requestId
2297
+ - DO: }, error)
2298
+ - FIELD: res.status(500).json({ error: 'Create failed', requestId })
2299
+ - }
2300
+ - DO: })
2301
+ - NOTE: LIST
2302
+ - DO: router.post('/list', async (req, res) => {
2303
+ - DO: try {
2304
+ - SET records TO await db.all(
2305
+ - DO: 'SELECT * FROM comments ORDER BY created_at DESC'
2306
+ - DO: )
2307
+ - FIELD: res.json({ records, requestId: req.requestId })
2308
+ - DO: } catch (error) {
2309
+ - FIELD: res.status(500).json({ error: 'List failed', requestId: req.requestId })
2310
+ - }
2311
+ - DO: })
2312
+ - NOTE: READ
2313
+ - DO: router.post('/read', async (req, res) => {
2314
+ - DO: const { id } = req.body
2315
+ - DO: try {
2316
+ - SET record TO await db.get('SELECT * FROM comments WHERE id = ?', [id])
2317
+ - CHECK CONDITION: if (!record) {
2318
+ - RETURN res.status(404).json({ error: 'Not found', requestId: req.requestId })
2319
+ - }
2320
+ - FIELD: res.json({ record, requestId: req.requestId })
2321
+ - DO: } catch (error) {
2322
+ - FIELD: res.status(500).json({ error: 'Read failed', requestId: req.requestId })
2323
+ - }
2324
+ - DO: })
2325
+ - NOTE: UPDATE
2326
+ - DO: router.post('/update', async (req, res) => {
2327
+ - DO: const { id, comment } = req.body
2328
+ - DO: try {
2329
+ - WAIT FOR: db.run(
2330
+ - DO: 'UPDATE comments SET comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
2331
+ - DO: [comment, id]
2332
+ - DO: )
2333
+ - WAIT FOR: logAndSendWebhook({
2334
+ - FIELD: event: 'comment_updated',
2335
+ - FIELD: data: { id, comment },
2336
+ - FIELD: isError: false
2337
+ - DO: }, req)
2338
+ - FIELD: res.json({ success: true, requestId: req.requestId })
2339
+ - DO: } catch (error) {
2340
+ - FIELD: res.status(500).json({ error: 'Update failed', requestId: req.requestId })
2341
+ - }
2342
+ - DO: })
2343
+ - NOTE: DELETE
2344
+ - DO: router.post('/destroy', async (req, res) => {
2345
+ - DO: const { id } = req.body
2346
+ - DO: try {
2347
+ - WAIT FOR: db.run('DELETE FROM comments WHERE id = ?', [id])
2348
+ - WAIT FOR: logAndSendWebhook({
2349
+ - FIELD: event: 'comment_deleted',
2350
+ - FIELD: data: { id },
2351
+ - FIELD: isError: false
2352
+ - DO: }, req)
2353
+ - FIELD: res.json({ success: true, requestId: req.requestId })
2354
+ - DO: } catch (error) {
2355
+ - FIELD: res.status(500).json({ error: 'Delete failed', requestId: req.requestId })
2356
+ - }
2357
+ - DO: })
2358
+ - NOTE: Export initialization function
2359
+ - DO: module.exports = router
2360
+ - DO: module.exports.initializeDatabase = initializeDatabase
2361
+ OUTPUTS:
2362
+ - Produces the section's intended result using equivalent logic.
2363
+ ```
2364
+
2365
+ ## API Reference
2366
+
2367
+ ### RoditClient Class
2368
+
2369
+ The main client class for all RODiT operations.
2370
+
2371
+ #### Static Methods
2372
+
2373
+ ##### RoditClient.create(role)
2374
+
2375
+ Create and initialize a RODiT client in one step.
2376
+
2377
+ ```text
2378
+ PSEUDOCODE
2379
+ INPUTS:
2380
+ - Use values defined by the surrounding section/context.
2381
+ STEPS:
2382
+ - SET client TO await RoditClient.create('server'); // For server applications
2383
+ - SET client TO await RoditClient.create('client'); // For client applications
2384
+ - SET client TO await RoditClient.create('portal'); // For portal authentication
2385
+ OUTPUTS:
2386
+ - Produces the section's intended result using equivalent logic.
2387
+ ```
2388
+
2389
+ **Parameters:**
2390
+ - `role` (string): Client role - `'server'`, `'client'`, or `'portal'`
2391
+
2392
+ **Returns:** `Promise<RoditClient>` - Fully initialized client instance
2393
+
2394
+ **Throws:** Error if initialization fails (e.g., missing credentials, Vault connection failure)
2395
+
2396
+ #### Instance Methods
2397
+
2398
+ ##### authenticate(req, res, next)
2399
+
2400
+ Express middleware for authenticating API requests. Validates JWT tokens and populates `req.user`.
2401
+
2402
+ ```text
2403
+ PSEUDOCODE
2404
+ INPUTS:
2405
+ - Use values defined by the surrounding section/context.
2406
+ STEPS:
2407
+ - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
2408
+ - DO: app.use('/api/protected', authenticate, handler)
2409
+ OUTPUTS:
2410
+ - Produces the section's intended result using equivalent logic.
2411
+ ```
2412
+
2413
+ **Validates:**
2414
+ - JWT signature
2415
+ - JWT expiration
2416
+ - Session exists and is active
2417
+ - Token not invalidated
2418
+ - Canonical JWT base64url encoding (header/payload/signature)
2419
+
2420
+ **Populates:** `req.user` with decoded JWT claims
2421
+
2422
+ ##### authenticateForLogout(req, res, next)
2423
+
2424
+ Express middleware for logout routes. It validates signature and claims like normal auth, but allows
2425
+ signature-valid expired JWT tokens so sessions can still be closed safely.
2426
+
2427
+ ```text
2428
+ PSEUDOCODE
2429
+ INPUTS:
2430
+ - Use values defined by the surrounding section/context.
2431
+ STEPS:
2432
+ - SET authenticateLogout TO (req, res, next) => roditClient.authenticateForLogout(req, res, next)
2433
+ - DO: app.post('/api/logout', authenticateLogout, (req, res) => roditClient.logout_client(req, res))
2434
+ OUTPUTS:
2435
+ - Produces the section's intended result using equivalent logic.
2436
+ ```
2437
+
2438
+ **Use case:** clean logout when token is expired but cryptographically valid.
2439
+
2440
+ ##### authorize(req, res, next)
2441
+
2442
+ Express middleware for validating route permissions. Must be used after `authenticate`.
2443
+
2444
+ ```text
2445
+ PSEUDOCODE
2446
+ INPUTS:
2447
+ - Use values defined by the surrounding section/context.
2448
+ STEPS:
2449
+ - SET authorize TO (req, res, next) => roditClient.authorize(req, res, next)
2450
+ - DO: app.use('/api/admin', authenticate, authorize, handler)
2451
+ OUTPUTS:
2452
+ - Produces the section's intended result using equivalent logic.
2453
+ ```
2454
+
2455
+ **Validates:** User has permission for the requested route and HTTP method
2456
+
2457
+ ##### login_client(req, res)
2458
+
2459
+ Handle Express login requests from clients. Validates RODiT credentials and issues JWT token.
2460
+
2461
+ ```text
2462
+ PSEUDOCODE
2463
+ INPUTS:
2464
+ - Use values defined by the surrounding section/context.
2465
+ STEPS:
2466
+ - DO: app.post('/api/login', (req, res) => roditClient.login_client(req, res))
2467
+ OUTPUTS:
2468
+ - Produces the section's intended result using equivalent logic.
2469
+ ```
2470
+
2471
+ **Request Body:** `login_client` accepts `accountid`, `timestamp`, and `base64url_signature`.
2472
+
2473
+ **Response:**
2474
+ ```text
2475
+ PSEUDOCODE
2476
+ INPUTS:
2477
+ - Use values defined by the surrounding section/context.
2478
+ STEPS:
2479
+ - {
2480
+ - FIELD: jwt_token: '<jwt-token>',
2481
+ - FIELD: requestId: '01HQXYZ...'
2482
+ - }
2483
+ OUTPUTS:
2484
+ - Produces the section's intended result using equivalent logic.
2485
+ ```
2486
+
2487
+ ##### logout_client(req, res)
2488
+
2489
+ Handle Express logout requests. Closes session and invalidates JWT token.
2490
+
2491
+ ```text
2492
+ PSEUDOCODE
2493
+ INPUTS:
2494
+ - Use values defined by the surrounding section/context.
2495
+ STEPS:
2496
+ - SET authenticateLogout TO (req, res, next) => roditClient.authenticateForLogout(req, res, next)
2497
+ - DO: app.post('/api/logout', authenticateLogout, (req, res) => {
2498
+ - RETURN roditClient.logout_client(req, res)
2499
+ - DO: })
2500
+ OUTPUTS:
2501
+ - Produces the section's intended result using equivalent logic.
2502
+ ```
2503
+
2504
+ **Response:**
2505
+ ```text
2506
+ PSEUDOCODE
2507
+ INPUTS:
2508
+ - Use values defined by the surrounding section/context.
2509
+ STEPS:
2510
+ - {
2511
+ - FIELD: message: 'Logout successful',
2512
+ - FIELD: terminationToken: '<jwt-token>', // Short-lived token
2513
+ - FIELD: requestId: '01HQXYZ...'
2514
+ - }
2515
+ OUTPUTS:
2516
+ - Produces the section's intended result using equivalent logic.
2517
+ ```
2518
+
2519
+ ##### login_portal(configObject, port)
2520
+
2521
+ Authenticate to RODiT portal for server-to-server operations using account-based login payloads.
2522
+
2523
+ ```text
2524
+ PSEUDOCODE
2525
+ INPUTS:
2526
+ - Use values defined by the surrounding section/context.
2527
+ STEPS:
2528
+ - SET configObject TO await roditClient.getConfigOwnRodit()
2529
+ - SET result TO await roditClient.login_portal(configObject, 8443)
2530
+ OUTPUTS:
2531
+ - Produces the section's intended result using equivalent logic.
2532
+ ```
2533
+
2534
+ **Returns:** `Promise<Object>` - Portal authentication result
2535
+
2536
+ ##### login_server(options)
2537
+
2538
+ Authenticate to a peer API using account-based login semantics: sign **`accountid + timestamp_iso`** and POST **`{ accountid, timestamp, base64url_signature }`**.
2539
+
2540
+ Optional: `options.timestamp`, `options.loginPath`.
2541
+
2542
+ ```text
2543
+ PSEUDOCODE
2544
+ INPUTS:
2545
+ - Use values defined by the surrounding section/context.
2546
+ STEPS:
2547
+ - SET result TO await roditClient.login_server({
2548
+ - FIELD: loginPath: '/api/login' // optional; default shown
2549
+ - DO: })
2550
+ OUTPUTS:
2551
+ - Produces the section's intended result using equivalent logic.
2552
+ ```
2553
+
2554
+ **Returns:** `Promise<Object>` - Authentication result with `jwt_token`
2555
+
2556
+ ##### logout_server()
2557
+
2558
+ Logout from server-to-server session.
2559
+
2560
+ ```text
2561
+ PSEUDOCODE
2562
+ INPUTS:
2563
+ - Use values defined by the surrounding section/context.
2564
+ STEPS:
2565
+ - SET result TO await roditClient.logout_server()
2566
+ OUTPUTS:
2567
+ - Produces the section's intended result using equivalent logic.
2568
+ ```
2569
+
2570
+ **Returns:** `Promise<Object>` - Logout result with session closure status
2571
+
2572
+ ##### getConfigOwnRodit()
2573
+
2574
+ Get the complete RODiT configuration including token metadata.
2575
+
2576
+ ```text
2577
+ PSEUDOCODE
2578
+ INPUTS:
2579
+ - Use values defined by the surrounding section/context.
2580
+ STEPS:
2581
+ - SET configObject TO await roditClient.getConfigOwnRodit()
2582
+ - SET metadata TO configObject.own_rodit.metadata
2583
+ - SET tokenId TO configObject.own_rodit.token_id
2584
+ OUTPUTS:
2585
+ - Produces the section's intended result using equivalent logic.
2586
+ ```
2587
+
2588
+ **Returns:** `Promise<Object>` - Complete RODiT configuration
2589
+
2590
+ **Structure:**
2591
+ ```text
2592
+ PSEUDOCODE
2593
+ INPUTS:
2594
+ - Use values defined by the surrounding section/context.
2595
+ STEPS:
2596
+ - {
2597
+ - FIELD: own_rodit: {
2598
+ - FIELD: token_id: string,
2599
+ - FIELD: metadata: {
2600
+ - FIELD: jwt_duration: number,
2601
+ - FIELD: max_requests: string,
2602
+ - FIELD: maxrq_window: string,
2603
+ - FIELD: permissioned_routes: string, // JSON string
2604
+ - FIELD: subjectuniqueidentifier_url: string,
2605
+ - FIELD: webhook_url: string,
2606
+ - NOTE: ... other metadata fields
2607
+ - }
2608
+ - DO: },
2609
+ - FIELD: port: number
2610
+ - }
2611
+ OUTPUTS:
2612
+ - Produces the section's intended result using equivalent logic.
2613
+ ```
2614
+
2615
+ ##### isOperationPermitted(method, path)
2616
+
2617
+ Check if an operation is permitted based on token permissions.
2618
+
2619
+ ```text
2620
+ PSEUDOCODE
2621
+ INPUTS:
2622
+ - Use values defined by the surrounding section/context.
2623
+ STEPS:
2624
+ - SET hasPermission TO roditClient.isOperationPermitted('POST', '/api/admin/users')
2625
+ - CHECK CONDITION: if (!hasPermission) {
2626
+ - RETURN res.status(403).json({ error: 'Forbidden' })
2627
+ - }
2628
+ OUTPUTS:
2629
+ - Produces the section's intended result using equivalent logic.
2630
+ ```
2631
+
2632
+ **Parameters:**
2633
+ - `method` (string): HTTP method
2634
+ - `path` (string): API path
2635
+
2636
+ **Returns:** `boolean`
2637
+
2638
+ ##### getStateManager()
2639
+
2640
+ Get the authentication state manager.
2641
+
2642
+ ```text
2643
+ PSEUDOCODE
2644
+ INPUTS:
2645
+ - Use values defined by the surrounding section/context.
2646
+ STEPS:
2647
+ - SET stateManager TO roditClient.getStateManager()
2648
+ OUTPUTS:
2649
+ - Produces the section's intended result using equivalent logic.
2650
+ ```
2651
+
2652
+ **Returns:** `AuthStateManager` instance
2653
+
2654
+ ##### getRoditManager()
2655
+
2656
+ Get the RODiT manager for credential operations.
2657
+
2658
+ ```text
2659
+ PSEUDOCODE
2660
+ INPUTS:
2661
+ - Use values defined by the surrounding section/context.
2662
+ STEPS:
2663
+ - SET roditManager TO roditClient.getRoditManager()
2664
+ - SET credentials TO await roditManager.getCredentials('server')
2665
+ OUTPUTS:
2666
+ - Produces the section's intended result using equivalent logic.
2667
+ ```
2668
+
2669
+ **Returns:** `RoditManager` instance
2670
+
2671
+ ##### getSessionManager()
2672
+
2673
+ Get the session manager.
2674
+
2675
+ ```text
2676
+ PSEUDOCODE
2677
+ INPUTS:
2678
+ - Use values defined by the surrounding section/context.
2679
+ STEPS:
2680
+ - SET sessionManager TO roditClient.getSessionManager()
2681
+ - SET activeCount TO await sessionManager.getActiveSessionCount()
2682
+ OUTPUTS:
2683
+ - Produces the section's intended result using equivalent logic.
2684
+ ```
2685
+
2686
+ **Returns:** `SessionManager` instance
2687
+
2688
+ ##### getLogger()
2689
+
2690
+ Get the logger instance.
2691
+
2692
+ ```text
2693
+ PSEUDOCODE
2694
+ INPUTS:
2695
+ - Use values defined by the surrounding section/context.
2696
+ STEPS:
2697
+ - SET logger TO roditClient.getLogger()
2698
+ - FIELD: logger.info('Message', { component: 'MyComponent' })
2699
+ OUTPUTS:
2700
+ - Produces the section's intended result using equivalent logic.
2701
+ ```
2702
+
2703
+ **Returns:** `Logger` instance
2704
+
2705
+ ##### getLoggingMiddleware()
2706
+
2707
+ Get the logging middleware.
2708
+
2709
+ ```text
2710
+ PSEUDOCODE
2711
+ INPUTS:
2712
+ - Use values defined by the surrounding section/context.
2713
+ STEPS:
2714
+ - SET loggingmw TO roditClient.getLoggingMiddleware()
2715
+ - DO: app.use(loggingmw)
2716
+ OUTPUTS:
2717
+ - Produces the section's intended result using equivalent logic.
2718
+ ```
2719
+
2720
+ **Returns:** Express middleware function
2721
+
2722
+ ##### getRateLimitMiddleware()
2723
+
2724
+ Get the rate limiting middleware factory.
2725
+
2726
+ ```text
2727
+ PSEUDOCODE
2728
+ INPUTS:
2729
+ - Use values defined by the surrounding section/context.
2730
+ STEPS:
2731
+ - SET ratelimitmw TO roditClient.getRateLimitMiddleware()
2732
+ - SET limiter TO ratelimitmw(100, 900); // 100 requests per 15 minutes
2733
+ - DO: app.use(limiter)
2734
+ OUTPUTS:
2735
+ - Produces the section's intended result using equivalent logic.
2736
+ ```
2737
+
2738
+ **Parameters:**
2739
+ - `maxRequests` (number): Maximum requests allowed
2740
+ - `windowSeconds` (number): Time window in seconds
2741
+
2742
+ **Returns:** Express middleware function
2743
+
2744
+ ##### getPerformanceService()
2745
+
2746
+ Get the performance tracking service.
2747
+
2748
+ ```text
2749
+ PSEUDOCODE
2750
+ INPUTS:
2751
+ - Use values defined by the surrounding section/context.
2752
+ STEPS:
2753
+ - SET performanceService TO roditClient.getPerformanceService()
2754
+ - DO: performanceService.recordRequest(req)
2755
+ - FIELD: performanceService.recordMetric('operation_duration', 150, { operation: 'db_query' })
2756
+ OUTPUTS:
2757
+ - Produces the section's intended result using equivalent logic.
2758
+ ```
2759
+
2760
+ **Returns:** `PerformanceService` instance
2761
+
2762
+ ##### getConfig()
2763
+
2764
+ Get the configuration service.
2765
+
2766
+ ```text
2767
+ PSEUDOCODE
2768
+ INPUTS:
2769
+ - Use values defined by the surrounding section/context.
2770
+ STEPS:
2771
+ - SET config TO roditClient.getConfig()
2772
+ - SET dbPath TO config.get('API_DEFAULT_OPTIONS.DB_PATH')
2773
+ OUTPUTS:
2774
+ - Produces the section's intended result using equivalent logic.
2775
+ ```
2776
+
2777
+ **Returns:** `Config` instance
2778
+
2779
+ ##### getWebhookHandler()
2780
+
2781
+ Get the webhook handler.
2782
+
2783
+ ```text
2784
+ PSEUDOCODE
2785
+ INPUTS:
2786
+ - Use values defined by the surrounding section/context.
2787
+ STEPS:
2788
+ - SET webhookHandler TO roditClient.getWebhookHandler()
2789
+ OUTPUTS:
2790
+ - Produces the section's intended result using equivalent logic.
2791
+ ```
2792
+
2793
+ **Returns:** `WebhookHandler` instance
2794
+
2795
+ ##### send_webhook(payload, req)
2796
+
2797
+ Send a webhook notification.
2798
+
2799
+ ```text
2800
+ PSEUDOCODE
2801
+ INPUTS:
2802
+ - Use values defined by the surrounding section/context.
2803
+ STEPS:
2804
+ - SET result TO await roditClient.send_webhook({
2805
+ - FIELD: event: 'user_action',
2806
+ - FIELD: data: { userId: '123', action: 'login' },
2807
+ - FIELD: isError: false
2808
+ - DO: }, req)
2809
+ OUTPUTS:
2810
+ - Produces the section's intended result using equivalent logic.
2811
+ ```
2812
+
2813
+ **Parameters:**
2814
+ - `payload` (Object): Webhook payload
2815
+ - `event` (string): Event name
2816
+ - `data` (Object): Event data
2817
+ - `isError` (boolean): Whether this is an error event
2818
+ - `req` (Object): Express request object (optional)
2819
+
2820
+ **Returns:** `Promise<Object>` - `{ success: boolean, ... }`
2821
+
2822
+ ### Exported Components
2823
+
2824
+ The SDK exports these components for direct use:
2825
+
2826
+ ```text
2827
+ PSEUDOCODE
2828
+ INPUTS:
2829
+ - Use values defined by the surrounding section/context.
2830
+ STEPS:
2831
+ - DO: const {
2832
+ - DO: RoditClient, // Main client class
2833
+ - DO: logger, // Logger instance
2834
+ - DO: stateManager, // Authentication state manager
2835
+ - DO: roditManager, // RODiT credential manager
2836
+ - DO: sessionManager, // Session manager instance
2837
+ - DO: blockchainService, // Blockchain operations
2838
+ - DO: utils, // Utility functions
2839
+ - DO: config, // Configuration service
2840
+ - DO: performanceService, // Performance tracking
2841
+ - DO: authenticate_apicall, // Authentication middleware
2842
+ - DO: authenticate_logout, // Logout authentication middleware (expired-token tolerant)
2843
+ - DO: login_client, // Login handler
2844
+ - DO: logout_client, // Logout handler
2845
+ - DO: login_client_withnep413, // NEP-413 login
2846
+ - DO: login_portal, // Portal authentication
2847
+ - DO: login_server, // Outbound peer login
2848
+ - DO: logout_server, // Server logout
2849
+ - DO: validate_jwt_token_be, // JWT validation
2850
+ - DO: generate_jwt_token, // JWT generation
2851
+ - DO: validatepermissions, // Permission middleware
2852
+ - DO: webhookHandler, // Webhook handler
2853
+ - DO: versioningMiddleware, // API versioning
2854
+ - DO: loggingmw, // Logging middleware
2855
+ - DO: ratelimitmw, // Rate limiting middleware
2856
+ - DO: versionManager, // Version manager
2857
+ - DO: VersionManager, // Version manager class
2858
+ - DO: nearorg_rpc_timestamp // Blockchain RPC timestamp function
2859
+ - DO: } = require('@rodit/rodit-auth-be')
2860
+ - NOTE: Note: Session storage configuration functions are available via:
2861
+ - NOTE: const { setExpressSessionStore, configureStorageFromConfig,
2862
+ - NOTE: createExpressSessionMiddleware, InMemorySessionStorage }
2863
+ - NOTE: = require('@rodit/rodit-auth-be/lib/auth/sessionmanager');
2864
+ OUTPUTS:
2865
+ - Produces the section's intended result using equivalent logic.
2866
+ ```
2867
+
2868
+ ### RODiT Token Metadata Fields
2869
+
2870
+ When you call `roditClient.getConfigOwnRodit()`, you get access to these metadata fields:
2871
+
2872
+ | Field | Type | Description |
2873
+ |-------|------|-------------|
2874
+ | `token_id` | string | Unique RODiT token identifier |
2875
+ | `allowed_cidr` | string | Permitted IP address ranges (CIDR format) |
2876
+ | `allowed_iso3166list` | string | Geographic restrictions (JSON string) |
2877
+ | `jwt_duration` | number | Access JWT credential lifetime in seconds (renewed until `session_exp`; does not set server session length when `SESSION_TTL_SECONDS` is configured) |
2878
+ | `max_requests` | string | Rate limit - maximum requests per window |
2879
+ | `maxrq_window` | string | Rate limit - time window in seconds |
2880
+ | `not_before` | string | Token validity start date (ISO format) |
2881
+ | `not_after` | string | Token validity end date (ISO format) |
2882
+ | `openapijson_url` | string | OpenAPI specification URL |
2883
+ | `permissioned_routes` | string | Allowed API routes and methods (JSON string) |
2884
+ | `serviceprovider_id` | string | Blockchain contract and service provider info |
2885
+ | `serviceprovider_signature` | string | Cryptographic signature for verification |
2886
+ | `subjectuniqueidentifier_url` | string | Primary API service endpoint |
2887
+ | `userselected_dn` | string | User-selected display name |
2888
+ | `webhook_cidr` | string | Allowed IP ranges for webhooks |
2889
+ | `webhook_url` | string | Webhook endpoint URL |
2890
+
2891
+ ## Best Practices
2892
+
2893
+ ### 1. Single Client Initialization
2894
+
2895
+ Always initialize the RoditClient once in your main application file:
2896
+
2897
+ ```text
2898
+ PSEUDOCODE
2899
+ INPUTS:
2900
+ - Use values defined by the surrounding section/context.
2901
+ STEPS:
2902
+ - NOTE: ✅ Good - Single initialization
2903
+ - DO: async function startServer() {
2904
+ - SET roditClient TO await RoditClient.create('server')
2905
+ - DO: app.locals.roditClient = roditClient
2906
+ - NOTE: Mount protected routes AFTER client initialization
2907
+ - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
2908
+ - SET authorize TO (req, res, next) => roditClient.authorize(req, res, next)
2909
+ - DO: app.use('/api/echo', authenticate, echoRoutes)
2910
+ - DO: app.use('/api/cruda', authenticate, authorize, crudaRoutes)
2911
+ - NOTE: ... rest of server setup
2912
+ - }
2913
+ - NOTE: ❌ Bad - Multiple initializations
2914
+ - DO: app.get('/route1', async (req, res) => {
2915
+ - SET client TO await RoditClient.create('server'); // Don't do this
2916
+ - DO: })
2917
+ OUTPUTS:
2918
+ - Produces the section's intended result using equivalent logic.
2919
+ ```
2920
+
2921
+ ### 2. Use App.locals for Shared Access
2922
+
2923
+ Store the client in `app.locals` for access across all routes:
2924
+
2925
+ ```text
2926
+ PSEUDOCODE
2927
+ INPUTS:
2928
+ - Use values defined by the surrounding section/context.
2929
+ STEPS:
2930
+ - NOTE: ✅ Good - Shared instance via app.locals
2931
+ - SET router TO express.Router()
2932
+ - DO: router.get('/data', (req, res) => {
2933
+ - SET client TO req.app.locals.roditClient
2934
+ - SET logger TO client.getLogger()
2935
+ - DO: logger.info('Processing request', {
2936
+ - FIELD: component: 'DataRoute',
2937
+ - FIELD: userId: req.user?.id,
2938
+ - FIELD: requestId: req.requestId
2939
+ - DO: })
2940
+ - FIELD: res.json({ data: 'example' })
2941
+ - DO: })
2942
+ - NOTE: ❌ Bad - Creating new instances in routes
2943
+ - DO: const { RoditClient } = require('@rodit/rodit-auth-be')
2944
+ - SET client TO new RoditClient(); // Don't do this in routes
2945
+ OUTPUTS:
2946
+ - Produces the section's intended result using equivalent logic.
2947
+ ```
2948
+
2949
+ ### 3. Proper Error Handling
2950
+
2951
+ Always wrap SDK operations in try-catch blocks and include request context:
2952
+
2953
+ ```text
2954
+ PSEUDOCODE
2955
+ INPUTS:
2956
+ - Use values defined by the surrounding section/context.
2957
+ STEPS:
2958
+ - NOTE: ✅ Good - Comprehensive error handling
2959
+ - DO: app.get('/api/data', authenticate, async (req, res) => {
2960
+ - SET startTime TO Date.now()
2961
+ - SET client TO req.app.locals.roditClient
2962
+ - SET logger TO client.getLogger()
2963
+ - DO: try {
2964
+ - SET data TO await processData(req.user.id)
2965
+ - DO: logger.infoWithContext('Request successful', {
2966
+ - FIELD: component: 'API',
2967
+ - FIELD: method: 'getData',
2968
+ - FIELD: userId: req.user.id,
2969
+ - FIELD: requestId: req.requestId,
2970
+ - FIELD: duration: Date.now() - startTime
2971
+ - DO: })
2972
+ - FIELD: res.json({ data, requestId: req.requestId })
2973
+ - DO: } catch (error) {
2974
+ - DO: logger.errorWithContext('Request failed', {
2975
+ - FIELD: component: 'API',
2976
+ - FIELD: method: 'getData',
2977
+ - FIELD: userId: req.user.id,
2978
+ - FIELD: requestId: req.requestId,
2979
+ - FIELD: duration: Date.now() - startTime,
2980
+ - FIELD: error: error.message
2981
+ - DO: }, error)
2982
+ - DO: res.status(500).json({
2983
+ - FIELD: error: 'Internal server error',
2984
+ - FIELD: requestId: req.requestId
2985
+ - DO: })
2986
+ - }
2987
+ - DO: })
2988
+ OUTPUTS:
2989
+ - Produces the section's intended result using equivalent logic.
2990
+ ```
2991
+
2992
+ ### 4. Structured Logging
2993
+
2994
+ Use consistent logging patterns with context:
2995
+
2996
+ ```text
2997
+ PSEUDOCODE
2998
+ INPUTS:
2999
+ - Use values defined by the surrounding section/context.
3000
+ STEPS:
3001
+ - NOTE: ✅ Good - Structured logging with context
3002
+ - SET logger TO req.app.locals.roditClient.getLogger()
3003
+ - DO: logger.infoWithContext('User action completed', {
3004
+ - FIELD: component: 'UserService',
3005
+ - FIELD: action: 'updateProfile',
3006
+ - FIELD: userId: user.id,
3007
+ - FIELD: requestId: req.requestId,
3008
+ - FIELD: duration: Date.now() - startTime,
3009
+ - FIELD: changes: Object.keys(updates)
3010
+ - DO: })
3011
+ - NOTE: For errors, pass the error object
3012
+ - DO: logger.errorWithContext('Operation failed', {
3013
+ - FIELD: component: 'UserService',
3014
+ - FIELD: action: 'updateProfile',
3015
+ - FIELD: userId: user.id,
3016
+ - FIELD: requestId: req.requestId,
3017
+ - FIELD: error: error.message
3018
+ - DO: }, error)
3019
+ - NOTE: ❌ Bad - Unstructured logging
3020
+ - DO: console.log('User updated profile'); // Don't do this
3021
+ OUTPUTS:
3022
+ - Produces the section's intended result using equivalent logic.
3023
+ ```
3024
+
3025
+ ### 5. Environment-Specific Configuration
3026
+
3027
+ Use environment variables for sensitive and environment-specific values:
3028
+
3029
+ ```text
3030
+ PSEUDOCODE
3031
+ INPUTS:
3032
+ - Use values defined by the surrounding section/context.
3033
+ STEPS:
3034
+ - NOTE: ✅ Good - Environment-aware configuration
3035
+ - SET config TO roditClient.getConfig()
3036
+ - SET logLevel TO config.get('LOG_LEVEL', 'info')
3037
+ - SET isMainDeploy TO ['info', 'warn', 'error'].includes(logLevel)
3038
+ - NOTE: Main should use vault credentials
3039
+ - CHECK CONDITION: if (isMainDeploy && process.env.RODIT_NEAR_CREDENTIALS_SOURCE !== 'vault') {
3040
+ - DO: logger.warn('Main environment should use vault credentials', {
3041
+ - FIELD: component: 'Configuration',
3042
+ - FIELD: environment: 'main',
3043
+ - FIELD: credentialsSource: process.env.RODIT_NEAR_CREDENTIALS_SOURCE || 'not-set'
3044
+ - DO: })
3045
+ - }
3046
+ - NOTE: Configure session storage before initializing client
3047
+ - CHECK CONDITION: if (isMainDeploy) {
3048
+ - SET SQLiteStore TO require('connect-sqlite3')(require('express-session'))
3049
+ - SET sessionStore TO new SQLiteStore({
3050
+ - FIELD: db: 'sessions.db',
3051
+ - FIELD: dir: config.get('API_DEFAULT_OPTIONS.DB_PATH', './data')
3052
+ - DO: })
3053
+ - DO: setExpressSessionStore(sessionStore)
3054
+ - }
3055
+ OUTPUTS:
3056
+ - Produces the section's intended result using equivalent logic.
3057
+ ```
3058
+
3059
+ ### 6. Graceful Shutdown
3060
+
3061
+ Implement proper shutdown handling:
3062
+
3063
+ ```text
3064
+ PSEUDOCODE
3065
+ INPUTS:
3066
+ - Use values defined by the surrounding section/context.
3067
+ STEPS:
3068
+ - NOTE: ✅ Good - Graceful shutdown
3069
+ - SET shutdown TO async (signal) => {
3070
+ - SET logger TO roditClient.getLogger()
3071
+ - DO: logger.info('Shutting down gracefully', {
3072
+ - FIELD: component: 'AppLifecycle',
3073
+ - FIELD: signal: signal || 'unknown',
3074
+ - FIELD: time: new Date().toISOString()
3075
+ - DO: })
3076
+ - CHECK CONDITION: if (server) {
3077
+ - DO: server.close(async () => {
3078
+ - DO: logger.info('HTTP server closed')
3079
+ - NOTE: Close database connections
3080
+ - CHECK CONDITION: if (db && typeof db.close === 'function') {
3081
+ - WAIT FOR: db.close()
3082
+ - DO: logger.info('Database connections closed')
3083
+ - }
3084
+ - NOTE: Close session store
3085
+ - CHECK CONDITION: if (sessionStore && typeof sessionStore.close === 'function') {
3086
+ - WAIT FOR: new Promise((resolve) => sessionStore.close(resolve))
3087
+ - DO: logger.info('Session store closed')
3088
+ - }
3089
+ - DO: process.exit(0)
3090
+ - DO: })
3091
+ - NOTE: Force shutdown after timeout
3092
+ - DO: setTimeout(() => {
3093
+ - DO: logger.error('Forced shutdown after timeout')
3094
+ - DO: process.exit(1)
3095
+ - DO: }, 10000)
3096
+ - }
3097
+ - DO: }
3098
+ - DO: process.on('SIGTERM', () => shutdown('SIGTERM'))
3099
+ - DO: process.on('SIGINT', () => shutdown('SIGINT'))
3100
+ OUTPUTS:
3101
+ - Produces the section's intended result using equivalent logic.
3102
+ ```
3103
+
3104
+ ### 7. Request Context and Performance Tracking
3105
+
3106
+ Always include request context and track performance:
3107
+
3108
+ ```text
3109
+ PSEUDOCODE
3110
+ INPUTS:
3111
+ - Use values defined by the surrounding section/context.
3112
+ STEPS:
3113
+ - NOTE: ✅ Good - Request context and performance tracking
3114
+ - DO: app.use((req, res, next) => {
3115
+ - DO: req.requestId = req.headers['x-request-id'] || ulid()
3116
+ - DO: req.startTime = Date.now()
3117
+ - DO: next()
3118
+ - DO: })
3119
+ - NOTE: Performance monitoring
3120
+ - DO: app.use((req, res, next) => {
3121
+ - SET performanceService TO roditClient.getPerformanceService()
3122
+ - CHECK CONDITION: if (performanceService) {
3123
+ - DO: performanceService.recordRequest(req)
3124
+ - }
3125
+ - DO: res.on('finish', () => {
3126
+ - SET duration TO Date.now() - req.startTime
3127
+ - CHECK CONDITION: if (performanceService) {
3128
+ - DO: performanceService.recordMetric('request_duration_ms', duration, {
3129
+ - FIELD: method: req.method,
3130
+ - FIELD: path: req.path,
3131
+ - FIELD: status: res.statusCode
3132
+ - DO: })
3133
+ - CHECK CONDITION: if (res.statusCode >= 400) {
3134
+ - DO: performanceService.recordMetric('error_count', 1, {
3135
+ - FIELD: method: req.method,
3136
+ - FIELD: path: req.path,
3137
+ - FIELD: status: res.statusCode
3138
+ - DO: })
3139
+ - }
3140
+ - }
3141
+ - DO: })
3142
+ - DO: next()
3143
+ - DO: })
3144
+ OUTPUTS:
3145
+ - Produces the section's intended result using equivalent logic.
3146
+ ```
3147
+
3148
+ ### 8. Login Endpoint Protection
3149
+
3150
+ **CRITICAL:** Never protect the login endpoint with authentication middleware:
3151
+
3152
+ ```text
3153
+ PSEUDOCODE
3154
+ INPUTS:
3155
+ - Use values defined by the surrounding section/context.
3156
+ STEPS:
3157
+ - NOTE: ✅ Good - Login endpoint without authentication
3158
+ - DO: app.post('/api/login', (req, res) => {
3159
+ - DO: req.logAction = 'login-attempt'
3160
+ - RETURN roditClient.login_client(req, res)
3161
+ - DO: })
3162
+ - NOTE: ❌ Bad - Login endpoint with authentication (creates circular dependency)
3163
+ - DO: app.post('/api/login', authenticate, (req, res) => { // DON'T DO THIS
3164
+ - RETURN roditClient.login_client(req, res)
3165
+ - DO: })
3166
+ - NOTE: ✅ Better - Logout endpoint with logout-specific authentication
3167
+ - SET authenticateLogout TO (req, res, next) => roditClient.authenticateForLogout(req, res, next)
3168
+ - DO: app.post('/api/logout', authenticateLogout, (req, res) => {
3169
+ - DO: req.logAction = 'logout-attempt'
3170
+ - RETURN roditClient.logout_client(req, res)
3171
+ - DO: })
3172
+ OUTPUTS:
3173
+ - Produces the section's intended result using equivalent logic.
3174
+ ```
3175
+
3176
+ ### 9. Route Mounting Order
3177
+
3178
+ Mount protected routes AFTER client initialization:
3179
+
3180
+ ```text
3181
+ PSEUDOCODE
3182
+ INPUTS:
3183
+ - Use values defined by the surrounding section/context.
3184
+ STEPS:
3185
+ - NOTE: ✅ Good - Correct order
3186
+ - DO: async function startServer() {
3187
+ - NOTE: 1. Configure session storage
3188
+ - DO: setExpressSessionStore(sessionStore)
3189
+ - NOTE: 2. Initialize client
3190
+ - SET roditClient TO await RoditClient.create('server')
3191
+ - DO: app.locals.roditClient = roditClient
3192
+ - NOTE: 3. Create middleware
3193
+ - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
3194
+ - SET authenticateLogout TO (req, res, next) => roditClient.authenticateForLogout(req, res, next)
3195
+ - SET authorize TO (req, res, next) => roditClient.authorize(req, res, next)
3196
+ - NOTE: 4. Mount public routes
3197
+ - DO: app.post('/api/login', loginRoute)
3198
+ - NOTE: 5. Mount protected routes
3199
+ - DO: app.use('/api/echo', authenticate, echoRoutes)
3200
+ - DO: app.use('/api/cruda', authenticate, authorize, crudaRoutes)
3201
+ - DO: app.post('/api/logout', authenticateLogout, logoutRoute)
3202
+ - NOTE: 6. Start server
3203
+ - DO: app.listen(port)
3204
+ - }
3205
+ - NOTE: ❌ Bad - Routes mounted before client initialization
3206
+ - DO: app.use('/api/echo', authenticate, echoRoutes); // authenticate is undefined!
3207
+ - SET roditClient TO await RoditClient.create('server')
3208
+ OUTPUTS:
3209
+ - Produces the section's intended result using equivalent logic.
3210
+ ```
3211
+
3212
+ ## Troubleshooting
3213
+
3214
+ ### Common Issues
3215
+
3216
+ #### 1. Authentication Middleware Errors
3217
+
3218
+ **Problem:** `roditClient.authenticate is not a function` or `Cannot read properties of undefined`
3219
+
3220
+ **Solution:** Ensure client is initialized and stored in app.locals:
3221
+ ```text
3222
+ PSEUDOCODE
3223
+ INPUTS:
3224
+ - Use values defined by the surrounding section/context.
3225
+ STEPS:
3226
+ - NOTE: ✅ Correct - Check client availability
3227
+ - SET authenticate TO (req, res, next) => {
3228
+ - SET client TO req.app.locals.roditClient
3229
+ - CHECK CONDITION: if (!client) {
3230
+ - RETURN res.status(503).json({ error: 'Authentication service unavailable' })
3231
+ - }
3232
+ - RETURN client.authenticate(req, res, next)
3233
+ - DO: }
3234
+ - NOTE: ❌ Wrong - Direct access without checking
3235
+ - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
3236
+ - NOTE: This fails if roditClient is not initialized yet
3237
+ OUTPUTS:
3238
+ - Produces the section's intended result using equivalent logic.
3239
+ ```
3240
+
3241
+ #### 2. Configuration Not Found
3242
+
3243
+ **Problem:** `Failed to initialize RODiT configuration`
3244
+
3245
+ **Solutions:**
3246
+ ```text
3247
+ PSEUDOCODE
3248
+ INPUTS:
3249
+ - Use values defined by the surrounding section/context.
3250
+ STEPS:
3251
+ - NOTE: Check environment variables
3252
+ - DO: echo $RODIT_NEAR_CREDENTIALS_SOURCE # Should be 'vault' or 'file'
3253
+ - DO: echo $VAULT_ENDPOINT
3254
+ - DO: echo $NEAR_CONTRACT_ID
3255
+ - DO: echo $SERVICE_NAME
3256
+ - NOTE: For vault-based credentials
3257
+ - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=vault
3258
+ - FIELD: export VAULT_ENDPOINT=https://vault.example.com
3259
+ - DO: export VAULT_ROLE_ID=your-role-id
3260
+ - DO: export VAULT_SECRET_ID=your-secret-id
3261
+ - NOTE: For file-based credentials (development)
3262
+ - DO: export RODIT_NEAR_CREDENTIALS_SOURCE=file
3263
+ - DO: export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json
3264
+ OUTPUTS:
3265
+ - Produces the section's intended result using equivalent logic.
3266
+ ```
3267
+
3268
+ **Verify configuration:**
3269
+ ```text
3270
+ PSEUDOCODE
3271
+ INPUTS:
3272
+ - Use values defined by the surrounding section/context.
3273
+ STEPS:
3274
+ - SET config TO roditClient.getConfig()
3275
+ - FIELD: console.log('NEAR_CONTRACT_ID:', config.get('NEAR_CONTRACT_ID'))
3276
+ - FIELD: console.log('SERVICE_NAME:', config.get('SERVICE_NAME'))
3277
+ OUTPUTS:
3278
+ - Produces the section's intended result using equivalent logic.
3279
+ ```
3280
+
3281
+ #### 3. Missing App.locals Client
3282
+
3283
+ **Problem:** `RoditClient not available in app.locals` or `Cannot read properties of undefined (reading 'roditClient')`
3284
+
3285
+ **Solution:** Ensure client is stored during initialization:
3286
+ ```text
3287
+ PSEUDOCODE
3288
+ INPUTS:
3289
+ - Use values defined by the surrounding section/context.
3290
+ STEPS:
3291
+ - DO: async function startServer() {
3292
+ - DO: try {
3293
+ - NOTE: Initialize client
3294
+ - SET roditClient TO await RoditClient.create('server')
3295
+ - NOTE: Store in app.locals BEFORE mounting routes
3296
+ - DO: app.locals.roditClient = roditClient
3297
+ - NOTE: Verify it's stored
3298
+ - CHECK CONDITION: if (!app.locals.roditClient) {
3299
+ - DO: throw new Error('Failed to store roditClient in app.locals')
3300
+ - }
3301
+ - NOTE: Now mount routes
3302
+ - SET authenticate TO (req, res, next) => roditClient.authenticate(req, res, next)
3303
+ - DO: app.use('/api/protected', authenticate, protectedRoutes)
3304
+ - DO: app.listen(port)
3305
+ - DO: } catch (error) {
3306
+ - FIELD: console.error('Server initialization failed:', error)
3307
+ - DO: process.exit(1)
3308
+ - }
3309
+ - }
3310
+ OUTPUTS:
3311
+ - Produces the section's intended result using equivalent logic.
3312
+ ```
3313
+
3314
+ #### 4. Permission Denied Errors
3315
+
3316
+ **Problem:** Routes return 403 Forbidden
3317
+
3318
+ **Debug steps:**
3319
+ ```text
3320
+ PSEUDOCODE
3321
+ INPUTS:
3322
+ - Use values defined by the surrounding section/context.
3323
+ STEPS:
3324
+ - NOTE: Check token permissions
3325
+ - SET configObject TO await roditClient.getConfigOwnRodit()
3326
+ - SET permissionedRoutes TO JSON.parse(
3327
+ - DO: configObject.own_rodit.metadata.permissioned_routes || '{}'
3328
+ - DO: )
3329
+ - FIELD: console.log('Configured permissions:', permissionedRoutes)
3330
+ - NOTE: Check specific operation
3331
+ - SET hasPermission TO roditClient.isOperationPermitted('POST', '/api/cruda/create')
3332
+ - FIELD: console.log('Has permission:', hasPermission)
3333
+ - NOTE: Verify route path matches exactly
3334
+ - FIELD: console.log('Requested path:', req.path); // Must match permission key exactly
3335
+ OUTPUTS:
3336
+ - Produces the section's intended result using equivalent logic.
3337
+ ```
3338
+
3339
+ **Common issues:**
3340
+ - Route path doesn't match permission key exactly (e.g., `/api/cruda/create` vs `/cruda/create`)
3341
+ - HTTP method not allowed in permission configuration
3342
+ - Permission format incorrect (should be `"+0"` for all methods)
3343
+ - Client token has different permissions than server token
3344
+
3345
+ #### 5. Session Not Found Errors
3346
+
3347
+ **Problem:** `401 Unauthorized - session_not_found`
3348
+
3349
+ **Cause:** JWT token contains session ID that doesn't exist in session storage
3350
+
3351
+ **Solutions:**
3352
+ ```text
3353
+ PSEUDOCODE
3354
+ INPUTS:
3355
+ - Use values defined by the surrounding section/context.
3356
+ STEPS:
3357
+ - NOTE: Verify session storage is configured
3358
+ - SET sessionManager TO roditClient.getSessionManager()
3359
+ - SET storageInfo TO await sessionManager.getStorageInfo()
3360
+ - FIELD: console.log('Storage type:', storageInfo.storageType)
3361
+ - FIELD: console.log('Active sessions:', storageInfo.sessionCount)
3362
+ - NOTE: Check if token is invalidated
3363
+ - SET isInvalidated TO await sessionManager.isTokenInvalidated(jwtToken)
3364
+ - FIELD: console.log('Token invalidated:', isInvalidated)
3365
+ - NOTE: Enumerate sessions via storage for debugging
3366
+ - SET allSessions TO await sessionManager.storage.getAll()
3367
+ - FIELD: console.log('Active sessions:', allSessions.filter(s => s.status === 'active').length)
3368
+ OUTPUTS:
3369
+ - Produces the section's intended result using equivalent logic.
3370
+ ```
3371
+
3372
+ **Common causes:**
3373
+ - Server restarted with in-memory storage (sessions lost)
3374
+ - Session expired
3375
+ - Token was invalidated by logout
3376
+ - Session storage not configured properly
3377
+
3378
+ **Solution:** Use persistent storage (SQLite or Redis) for main
3379
+
3380
+ #### 6. Logging Issues
3381
+
3382
+ **Problem:** Logs not appearing in Loki or console
3383
+
3384
+ **Solutions:**
3385
+ ```text
3386
+ PSEUDOCODE
3387
+ INPUTS:
3388
+ - Use values defined by the surrounding section/context.
3389
+ STEPS:
3390
+ - NOTE: Check logging configuration
3391
+ - DO: export LOG_LEVEL=debug # Enable debug logging
3392
+ - FIELD: export LOKI_URL=https://loki.example.com:3100
3393
+ - FIELD: export LOKI_BASIC_AUTH=username:password
3394
+ OUTPUTS:
3395
+ - Produces the section's intended result using equivalent logic.
3396
+ ```
3397
+
3398
+ ```text
3399
+ PSEUDOCODE
3400
+ INPUTS:
3401
+ - Use values defined by the surrounding section/context.
3402
+ STEPS:
3403
+ - NOTE: Test logger directly
3404
+ - SET logger TO roditClient.getLogger()
3405
+ - FIELD: logger.info('Test message', { component: 'Test' })
3406
+ - FIELD: logger.error('Test error', { component: 'Test' })
3407
+ - NOTE: Check if Loki transport is configured
3408
+ - SET transports TO logger.transports
3409
+ - FIELD: console.log('Logger transports:', transports.map(t => t.name))
3410
+ OUTPUTS:
3411
+ - Produces the section's intended result using equivalent logic.
3412
+ ```
3413
+
3414
+ ### Debug Mode
3415
+
3416
+ Enable debug logging for troubleshooting:
3417
+
3418
+ ```text
3419
+ PSEUDOCODE
3420
+ INPUTS:
3421
+ - Use values defined by the surrounding section/context.
3422
+ STEPS:
3423
+ - DO: export LOG_LEVEL=debug # Use 'debug' or 'trace' for development mode
3424
+ OUTPUTS:
3425
+ - Produces the section's intended result using equivalent logic.
3426
+ ```
3427
+
3428
+ This will provide detailed information about:
3429
+ - Authentication flows and token validation
3430
+ - Configuration loading from Vault/files
3431
+ - Permission checks and route matching
3432
+ - Session creation and validation
3433
+ - Network requests to portal/blockchain
3434
+ - Internal SDK operations
3435
+ - Request/response details
3436
+
3437
+ **Example debug output:**
3438
+ ```text
3439
+ PSEUDOCODE
3440
+ INPUTS:
3441
+ - Use values defined by the surrounding section/context.
3442
+ STEPS:
3443
+ - SET logger TO roditClient.getLogger()
3444
+ - NOTE: Enable debug logging programmatically
3445
+ - DO: logger.level = 'debug'
3446
+ - NOTE: Debug authentication
3447
+ - DO: logger.debug('Authenticating request', {
3448
+ - FIELD: component: 'Authentication',
3449
+ - FIELD: hasAuthHeader: !!req.headers.authorization,
3450
+ - FIELD: path: req.path,
3451
+ - FIELD: method: req.method
3452
+ - DO: })
3453
+ OUTPUTS:
3454
+ - Produces the section's intended result using equivalent logic.
3455
+ ```
3456
+
3457
+ ### Health Checks
3458
+
3459
+ Implement comprehensive health check endpoints:
3460
+
3461
+ ```text
3462
+ PSEUDOCODE
3463
+ INPUTS:
3464
+ - Use values defined by the surrounding section/context.
3465
+ STEPS:
3466
+ - DO: app.get('/health', async (req, res) => {
3467
+ - DO: try {
3468
+ - SET client TO req.app.locals.roditClient
3469
+ - CHECK CONDITION: if (!client) {
3470
+ - RETURN res.status(503).json({
3471
+ - FIELD: status: 'error',
3472
+ - FIELD: message: 'RoditClient not available'
3473
+ - DO: })
3474
+ - }
3475
+ - SET configObject TO await client.getConfigOwnRodit()
3476
+ - SET sessionManager TO client.getSessionManager()
3477
+ - SET performanceService TO client.getPerformanceService()
3478
+ - SET health TO {
3479
+ - FIELD: status: 'healthy',
3480
+ - FIELD: timestamp: new Date().toISOString(),
3481
+ - FIELD: logLevel: config.get('LOG_LEVEL', 'info'),
3482
+ - FIELD: components: {
3483
+ - FIELD: roditClient: !!client,
3484
+ - FIELD: configuration: !!(configObject && configObject.own_rodit),
3485
+ - FIELD: sessionManager: !!sessionManager,
3486
+ - FIELD: performanceService: !!performanceService
3487
+ - DO: },
3488
+ - FIELD: metrics: {
3489
+ - FIELD: activeSessions: await sessionManager.getActiveSessionCount(),
3490
+ - FIELD: totalRequests: performanceService.getRequestCount(),
3491
+ - FIELD: errorCount: performanceService.getErrorCount()
3492
+ - DO: },
3493
+ - FIELD: roditToken: {
3494
+ - FIELD: tokenId: configObject?.own_rodit?.token_id,
3495
+ - FIELD: apiUrl: configObject?.own_rodit?.metadata?.subjectuniqueidentifier_url,
3496
+ - FIELD: jwtDuration: configObject?.own_rodit?.metadata?.jwt_duration
3497
+ - }
3498
+ - DO: }
3499
+ - DO: res.json(health)
3500
+ - DO: } catch (error) {
3501
+ - DO: res.status(503).json({
3502
+ - FIELD: status: 'error',
3503
+ - FIELD: message: error.message,
3504
+ - FIELD: timestamp: new Date().toISOString()
3505
+ - DO: })
3506
+ - }
3507
+ - DO: })
3508
+ - NOTE: Readiness check (for Kubernetes)
3509
+ - DO: app.get('/ready', async (req, res) => {
3510
+ - SET client TO req.app.locals.roditClient
3511
+ - CHECK CONDITION: if (!client) {
3512
+ - RETURN res.status(503).json({ ready: false })
3513
+ - }
3514
+ - DO: try {
3515
+ - SET configObject TO await client.getConfigOwnRodit()
3516
+ - SET ready TO !!(configObject && configObject.own_rodit)
3517
+ - FIELD: res.status(ready ? 200 : 503).json({ ready })
3518
+ - DO: } catch (error) {
3519
+ - FIELD: res.status(503).json({ ready: false, error: error.message })
3520
+ - }
3521
+ - DO: })
3522
+ - NOTE: Liveness check (for Kubernetes)
3523
+ - DO: app.get('/live', (req, res) => {
3524
+ - FIELD: res.json({ alive: true })
3525
+ - DO: })
3526
+ OUTPUTS:
3527
+ - Produces the section's intended result using equivalent logic.
3528
+ ```
3529
+
3530
+ ### Support
3531
+
3532
+ For additional support:
3533
+ 1. Check the debug logs with `LOG_LEVEL=debug`
3534
+ 2. Verify your RODiT token configuration
3535
+ 3. Test with the health check endpoint
3536
+ 4. Review the authentication flow in the logs
3537
+ 5. Ensure all required environment variables are set
3538
+
3539
+ ---
3540
+
3541
+ ## License
3542
+
3543
+ Copyright (c) 2026 Discernible IO. All rights reserved.