@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/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
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.
|