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