@rodit/rodit-auth-be 9.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
package/index.js
ADDED
|
@@ -0,0 +1,1884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RODiT Client Interface
|
|
3
|
+
* Provides a clean API for interacting with RODiT services
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { ulid } = require("ulid");
|
|
9
|
+
const roditManager = require('./lib/auth/roditmanager');
|
|
10
|
+
const stateManager = require('./lib/blockchain/statemanager');
|
|
11
|
+
const authMw = require('./lib/middleware/authenticationmw');
|
|
12
|
+
const { ensureProtocol, isRoditUnboundedDate } = require('./services/utils');
|
|
13
|
+
const { versionManager } = require('./services/versionmanager');
|
|
14
|
+
|
|
15
|
+
// Import all SDK components that need to be accessible through RoditClient
|
|
16
|
+
const {
|
|
17
|
+
authenticate_apicall,
|
|
18
|
+
authenticate_logout,
|
|
19
|
+
login_client,
|
|
20
|
+
logout_client,
|
|
21
|
+
login_client_withnep413,
|
|
22
|
+
login_portal,
|
|
23
|
+
login_server,
|
|
24
|
+
logout_server
|
|
25
|
+
} = require('./lib/middleware/authenticationmw');
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
validate_jwt_token_be,
|
|
29
|
+
generate_jwt_token
|
|
30
|
+
} = require('./lib/auth/tokenservice');
|
|
31
|
+
|
|
32
|
+
const validatepermissions = require('./lib/middleware/validatepermissionsmw');
|
|
33
|
+
const { sessionManager } = require('./lib/auth/sessionmanager');
|
|
34
|
+
const blockchainService = require('./lib/blockchain/blockchainservice');
|
|
35
|
+
const webhookHandler = require('./lib/middleware/webhookhandlermw');
|
|
36
|
+
const { versioningMiddleware } = require('./lib/middleware/versioningmw');
|
|
37
|
+
const { VersionManager } = require('./services/versionmanager');
|
|
38
|
+
const loggingmw = require('./lib/middleware/loggingmw');
|
|
39
|
+
const ratelimitmw = require('./lib/middleware/ratelimitmw');
|
|
40
|
+
const utils = require('./services/utils');
|
|
41
|
+
const config = require('./services/configsdk');
|
|
42
|
+
const performanceService = require('./services/performanceservice');
|
|
43
|
+
const errorResponse = require('./services/error-response');
|
|
44
|
+
const { sendError, buildErrorResponse } = errorResponse;
|
|
45
|
+
|
|
46
|
+
// Use the proper logger service
|
|
47
|
+
const logger = require('./services/logger');
|
|
48
|
+
// Avoid circular dependency - will require filecredentialsstore dynamically when needed
|
|
49
|
+
|
|
50
|
+
function sessionFieldsFromJwt(token) {
|
|
51
|
+
if (!token || typeof token !== "string") {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
const parts = token.split(".");
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
60
|
+
return {
|
|
61
|
+
sessionId: payload.session_id,
|
|
62
|
+
expiresAt: payload.session_exp,
|
|
63
|
+
createdAt: payload.session_iat,
|
|
64
|
+
};
|
|
65
|
+
} catch (_error) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* RODiT Client Interface
|
|
72
|
+
* Provides a clean API for interacting with RODiT services
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const { RoditClient } = require('@rodit/rodit-sdk');
|
|
76
|
+
* const client = new RoditClient();
|
|
77
|
+
* // Initialize the client
|
|
78
|
+
* await client.init();
|
|
79
|
+
*/
|
|
80
|
+
class RoditClient {
|
|
81
|
+
/**
|
|
82
|
+
* Create a new RODiT client
|
|
83
|
+
* @param {Object} [rcoptions] - Optional configuration
|
|
84
|
+
* @param {string} [rcoptions.credentialsFilePath] - Path to credentials file
|
|
85
|
+
* @param {boolean} [rcoptions.testMode] - Enable test mode for multiple instances
|
|
86
|
+
*/
|
|
87
|
+
constructor(rcoptions = {}) {
|
|
88
|
+
this.requestId = ulid();
|
|
89
|
+
this.initialized = false;
|
|
90
|
+
this.testMode = rcoptions.testMode || false;
|
|
91
|
+
|
|
92
|
+
// Store configuration directly as instance properties
|
|
93
|
+
this.credentialsFilePath = rcoptions.credentialsFilePath;
|
|
94
|
+
// Set API version from config (env var), constructor option, or config default
|
|
95
|
+
this.apiVersion = rcoptions.apiVersion || config.get('API_VERSION');
|
|
96
|
+
|
|
97
|
+
// Create test instance of stateManager if in test mode
|
|
98
|
+
if (this.testMode) {
|
|
99
|
+
const { AuthStateManager } = require('./lib/blockchain/statemanager');
|
|
100
|
+
this.stateManager = AuthStateManager.createTestInstance();
|
|
101
|
+
logger.debug('Created test instance of stateManager', {
|
|
102
|
+
component: 'RoditClient',
|
|
103
|
+
method: 'constructor',
|
|
104
|
+
requestId: this.requestId,
|
|
105
|
+
testMode: true,
|
|
106
|
+
stateManagerInstanceId: this.stateManager.instanceId
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
// Use the singleton stateManager for normal operation
|
|
110
|
+
this.stateManager = stateManager;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Always configure version manager with the determined API version
|
|
114
|
+
versionManager.setVersion(this.apiVersion);
|
|
115
|
+
|
|
116
|
+
logger.debug('RODiT client instance created', {
|
|
117
|
+
component: 'RoditClient',
|
|
118
|
+
method: 'constructor',
|
|
119
|
+
requestId: this.requestId,
|
|
120
|
+
apiVersion: this.apiVersion,
|
|
121
|
+
testMode: this.testMode,
|
|
122
|
+
hasIndependentStateManager: this.testMode
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get configuration object
|
|
129
|
+
* @returns {Object} Configuration object
|
|
130
|
+
*/
|
|
131
|
+
getConfig() {
|
|
132
|
+
return config;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get authentication middleware (clean syntax)
|
|
137
|
+
* @returns {Function} Authentication middleware function
|
|
138
|
+
*/
|
|
139
|
+
get authenticate() {
|
|
140
|
+
return authenticate_apicall;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get logout-specific authentication middleware.
|
|
145
|
+
* Allows signature-valid expired tokens to reach logout handler.
|
|
146
|
+
*
|
|
147
|
+
* @returns {Function} Logout authentication middleware function
|
|
148
|
+
*/
|
|
149
|
+
get authenticateForLogout() {
|
|
150
|
+
return authenticate_logout;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get permissions validation middleware (clean syntax)
|
|
155
|
+
* @returns {Function} Permissions validation middleware function
|
|
156
|
+
*/
|
|
157
|
+
get authorize() {
|
|
158
|
+
return validatepermissions;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Login client with NEP413
|
|
165
|
+
* @param {Object} credentials - NEP413 credentials
|
|
166
|
+
* @returns {Promise<Object>} Login result
|
|
167
|
+
*/
|
|
168
|
+
async loginClientWithNEP413(credentials) {
|
|
169
|
+
return login_client_withnep413(credentials);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate JWT token
|
|
174
|
+
* @param {string} token - JWT token to validate
|
|
175
|
+
* @param {Object} vtoptions - Validation vtoptions
|
|
176
|
+
* @returns {Promise<Object>} Validation result
|
|
177
|
+
*/
|
|
178
|
+
async validateToken(token, vtoptions = {}) {
|
|
179
|
+
return validate_jwt_token_be(token, vtoptions);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generate JWT token
|
|
184
|
+
* @param {Object} payload - Token payload
|
|
185
|
+
* @param {Object} gtoptions - Generation gtoptions
|
|
186
|
+
* @returns {Promise<string>} Generated token
|
|
187
|
+
*/
|
|
188
|
+
async generateToken(payload, gtoptions = {}) {
|
|
189
|
+
return generate_jwt_token(payload, gtoptions);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get session manager instance
|
|
194
|
+
* @returns {Object} Session manager
|
|
195
|
+
*/
|
|
196
|
+
getSessionManager() {
|
|
197
|
+
return sessionManager;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get session storage information
|
|
202
|
+
* @returns {Object} Storage information including type and session count
|
|
203
|
+
*/
|
|
204
|
+
async getSessionStorageInfo() {
|
|
205
|
+
try {
|
|
206
|
+
const storage = sessionManager.storage;
|
|
207
|
+
const info = {
|
|
208
|
+
storageType: storage.constructor?.name || 'UnknownStorage',
|
|
209
|
+
sessionCount: await storage.size(),
|
|
210
|
+
hasGetStorageInfo: typeof storage.getStorageInfo === 'function'
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// If the storage has a getStorageInfo method (like InMemorySessionStorage), use it
|
|
214
|
+
if (info.hasGetStorageInfo) {
|
|
215
|
+
const detailedInfo = storage.getStorageInfo();
|
|
216
|
+
return { ...info, ...detailedInfo };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return info;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logger.errorWithContext('Failed to get session storage info', {
|
|
222
|
+
component: 'RoditClient',
|
|
223
|
+
method: 'getSessionStorageInfo'
|
|
224
|
+
}, error);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
storageType: 'Unknown',
|
|
228
|
+
sessionCount: 0,
|
|
229
|
+
error: error.message
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get blockchain service instance
|
|
236
|
+
* @returns {Object} Blockchain service
|
|
237
|
+
*/
|
|
238
|
+
getBlockchainService() {
|
|
239
|
+
return blockchainService;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get state manager instance
|
|
244
|
+
* @returns {Object} State manager
|
|
245
|
+
*/
|
|
246
|
+
getStateManager() {
|
|
247
|
+
return this.stateManager;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get client state information
|
|
252
|
+
* @returns {Object} Client state information
|
|
253
|
+
*/
|
|
254
|
+
getClientState() {
|
|
255
|
+
return {
|
|
256
|
+
initialized: this.initialized,
|
|
257
|
+
testMode: this.testMode,
|
|
258
|
+
hasToken: !!this.jwt_token,
|
|
259
|
+
sessionId: this.sessionId,
|
|
260
|
+
apiEndpoint: this.apiendpoint,
|
|
261
|
+
webhookUrl: this.webhookUrl,
|
|
262
|
+
openApiUrl: this.openApiUrl,
|
|
263
|
+
isTokenValid: this.isTokenValid(),
|
|
264
|
+
isSubscriptionActive: this.isSubscriptionActive(),
|
|
265
|
+
stateManagerId: this.stateManager?.instanceId || 'singleton'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get webhook handler
|
|
271
|
+
* @returns {Object} Webhook handler
|
|
272
|
+
*/
|
|
273
|
+
getWebhookHandler() {
|
|
274
|
+
return webhookHandler;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Send webhook to custom endpoint
|
|
279
|
+
* @param {Object} data - Webhook payload object
|
|
280
|
+
* @param {string} endpoint - Target endpoint path (e.g., '/webhook', '/hooks/wake', '/hooks/agent')
|
|
281
|
+
* @param {Object} [req] - Express request (for deriving peer webhook URL and headers)
|
|
282
|
+
* @returns {Promise<Object>} Webhook result
|
|
283
|
+
*/
|
|
284
|
+
async sendWebhookToEndpoint(data, endpoint, req) {
|
|
285
|
+
if (webhookHandler.send_webhook) {
|
|
286
|
+
return webhookHandler.send_webhook(data, req, { endpoint });
|
|
287
|
+
}
|
|
288
|
+
throw new Error('Webhook functionality not available');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Send webhook (backward compatibility alias)
|
|
293
|
+
* @param {Object} data - Webhook payload object
|
|
294
|
+
* @param {Object} [req] - Express request (for deriving peer webhook URL and headers)
|
|
295
|
+
* @returns {Promise<Object>} Webhook result
|
|
296
|
+
*/
|
|
297
|
+
async send_webhook(data, req) {
|
|
298
|
+
return this.sendWebhook(data, req);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Send webhook to default /webhook endpoint
|
|
303
|
+
* @param {Object} data - Webhook payload object
|
|
304
|
+
* @param {Object} [req] - Express request (optional)
|
|
305
|
+
* @returns {Promise<Object>} Webhook result
|
|
306
|
+
*/
|
|
307
|
+
async sendWebhook(data, req) {
|
|
308
|
+
if (webhookHandler.send_webhook) {
|
|
309
|
+
return webhookHandler.send_webhook(data, req, { endpoint: '/webhook' });
|
|
310
|
+
}
|
|
311
|
+
throw new Error('Webhook functionality not available');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Send webhook to /hooks/wake endpoint
|
|
316
|
+
* Trigger immediate heartbeat (enqueues system event for main session)
|
|
317
|
+
* @param {Object} data - Webhook payload
|
|
318
|
+
* @param {Object} [req] - Express request
|
|
319
|
+
* @returns {Promise<Object>} Webhook result
|
|
320
|
+
*/
|
|
321
|
+
async sendWakeHook(data, req) {
|
|
322
|
+
return this.sendWebhookToEndpoint(data, '/hooks/wake', req);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Send webhook to /hooks/agent endpoint
|
|
327
|
+
* Run isolated agent task with optional reply to messaging channels
|
|
328
|
+
* @param {Object} data - Webhook payload
|
|
329
|
+
* @param {Object} [req] - Express request
|
|
330
|
+
* @returns {Promise<Object>} Webhook result
|
|
331
|
+
*/
|
|
332
|
+
async sendAgentHook(data, req) {
|
|
333
|
+
return this.sendWebhookToEndpoint(data, '/hooks/agent', req);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get versioning middleware
|
|
338
|
+
* @returns {Function} Versioning middleware
|
|
339
|
+
*/
|
|
340
|
+
getVersioningMiddleware() {
|
|
341
|
+
return versioningMiddleware;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get version manager
|
|
346
|
+
* @returns {Object} Version manager
|
|
347
|
+
*/
|
|
348
|
+
getVersionManager() {
|
|
349
|
+
return versionManager;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create new version manager instance
|
|
354
|
+
* @returns {VersionManager} New version manager instance
|
|
355
|
+
*/
|
|
356
|
+
createVersionManager() {
|
|
357
|
+
return new VersionManager();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get logging middleware
|
|
362
|
+
* @returns {Function} Logging middleware
|
|
363
|
+
*/
|
|
364
|
+
getLoggingMiddleware() {
|
|
365
|
+
return loggingmw;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get rate limit middleware
|
|
370
|
+
* @returns {Function} Rate limit middleware
|
|
371
|
+
*/
|
|
372
|
+
getRateLimitMiddleware() {
|
|
373
|
+
return ratelimitmw;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get utilities
|
|
378
|
+
* @returns {Object} Utilities object
|
|
379
|
+
*/
|
|
380
|
+
getUtils() {
|
|
381
|
+
return utils;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Validate and set date
|
|
386
|
+
* @param {*} value - Value to validate and set
|
|
387
|
+
* @returns {Date} Validated date
|
|
388
|
+
*/
|
|
389
|
+
validateAndSetDate(value) {
|
|
390
|
+
return utils.validateAndSetDate(value);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Validate and set JSON
|
|
395
|
+
* @param {*} value - Value to validate and set
|
|
396
|
+
* @returns {Object} Validated JSON object
|
|
397
|
+
*/
|
|
398
|
+
validateAndSetJson(value) {
|
|
399
|
+
return utils.validateAndSetJson(value);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Validate and set URL
|
|
404
|
+
* @param {*} value - Value to validate and set
|
|
405
|
+
* @returns {string} Validated URL
|
|
406
|
+
*/
|
|
407
|
+
validateAndSetUrl(value) {
|
|
408
|
+
return utils.validateAndSetUrl(value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Calculate canonical hash
|
|
413
|
+
* @param {*} data - Data to hash
|
|
414
|
+
* @returns {string} Canonical hash
|
|
415
|
+
*/
|
|
416
|
+
calculateCanonicalHash(data) {
|
|
417
|
+
return utils.calculateCanonicalHash(data);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get logger instance
|
|
422
|
+
* @returns {Object} Logger instance
|
|
423
|
+
*/
|
|
424
|
+
getLogger() {
|
|
425
|
+
return logger;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get performance service
|
|
430
|
+
* @returns {Object} Performance service
|
|
431
|
+
*/
|
|
432
|
+
getPerformanceService() {
|
|
433
|
+
return performanceService;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get RODiT manager
|
|
438
|
+
* @returns {Object} RODiT manager
|
|
439
|
+
*/
|
|
440
|
+
getRoditManager() {
|
|
441
|
+
return roditManager;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Run manual cleanup on session manager
|
|
446
|
+
* @param {...*} args - Arguments to pass to cleanup
|
|
447
|
+
* @returns {Promise<*>} Cleanup result
|
|
448
|
+
*/
|
|
449
|
+
async runManualCleanup(...args) {
|
|
450
|
+
return sessionManager.runManualCleanup(...args);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Initialize the RODiT client with configuration
|
|
455
|
+
* @param {Object} [config] - Configuration overrides
|
|
456
|
+
* @returns {Promise<boolean>} True if initialization was successful
|
|
457
|
+
*/
|
|
458
|
+
async init(config = {}) {
|
|
459
|
+
const requestId = this.requestId;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
// Update configuration from overrides
|
|
463
|
+
if (config.credentialsFilePath) this.credentialsFilePath = config.credentialsFilePath;
|
|
464
|
+
if (config.apiVersion) this.apiVersion = config.apiVersion;
|
|
465
|
+
if (config.versionHeaderType) this.versionHeaderType = config.versionHeaderType;
|
|
466
|
+
|
|
467
|
+
// Initialize the RODiT SDK first to load credentials from Vault
|
|
468
|
+
// For test instances, we need to initialize configuration in the test instance's stateManager
|
|
469
|
+
if (this.testMode) {
|
|
470
|
+
// For test instances, initialize credentials store and config directly in the test stateManager
|
|
471
|
+
await roditManager.initializeCredentialsStore();
|
|
472
|
+
await roditManager.initializeRoditConfig(config.role || 'client', this.stateManager);
|
|
473
|
+
} else {
|
|
474
|
+
// For normal instances, use the standard initialization
|
|
475
|
+
await roditManager.initializeRoditSdk(config);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Get the loaded configuration
|
|
479
|
+
const config_own_rodit = await this.stateManager.getConfigOwnRodit();
|
|
480
|
+
if (!config_own_rodit) {
|
|
481
|
+
throw new Error('Failed to load RODiT configuration from credentials store');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Extract metadata and configure client
|
|
485
|
+
this.roditMetadata = (config_own_rodit.own_rodit && config_own_rodit.own_rodit.metadata) || {};
|
|
486
|
+
|
|
487
|
+
// Set API endpoint from metadata
|
|
488
|
+
this.apiendpoint = ensureProtocol(this.roditMetadata.subjectuniqueidentifier_url);
|
|
489
|
+
|
|
490
|
+
// Configure rate limiting
|
|
491
|
+
if (this.roditMetadata.max_requests && this.roditMetadata.maxrq_window) {
|
|
492
|
+
this.rateLimitState = {
|
|
493
|
+
maxRequests: parseInt(this.roditMetadata.max_requests, 10),
|
|
494
|
+
windowSeconds: parseInt(this.roditMetadata.maxrq_window, 10),
|
|
495
|
+
requestCount: 0,
|
|
496
|
+
windowStart: Date.now()
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Parse JSON configuration fields
|
|
501
|
+
this._parseJsonFields(requestId);
|
|
502
|
+
|
|
503
|
+
// Configure optional URLs
|
|
504
|
+
if (this.roditMetadata.openapijson_url) {
|
|
505
|
+
this.openApiUrl = ensureProtocol(this.roditMetadata.openapijson_url);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (this.roditMetadata.webhook_url) {
|
|
509
|
+
this.webhookUrl = ensureProtocol(this.roditMetadata.webhook_url);
|
|
510
|
+
this.webhookCidr = this.roditMetadata.webhook_cidr || '0.0.0.0/0';
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
this.initialized = true;
|
|
514
|
+
|
|
515
|
+
logger.info('RODiT client initialized successfully', {
|
|
516
|
+
component: 'RoditClient',
|
|
517
|
+
method: 'init',
|
|
518
|
+
requestId,
|
|
519
|
+
endpoints: {
|
|
520
|
+
api: this.apiendpoint,
|
|
521
|
+
openApi: this.openApiUrl,
|
|
522
|
+
webhook: this.webhookUrl
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return true;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
logger.error('Failed to initialize RODiT client', {
|
|
529
|
+
component: 'RoditClient',
|
|
530
|
+
method: 'init',
|
|
531
|
+
requestId,
|
|
532
|
+
error: error.message,
|
|
533
|
+
stack: error.stack
|
|
534
|
+
});
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Parse JSON configuration fields
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
_parseJsonFields(requestId) {
|
|
544
|
+
try {
|
|
545
|
+
if (this.roditMetadata.allowed_iso3166list) {
|
|
546
|
+
this.allowedRegions = JSON.parse(this.roditMetadata.allowed_iso3166list);
|
|
547
|
+
|
|
548
|
+
if (this.allowedRegions.allow && Array.isArray(this.allowedRegions.allow)) {
|
|
549
|
+
const wldIndex = this.allowedRegions.allow.indexOf('WLD');
|
|
550
|
+
|
|
551
|
+
if (wldIndex !== -1) {
|
|
552
|
+
this.geolocationConfig = {
|
|
553
|
+
allowList: this.allowedRegions.allow.slice(0, wldIndex),
|
|
554
|
+
denyList: this.allowedRegions.allow.slice(wldIndex + 1),
|
|
555
|
+
allowWorldwide: true
|
|
556
|
+
};
|
|
557
|
+
} else {
|
|
558
|
+
this.geolocationConfig = {
|
|
559
|
+
allowList: this.allowedRegions.allow,
|
|
560
|
+
denyList: [],
|
|
561
|
+
allowWorldwide: false
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
logger.debug('Parsed geolocation configuration', {
|
|
566
|
+
component: 'RoditClient',
|
|
567
|
+
method: '_parseJsonFields',
|
|
568
|
+
requestId,
|
|
569
|
+
allowList: this.geolocationConfig.allowList,
|
|
570
|
+
denyList: this.geolocationConfig.denyList,
|
|
571
|
+
allowWorldwide: this.geolocationConfig.allowWorldwide
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (this.roditMetadata.permissioned_routes) {
|
|
577
|
+
this.permissionedRoutes = JSON.parse(this.roditMetadata.permissioned_routes);
|
|
578
|
+
}
|
|
579
|
+
} catch (parseError) {
|
|
580
|
+
logger.warn('Failed to parse JSON metadata fields', {
|
|
581
|
+
component: 'RoditClient',
|
|
582
|
+
method: '_parseJsonFields',
|
|
583
|
+
requestId,
|
|
584
|
+
error: parseError.message
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Make an authenticated request to the API
|
|
591
|
+
* @param {string} method - HTTP method
|
|
592
|
+
* @param {string} path - API path
|
|
593
|
+
* @param {Object} [data] - Request data
|
|
594
|
+
* @param {Object} [roptions] - Additional roptions
|
|
595
|
+
* @returns {Promise<Object>} API response
|
|
596
|
+
*/
|
|
597
|
+
async request(method, path, data = null, roptions = {}) {
|
|
598
|
+
if (!this.initialized) {
|
|
599
|
+
throw new Error('Client not initialized. Call init() first.');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const requestId = ulid();
|
|
603
|
+
|
|
604
|
+
// Check token validity before proceeding
|
|
605
|
+
if (!this.isTokenValid()) {
|
|
606
|
+
throw new Error('RODiT token is not valid at the current time');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Check if the operation is permitted
|
|
610
|
+
if (!this.isOperationPermitted(method, path)) {
|
|
611
|
+
throw new Error(`Operation not permitted: ${method} ${path}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Apply rate limiting if configured
|
|
615
|
+
if (this.rateLimitState) {
|
|
616
|
+
await this.applyRateLimit();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const url = new URL(path, this.apiendpoint).toString();
|
|
620
|
+
const headers = {
|
|
621
|
+
'Content-Type': 'application/json',
|
|
622
|
+
'X-Request-ID': requestId,
|
|
623
|
+
...roptions.headers
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// Apply API version headers
|
|
627
|
+
const versionHeaders = versionManager.getVersionHeaders();
|
|
628
|
+
Object.assign(headers, versionHeaders);
|
|
629
|
+
|
|
630
|
+
// Get current session token
|
|
631
|
+
const jwt_token = await this.getSessionToken();
|
|
632
|
+
if (jwt_token) {
|
|
633
|
+
headers['Authorization'] = `Bearer ${jwt_token}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const config = {
|
|
637
|
+
method,
|
|
638
|
+
headers,
|
|
639
|
+
...roptions
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
if (data) {
|
|
643
|
+
config.body = JSON.stringify(data);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
logger.debug('Making API request', {
|
|
648
|
+
component: 'RoditClient',
|
|
649
|
+
method: 'request',
|
|
650
|
+
requestMethod: roptions.method || 'POST'
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const response = await fetch(url, config);
|
|
654
|
+
|
|
655
|
+
// Update rate limit counters
|
|
656
|
+
if (this.rateLimitState) {
|
|
657
|
+
this.rateLimitState.requestCount++;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Handle rate limiting response headers if present
|
|
661
|
+
if (response.headers.has('X-RateLimit-Remaining')) {
|
|
662
|
+
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'), 10);
|
|
663
|
+
const reset = parseInt(response.headers.get('X-RateLimit-Reset'), 10);
|
|
664
|
+
|
|
665
|
+
logger.debug('Rate limit info from server', {
|
|
666
|
+
component: 'RoditClient',
|
|
667
|
+
method: 'request',
|
|
668
|
+
requestId,
|
|
669
|
+
rateLimitRemaining: remaining,
|
|
670
|
+
rateLimitReset: reset
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!response.ok) {
|
|
675
|
+
const rawBody = await response.text();
|
|
676
|
+
let responseData = {};
|
|
677
|
+
try {
|
|
678
|
+
responseData = rawBody ? JSON.parse(rawBody) : {};
|
|
679
|
+
} catch {
|
|
680
|
+
responseData = {
|
|
681
|
+
message: rawBody.slice(0, 800),
|
|
682
|
+
_nonJsonErrorBody: true,
|
|
683
|
+
};
|
|
684
|
+
logger.warn('RoditClient request: error response body was not JSON', {
|
|
685
|
+
component: 'RoditClient',
|
|
686
|
+
method: 'request',
|
|
687
|
+
requestId,
|
|
688
|
+
url,
|
|
689
|
+
status: response.status,
|
|
690
|
+
statusText: response.statusText,
|
|
691
|
+
contentType: response.headers.get('content-type'),
|
|
692
|
+
bodyPreview: rawBody.slice(0, 400),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
logger.warn('RoditClient request: HTTP error response', {
|
|
697
|
+
component: 'RoditClient',
|
|
698
|
+
method: 'request',
|
|
699
|
+
requestId,
|
|
700
|
+
url,
|
|
701
|
+
path,
|
|
702
|
+
httpMethod: method,
|
|
703
|
+
status: response.status,
|
|
704
|
+
statusText: response.statusText,
|
|
705
|
+
apiErrorCode: responseData?.error?.code ?? null,
|
|
706
|
+
apiMessage:
|
|
707
|
+
responseData?.error?.message ??
|
|
708
|
+
responseData?.message ??
|
|
709
|
+
null,
|
|
710
|
+
responseKeys: responseData && typeof responseData === 'object'
|
|
711
|
+
? Object.keys(responseData)
|
|
712
|
+
: [],
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Handle specific error types
|
|
716
|
+
if (response.status === 429) {
|
|
717
|
+
const error = new Error('Rate limit exceeded');
|
|
718
|
+
error.statusCode = 429;
|
|
719
|
+
error.code = 'RATE_LIMIT_EXCEEDED';
|
|
720
|
+
error.responseData = responseData;
|
|
721
|
+
error.requestId = requestId;
|
|
722
|
+
error.timestamp = new Date().toISOString();
|
|
723
|
+
throw error;
|
|
724
|
+
} else if (response.status === 401) {
|
|
725
|
+
// Token might be expired, try to refresh
|
|
726
|
+
if (roptions.autoRefresh !== false) {
|
|
727
|
+
logger.debug('Attempting to refresh authentication token', {
|
|
728
|
+
component: 'RoditClient',
|
|
729
|
+
method: 'request',
|
|
730
|
+
requestId
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
await this.refreshToken();
|
|
734
|
+
|
|
735
|
+
// Retry the request once with the new token
|
|
736
|
+
return this.request(method, path, data, { ...roptions, autoRefresh: false });
|
|
737
|
+
}
|
|
738
|
+
const error = new Error('Authentication failed');
|
|
739
|
+
error.statusCode = 401;
|
|
740
|
+
error.code = 'AUTHENTICATION_FAILED';
|
|
741
|
+
error.responseData = responseData;
|
|
742
|
+
error.requestId = requestId;
|
|
743
|
+
error.timestamp = new Date().toISOString();
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// For all other errors, attach structured error information
|
|
748
|
+
const fallbackMsg = `Request failed with status ${response.status}`;
|
|
749
|
+
const error = new Error(
|
|
750
|
+
responseData?.error?.message ??
|
|
751
|
+
responseData?.message ??
|
|
752
|
+
fallbackMsg
|
|
753
|
+
);
|
|
754
|
+
error.statusCode = response.status;
|
|
755
|
+
error.code = responseData?.error?.code || null;
|
|
756
|
+
error.responseData = responseData;
|
|
757
|
+
error.requestId = requestId;
|
|
758
|
+
error.timestamp = new Date().toISOString();
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const responseData = await response.json().catch(() => ({}));
|
|
763
|
+
return responseData;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
logger.error('API request failed', {
|
|
766
|
+
component: 'RoditClient',
|
|
767
|
+
method: 'request',
|
|
768
|
+
requestId,
|
|
769
|
+
url,
|
|
770
|
+
path,
|
|
771
|
+
httpMethod: method,
|
|
772
|
+
error: error.message,
|
|
773
|
+
statusCode: error.statusCode ?? null,
|
|
774
|
+
code: error.code ?? null,
|
|
775
|
+
requestErrorId: error.requestId ?? requestId,
|
|
776
|
+
stack: error.stack,
|
|
777
|
+
});
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Get current session token
|
|
784
|
+
* @returns {Promise<string|null>} Current session token or null if not authenticated
|
|
785
|
+
*/
|
|
786
|
+
async getSessionToken() {
|
|
787
|
+
try {
|
|
788
|
+
// Correctly retrieve the JWT token from the state manager
|
|
789
|
+
return this.stateManager.getJwtToken();
|
|
790
|
+
} catch (error) {
|
|
791
|
+
logger.error('Failed to get session token from stateManager', {
|
|
792
|
+
component: 'RoditClient',
|
|
793
|
+
method: 'getSessionToken',
|
|
794
|
+
requestId: this.requestId,
|
|
795
|
+
error: error.message
|
|
796
|
+
});
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Set authentication token
|
|
803
|
+
*
|
|
804
|
+
* @param {string} token - Authentication token
|
|
805
|
+
* @returns {boolean} Success indicator
|
|
806
|
+
*/
|
|
807
|
+
async setSessionToken(token) {
|
|
808
|
+
const requestId = ulid();
|
|
809
|
+
|
|
810
|
+
logger.debug('Setting authentication token', {
|
|
811
|
+
component: 'RoditClient',
|
|
812
|
+
method: 'setSessionToken',
|
|
813
|
+
requestId,
|
|
814
|
+
hasToken: !!token
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Store token in AuthStateManager
|
|
818
|
+
this.stateManager.setJwtToken(token);
|
|
819
|
+
|
|
820
|
+
// Also cache locally for quick access
|
|
821
|
+
this.jwt_token = token;
|
|
822
|
+
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Set session data
|
|
828
|
+
*
|
|
829
|
+
* @param {Object} sessionData - Session data
|
|
830
|
+
* @returns {boolean} Success indicator
|
|
831
|
+
*/
|
|
832
|
+
setSessionData(sessionData) {
|
|
833
|
+
const requestId = ulid();
|
|
834
|
+
|
|
835
|
+
logger.debug('Setting session data', {
|
|
836
|
+
component: 'RoditClient',
|
|
837
|
+
method: 'setSessionData',
|
|
838
|
+
requestId,
|
|
839
|
+
hasSessionData: !!sessionData,
|
|
840
|
+
sessionId: sessionData?.id
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
this.sessionData = sessionData;
|
|
844
|
+
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Get session data
|
|
850
|
+
*
|
|
851
|
+
* @returns {Object|null} Session data or null if not set
|
|
852
|
+
*/
|
|
853
|
+
getSessionData() {
|
|
854
|
+
const requestId = ulid();
|
|
855
|
+
|
|
856
|
+
logger.debug('Getting session data', {
|
|
857
|
+
component: 'RoditClient',
|
|
858
|
+
method: 'getSessionData',
|
|
859
|
+
requestId,
|
|
860
|
+
hasSessionData: !!this.sessionData,
|
|
861
|
+
sessionId: this.sessionData?.id
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
return this.sessionData;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Clear session data and token
|
|
869
|
+
*
|
|
870
|
+
* @returns {boolean} Success indicator
|
|
871
|
+
*/
|
|
872
|
+
clearSession() {
|
|
873
|
+
const requestId = ulid();
|
|
874
|
+
|
|
875
|
+
logger.debug('Clearing session data', {
|
|
876
|
+
component: 'RoditClient',
|
|
877
|
+
method: 'clearSession',
|
|
878
|
+
requestId,
|
|
879
|
+
hasSession: !!this.sessionData,
|
|
880
|
+
sessionId: this.sessionData?.id
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Clear session data
|
|
884
|
+
this.sessionData = null;
|
|
885
|
+
this.jwt_token = null;
|
|
886
|
+
|
|
887
|
+
// Also clear JWT token from stateManager
|
|
888
|
+
this.stateManager.setJwtToken(null);
|
|
889
|
+
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Create and initialize a new RODiT client in one step
|
|
896
|
+
* @param {string|Object} [coptions] - Client role (string) or configuration coptions (object)
|
|
897
|
+
* @returns {Promise<RoditClient>} Fully initialized client
|
|
898
|
+
*/
|
|
899
|
+
static async create(coptions = {}) {
|
|
900
|
+
// Handle string input for role
|
|
901
|
+
const config = typeof coptions === 'string' ? { role: coptions } : coptions;
|
|
902
|
+
const client = new RoditClient(config);
|
|
903
|
+
await client.init(config);
|
|
904
|
+
return client;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Create a test instance of RODiT client with independent state
|
|
909
|
+
* This is useful for testing multiple concurrent sessions
|
|
910
|
+
* @param {Object} [ctioptions] - Client ctioptions
|
|
911
|
+
* @returns {Promise<RoditClient>} Fully initialized test client
|
|
912
|
+
*/
|
|
913
|
+
static async createTestInstance(ctioptions = {}) {
|
|
914
|
+
const testOptions = {
|
|
915
|
+
...ctioptions,
|
|
916
|
+
testMode: true
|
|
917
|
+
};
|
|
918
|
+
const client = new RoditClient(testOptions);
|
|
919
|
+
await client.init(testOptions);
|
|
920
|
+
return client;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Handle Express login request (for server-side API endpoints)
|
|
927
|
+
* Delegates to the authentication middleware's login_client function
|
|
928
|
+
*
|
|
929
|
+
* @param {Object} req - Express request object
|
|
930
|
+
* @param {Object} res - Express response object
|
|
931
|
+
* @returns {Promise<void>}
|
|
932
|
+
*/
|
|
933
|
+
async login_client(req, res) {
|
|
934
|
+
logger.debug('Processing Express login request', {
|
|
935
|
+
component: 'RoditClient',
|
|
936
|
+
method: 'login_client',
|
|
937
|
+
path: req.path,
|
|
938
|
+
ip: req.ip
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Delegate directly to the authentication middleware's login_client function
|
|
942
|
+
// The middleware handles all the logic including credential extraction, validation, and response
|
|
943
|
+
return await login_client(req, res);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Handle Express login request using account ID (for server-side API endpoints)
|
|
948
|
+
* Delegates to the authentication middleware's login_client_withaccountid function
|
|
949
|
+
*
|
|
950
|
+
* @param {Object} req - Express request object
|
|
951
|
+
* @param {Object} res - Express response object
|
|
952
|
+
* @returns {Promise<void>}
|
|
953
|
+
*/
|
|
954
|
+
async login_client_withaccountid(req, res) {
|
|
955
|
+
logger.debug('Processing Express account login request', {
|
|
956
|
+
component: 'RoditClient',
|
|
957
|
+
method: 'login_client_withaccountid',
|
|
958
|
+
path: req.path,
|
|
959
|
+
ip: req.ip
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// Delegate directly to the authentication middleware's account-based login function
|
|
963
|
+
return await login_client_withaccountid(req, res);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Handle Express logout request (for server-side API endpoints)
|
|
968
|
+
* Delegates to the authentication middleware's logout_client function
|
|
969
|
+
*
|
|
970
|
+
* @param {Object} req - Express request object
|
|
971
|
+
* @param {Object} res - Express response object
|
|
972
|
+
* @returns {Promise<void>}
|
|
973
|
+
*/
|
|
974
|
+
async logout_client(req, res) {
|
|
975
|
+
logger.debug('Processing Express logout request', {
|
|
976
|
+
component: 'RoditClient',
|
|
977
|
+
method: 'logout_client',
|
|
978
|
+
path: req.path,
|
|
979
|
+
ip: req.ip
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// Delegate directly to the authentication middleware's logout_client function
|
|
983
|
+
// The middleware handles all the logic including session termination and response
|
|
984
|
+
return await logout_client(req, res);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Login to a peer RODiT API using RODiT id (matches login_client / POST /api/login).
|
|
989
|
+
*
|
|
990
|
+
* @param {Object} [lsoptions] - Optional settings
|
|
991
|
+
* @param {string} [lsoptions.loginPath] - Login path (default /api/login)
|
|
992
|
+
* @returns {Promise<Object>} Login result with token
|
|
993
|
+
*/
|
|
994
|
+
async login_server(lsoptions = {}) {
|
|
995
|
+
const requestId = ulid();
|
|
996
|
+
const startTime = Date.now();
|
|
997
|
+
|
|
998
|
+
logger.debug('Starting login process', {
|
|
999
|
+
component: 'RoditClient',
|
|
1000
|
+
method: 'login_server',
|
|
1001
|
+
requestId,
|
|
1002
|
+
lsoptions: { loginPath: lsoptions.loginPath }
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
const config_own_rodit = await this.stateManager.getConfigOwnRodit();
|
|
1007
|
+
|
|
1008
|
+
if (!config_own_rodit) {
|
|
1009
|
+
logger.error('RODiT configuration not set in AuthStateManager', {
|
|
1010
|
+
component: 'RoditClient',
|
|
1011
|
+
method: 'login_server',
|
|
1012
|
+
requestId
|
|
1013
|
+
});
|
|
1014
|
+
throw new Error('RODiT configuration not set in AuthStateManager');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (!config_own_rodit.own_rodit) {
|
|
1018
|
+
logger.error('Valid RODiT configuration not found in AuthStateManager', {
|
|
1019
|
+
component: 'RoditClient',
|
|
1020
|
+
method: 'login_server',
|
|
1021
|
+
requestId,
|
|
1022
|
+
configKeys: Object.keys(config_own_rodit)
|
|
1023
|
+
});
|
|
1024
|
+
throw new Error('Valid RODiT configuration not found in AuthStateManager');
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let loginResult;
|
|
1028
|
+
try {
|
|
1029
|
+
loginResult = await authMw.login_server(config_own_rodit, lsoptions);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
const errorMessage = 'Unable to connect to authentication server. The server may be down or unreachable.';
|
|
1032
|
+
logger.error(errorMessage, {
|
|
1033
|
+
component: 'RoditClient',
|
|
1034
|
+
method: 'login_server',
|
|
1035
|
+
requestId,
|
|
1036
|
+
error: error.message,
|
|
1037
|
+
stack: error.stack
|
|
1038
|
+
});
|
|
1039
|
+
throw new Error(errorMessage);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (loginResult.error) {
|
|
1043
|
+
const errorCode = loginResult.errorCode || loginResult.failureReason || 'UNKNOWN_ERROR';
|
|
1044
|
+
const failureReason = loginResult.failureReason;
|
|
1045
|
+
|
|
1046
|
+
logger.error('Login failed with detailed error information', {
|
|
1047
|
+
component: 'RoditClient',
|
|
1048
|
+
method: 'login_server',
|
|
1049
|
+
requestId,
|
|
1050
|
+
errorCode: errorCode,
|
|
1051
|
+
failureReason: failureReason,
|
|
1052
|
+
errorMessage: loginResult.error,
|
|
1053
|
+
httpStatus: loginResult.status,
|
|
1054
|
+
serverRequestId: loginResult.requestId
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
logger.debug('Login failure context', {
|
|
1058
|
+
component: 'RoditClient',
|
|
1059
|
+
method: 'login_server',
|
|
1060
|
+
requestId,
|
|
1061
|
+
apiEndpoint: config_own_rodit?.apiendpoint || 'unknown',
|
|
1062
|
+
roditId: config_own_rodit?.own_rodit?.token_id || 'unknown',
|
|
1063
|
+
hasPrivateKey: !!(config_own_rodit?.own_rodit_bytes_private_key)
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
let errorMessage = `Login failed: ${loginResult.error}`;
|
|
1067
|
+
|
|
1068
|
+
switch (errorCode) {
|
|
1069
|
+
case 'LOGIN_CHALLENGE_TIMESTAMP_INVALID':
|
|
1070
|
+
errorMessage += '\n→ [CLIENT REJECTED] The login challenge timestamp is invalid or too far in the future. Use timestamp and timestamp_iso from one GET /api/login/timestamp call and check your system clock.';
|
|
1071
|
+
break;
|
|
1072
|
+
case 'RODIT_NOT_FOUND':
|
|
1073
|
+
errorMessage += '\n→ [CLIENT REJECTED] The RODiT was not found on the blockchain. Verify the RODiT ID is correct.';
|
|
1074
|
+
break;
|
|
1075
|
+
case 'RODIT_MISSING_METADATA':
|
|
1076
|
+
errorMessage += '\n→ [CLIENT REJECTED] The RODiT is missing required metadata. The RODiT may be corrupted or incomplete.';
|
|
1077
|
+
break;
|
|
1078
|
+
case 'LOGIN_BASE64URL_SIGNATURE_INVALID':
|
|
1079
|
+
errorMessage += '\n→ [CLIENT REJECTED] The base64url login signature did not verify. Sign UTF-8 (roditid or accountid + canonical timestamp_iso) with the correct NEAR account private key; encoding must be base64url.';
|
|
1080
|
+
break;
|
|
1081
|
+
case 'RODIT_FAMILY_MISMATCH':
|
|
1082
|
+
errorMessage += '\n→ [CLIENT REJECTED] Your RODiT does not belong to the same family as the server. You may need a different RODiT.';
|
|
1083
|
+
break;
|
|
1084
|
+
case 'RODIT_NOT_LIVE':
|
|
1085
|
+
errorMessage += '\n→ [CLIENT REJECTED] Your RODiT is expired or not yet valid. Check the validity period.';
|
|
1086
|
+
break;
|
|
1087
|
+
case 'RODIT_REVOKED':
|
|
1088
|
+
errorMessage += '\n→ [CLIENT REJECTED] Your RODiT has been revoked and is no longer valid.';
|
|
1089
|
+
break;
|
|
1090
|
+
case 'SMART_CONTRACT_NOT_TRUSTED':
|
|
1091
|
+
errorMessage += '\n→ [CLIENT REJECTED] The smart contract that issued your RODiT is not trusted by this server.';
|
|
1092
|
+
break;
|
|
1093
|
+
case 'SERVER_CONFIG_INCOMPLETE':
|
|
1094
|
+
errorMessage += '\n→ [CLIENT REJECTED] The server configuration is incomplete. Contact the server administrator.';
|
|
1095
|
+
break;
|
|
1096
|
+
case 'SERVER_RODIT_FAMILY_MISMATCH':
|
|
1097
|
+
errorMessage += '\n→ [SERVER REJECTED] The server\'s RODiT does not belong to the same family as your client. Contact the server administrator.';
|
|
1098
|
+
break;
|
|
1099
|
+
case 'SERVER_RODIT_NOT_LIVE':
|
|
1100
|
+
errorMessage += '\n→ [SERVER REJECTED] The server\'s RODiT is expired or not yet valid. Contact the server administrator.';
|
|
1101
|
+
break;
|
|
1102
|
+
case 'SERVER_RODIT_REVOKED':
|
|
1103
|
+
errorMessage += '\n→ [SERVER REJECTED] The server\'s RODiT has been revoked. Contact the server administrator.';
|
|
1104
|
+
break;
|
|
1105
|
+
case 'SERVER_SMART_CONTRACT_NOT_TRUSTED':
|
|
1106
|
+
errorMessage += '\n→ [SERVER REJECTED] The server\'s issuing smart contract is not trusted by your client. Update your trust configuration.';
|
|
1107
|
+
break;
|
|
1108
|
+
case 'SERVER_TOKEN_IDENTITY_MISMATCH':
|
|
1109
|
+
errorMessage += '\n→ [SERVER REJECTED] The server\'s token identity does not match expected values. This may indicate a security issue.';
|
|
1110
|
+
break;
|
|
1111
|
+
default:
|
|
1112
|
+
if (loginResult.error.includes('client')) {
|
|
1113
|
+
errorMessage += '\n→ The authentication server may be down or experiencing issues. Please try again later or contact support.';
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
throw new Error(errorMessage);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (loginResult.jwt_token) {
|
|
1121
|
+
this.jwt_token = loginResult.jwt_token;
|
|
1122
|
+
this.setSessionToken(loginResult.jwt_token);
|
|
1123
|
+
|
|
1124
|
+
const fromJwt = sessionFieldsFromJwt(loginResult.jwt_token);
|
|
1125
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
1126
|
+
this.sessionId = fromJwt.sessionId || ulid();
|
|
1127
|
+
this.setSessionData({
|
|
1128
|
+
id: this.sessionId,
|
|
1129
|
+
createdAt: fromJwt.createdAt ?? nowSec,
|
|
1130
|
+
expiresAt:
|
|
1131
|
+
fromJwt.expiresAt ??
|
|
1132
|
+
nowSec + config.getDefaultJwtDurationSeconds(),
|
|
1133
|
+
status: 'active'
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const duration = Date.now() - startTime;
|
|
1138
|
+
logger.info('Login successful', {
|
|
1139
|
+
component: 'RoditClient',
|
|
1140
|
+
method: 'login_server',
|
|
1141
|
+
requestId,
|
|
1142
|
+
duration,
|
|
1143
|
+
roditId: config_own_rodit?.own_rodit?.token_id || 'unknown',
|
|
1144
|
+
hasToken: !!loginResult.jwt_token
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
logger.metric && logger.metric('login_duration_ms', duration, {
|
|
1148
|
+
component: 'RoditClient',
|
|
1149
|
+
success: true
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
success: true,
|
|
1154
|
+
jwt_token: loginResult.jwt_token,
|
|
1155
|
+
sessionId: this.sessionId
|
|
1156
|
+
};
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
const duration = Date.now() - startTime;
|
|
1159
|
+
|
|
1160
|
+
logger.error('Login failed', {
|
|
1161
|
+
component: 'RoditClient',
|
|
1162
|
+
method: 'login_server',
|
|
1163
|
+
requestId,
|
|
1164
|
+
duration,
|
|
1165
|
+
error: {
|
|
1166
|
+
message: error.message,
|
|
1167
|
+
stack: error.stack
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
logger.metric && logger.metric('login_duration_ms', duration, {
|
|
1172
|
+
component: 'RoditClient',
|
|
1173
|
+
success: false,
|
|
1174
|
+
error: error.name
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
logger.metric && logger.metric('login_errors', 1, {
|
|
1178
|
+
component: 'RoditClient',
|
|
1179
|
+
error: error.name
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Login to the RODiT API using NEAR account id (matches login_client_withaccountid on the peer).
|
|
1188
|
+
*
|
|
1189
|
+
* @param {Object} lsoptions - Login options
|
|
1190
|
+
* @param {string} [lsoptions.accountId] - NEAR account id override
|
|
1191
|
+
* @param {string} [lsoptions.loginPath] - Login path (default /api/login)
|
|
1192
|
+
* @returns {Promise<Object>} Login result with token
|
|
1193
|
+
*/
|
|
1194
|
+
/**
|
|
1195
|
+
* Login to SignPortal for token signing operations
|
|
1196
|
+
*
|
|
1197
|
+
* @param {Object} config_own_rodit - RODiT configuration object
|
|
1198
|
+
* @param {number} port - Portal port number
|
|
1199
|
+
* @param {Object} [options] - Optional login settings
|
|
1200
|
+
* @param {number} [options.timestamp] - Unix seconds used for signature generation (if omitted, local current time is used)
|
|
1201
|
+
* @param {string} [options.accountId] - Explicit account id fallback when token id is unavailable
|
|
1202
|
+
* @returns {Promise<Object>} Login result with JWT token
|
|
1203
|
+
*/
|
|
1204
|
+
async login_portal(config_own_rodit, port, options = {}) {
|
|
1205
|
+
const requestId = ulid();
|
|
1206
|
+
const startTime = Date.now();
|
|
1207
|
+
|
|
1208
|
+
logger.debug('Starting portal login process', {
|
|
1209
|
+
component: 'RoditClient',
|
|
1210
|
+
method: 'login_portal',
|
|
1211
|
+
requestId,
|
|
1212
|
+
port,
|
|
1213
|
+
roditId: config_own_rodit?.own_rodit?.token_id
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
try {
|
|
1217
|
+
// Delegate to the authentication middleware's login_portal function
|
|
1218
|
+
const loginResult = await login_portal(config_own_rodit, port, options);
|
|
1219
|
+
|
|
1220
|
+
const duration = Date.now() - startTime;
|
|
1221
|
+
logger.info('Portal login successful', {
|
|
1222
|
+
component: 'RoditClient',
|
|
1223
|
+
method: 'login_portal',
|
|
1224
|
+
requestId,
|
|
1225
|
+
duration,
|
|
1226
|
+
hasToken: !!loginResult.jwt_token
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Track metric
|
|
1230
|
+
logger.metric && logger.metric('portal_login_duration_ms', duration, {
|
|
1231
|
+
component: 'RoditClient',
|
|
1232
|
+
success: true
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
return loginResult;
|
|
1236
|
+
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
const duration = Date.now() - startTime;
|
|
1239
|
+
|
|
1240
|
+
logger.error('Portal login failed', {
|
|
1241
|
+
component: 'RoditClient',
|
|
1242
|
+
method: 'login_portal',
|
|
1243
|
+
requestId,
|
|
1244
|
+
duration,
|
|
1245
|
+
error: {
|
|
1246
|
+
message: error.message,
|
|
1247
|
+
name: error.name
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// Track error metric
|
|
1252
|
+
logger.metric && logger.metric('portal_login_duration_ms', duration, {
|
|
1253
|
+
component: 'RoditClient',
|
|
1254
|
+
success: false,
|
|
1255
|
+
error: error.name
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Logout from the RODiT API (for server-to-server usage)
|
|
1264
|
+
* Delegates to the authentication middleware's logout_server function
|
|
1265
|
+
*
|
|
1266
|
+
* @returns {Promise<Object>} Logout result with termination token
|
|
1267
|
+
*/
|
|
1268
|
+
async logout_server() {
|
|
1269
|
+
const requestId = ulid();
|
|
1270
|
+
const startTime = Date.now();
|
|
1271
|
+
|
|
1272
|
+
logger.debug('Processing server logout request', {
|
|
1273
|
+
component: 'RoditClient',
|
|
1274
|
+
method: 'logout_server',
|
|
1275
|
+
requestId,
|
|
1276
|
+
hasToken: !!this.jwt_token,
|
|
1277
|
+
sessionId: this.sessionId
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
if (!this.jwt_token) {
|
|
1281
|
+
logger.warn('Logout called without an active token', {
|
|
1282
|
+
component: 'RoditClient',
|
|
1283
|
+
method: 'logout_server',
|
|
1284
|
+
requestId
|
|
1285
|
+
});
|
|
1286
|
+
return {
|
|
1287
|
+
success: false,
|
|
1288
|
+
error: 'No active token to logout',
|
|
1289
|
+
requestId
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
try {
|
|
1294
|
+
// Delegate to the authentication middleware's logout_server function
|
|
1295
|
+
const logoutResult = await logout_server(this.jwt_token);
|
|
1296
|
+
|
|
1297
|
+
// Clear local session data if logout was successful
|
|
1298
|
+
if (logoutResult.success) {
|
|
1299
|
+
this.jwt_token = null;
|
|
1300
|
+
this.sessionId = null;
|
|
1301
|
+
this.clearSession();
|
|
1302
|
+
|
|
1303
|
+
const duration = Date.now() - startTime;
|
|
1304
|
+
logger.info('Server logout successful', {
|
|
1305
|
+
component: 'RoditClient',
|
|
1306
|
+
method: 'logout_server',
|
|
1307
|
+
requestId,
|
|
1308
|
+
duration,
|
|
1309
|
+
jwt_tokenInvalidated: logoutResult.jwt_tokenInvalidated,
|
|
1310
|
+
sessionClosed: logoutResult.sessionClosed,
|
|
1311
|
+
hasTerminationToken: !!logoutResult.terminationToken
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// Track success metric
|
|
1315
|
+
logger.metric && logger.metric('logout_server_duration_ms', duration, {
|
|
1316
|
+
component: 'RoditClient',
|
|
1317
|
+
success: true
|
|
1318
|
+
});
|
|
1319
|
+
} else {
|
|
1320
|
+
logger.warn('Server logout failed, clearing local session anyway', {
|
|
1321
|
+
component: 'RoditClient',
|
|
1322
|
+
method: 'logout_server',
|
|
1323
|
+
requestId,
|
|
1324
|
+
error: logoutResult.error
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// Clear local session even if server logout failed
|
|
1328
|
+
this.jwt_token = null;
|
|
1329
|
+
this.sessionId = null;
|
|
1330
|
+
this.clearSession();
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
return logoutResult;
|
|
1334
|
+
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
const duration = Date.now() - startTime;
|
|
1337
|
+
|
|
1338
|
+
logger.error('Server logout failed', {
|
|
1339
|
+
component: 'RoditClient',
|
|
1340
|
+
method: 'logout_server',
|
|
1341
|
+
requestId,
|
|
1342
|
+
duration,
|
|
1343
|
+
error: {
|
|
1344
|
+
message: error.message,
|
|
1345
|
+
stack: error.stack,
|
|
1346
|
+
name: error.name
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// Track error metric
|
|
1351
|
+
logger.metric && logger.metric('logout_server_duration_ms', duration, {
|
|
1352
|
+
component: 'RoditClient',
|
|
1353
|
+
success: false,
|
|
1354
|
+
error: error.name
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// Clear session data even if the logout call fails
|
|
1358
|
+
this.jwt_token = null;
|
|
1359
|
+
this.sessionId = null;
|
|
1360
|
+
this.clearSession();
|
|
1361
|
+
|
|
1362
|
+
return {
|
|
1363
|
+
success: false,
|
|
1364
|
+
error: error.message,
|
|
1365
|
+
requestId
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Check if the client is authenticated
|
|
1372
|
+
*
|
|
1373
|
+
* @returns {Promise<boolean>} True if the client is authenticated
|
|
1374
|
+
*/
|
|
1375
|
+
async isAuthenticated() {
|
|
1376
|
+
const requestId = ulid();
|
|
1377
|
+
|
|
1378
|
+
logger.debug('Checking authentication status', {
|
|
1379
|
+
component: 'RoditClient',
|
|
1380
|
+
method: 'isAuthenticated',
|
|
1381
|
+
requestId,
|
|
1382
|
+
hasToken: !!this.jwt_token,
|
|
1383
|
+
sessionId: this.sessionId
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// If we don't have a token, we're definitely not authenticated
|
|
1387
|
+
if (!this.jwt_token) {
|
|
1388
|
+
logger.debug('No token available, client is not authenticated', {
|
|
1389
|
+
component: 'RoditClient',
|
|
1390
|
+
method: 'isAuthenticated',
|
|
1391
|
+
requestId
|
|
1392
|
+
});
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
try {
|
|
1397
|
+
// Check if we have a valid session
|
|
1398
|
+
const sessionData = this.getSessionData();
|
|
1399
|
+
|
|
1400
|
+
if (!sessionData) {
|
|
1401
|
+
logger.debug('No session data available, client is not authenticated', {
|
|
1402
|
+
component: 'RoditClient',
|
|
1403
|
+
method: 'isAuthenticated',
|
|
1404
|
+
requestId
|
|
1405
|
+
});
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Check if the session has expired
|
|
1410
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
1411
|
+
if (sessionData.expiresAt && sessionData.expiresAt < currentTime) {
|
|
1412
|
+
logger.debug('Session has expired', {
|
|
1413
|
+
component: 'RoditClient',
|
|
1414
|
+
method: 'isAuthenticated',
|
|
1415
|
+
requestId,
|
|
1416
|
+
sessionId: sessionData.id,
|
|
1417
|
+
expiresAt: sessionData.expiresAt,
|
|
1418
|
+
currentTime
|
|
1419
|
+
});
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// If we have a token and a valid non-expired session, we're authenticated
|
|
1424
|
+
logger.debug('Client is authenticated with valid token and session', {
|
|
1425
|
+
component: 'RoditClient',
|
|
1426
|
+
method: 'isAuthenticated',
|
|
1427
|
+
requestId,
|
|
1428
|
+
sessionId: sessionData.id,
|
|
1429
|
+
sessionStatus: sessionData.status
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
return true;
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
logger.error('Authentication check failed', {
|
|
1435
|
+
component: 'RoditClient',
|
|
1436
|
+
method: 'isAuthenticated',
|
|
1437
|
+
requestId,
|
|
1438
|
+
error: {
|
|
1439
|
+
message: error.message,
|
|
1440
|
+
stack: error.stack,
|
|
1441
|
+
name: error.name
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Check if the RODiT token is valid at the current time
|
|
1451
|
+
* @returns {boolean} True if the token is valid
|
|
1452
|
+
*/
|
|
1453
|
+
isTokenValid() {
|
|
1454
|
+
if (!this.roditMetadata) {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const now = new Date();
|
|
1459
|
+
let isValid = true;
|
|
1460
|
+
|
|
1461
|
+
if (
|
|
1462
|
+
this.roditMetadata.not_before &&
|
|
1463
|
+
!isRoditUnboundedDate(this.roditMetadata.not_before)
|
|
1464
|
+
) {
|
|
1465
|
+
const notBefore = new Date(this.roditMetadata.not_before);
|
|
1466
|
+
if (now < notBefore) {
|
|
1467
|
+
logger.debug('Token not yet valid', {
|
|
1468
|
+
component: 'RoditClient',
|
|
1469
|
+
method: 'isTokenValid',
|
|
1470
|
+
now: now.toISOString(),
|
|
1471
|
+
notBefore: notBefore.toISOString()
|
|
1472
|
+
});
|
|
1473
|
+
isValid = false;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (
|
|
1478
|
+
this.roditMetadata.not_after &&
|
|
1479
|
+
!isRoditUnboundedDate(this.roditMetadata.not_after)
|
|
1480
|
+
) {
|
|
1481
|
+
const notAfter = new Date(this.roditMetadata.not_after);
|
|
1482
|
+
if (now > notAfter) {
|
|
1483
|
+
logger.debug('Token has expired', {
|
|
1484
|
+
component: 'RoditClient',
|
|
1485
|
+
method: 'isTokenValid',
|
|
1486
|
+
now: now.toISOString(),
|
|
1487
|
+
notAfter: notAfter.toISOString()
|
|
1488
|
+
});
|
|
1489
|
+
isValid = false;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return isValid;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Check if an operation is permitted based on permissioned_routes
|
|
1498
|
+
* @param {string} method - HTTP method
|
|
1499
|
+
* @param {string} path - API path
|
|
1500
|
+
* @returns {boolean} True if the operation is permitted
|
|
1501
|
+
*/
|
|
1502
|
+
isOperationPermitted(method, path) {
|
|
1503
|
+
// If no permissioned routes are defined, allow all
|
|
1504
|
+
if (!this.permissionedRoutes) {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
// Check if the path matches any permissioned route
|
|
1510
|
+
const entities = this.permissionedRoutes.entities;
|
|
1511
|
+
if (!entities) {
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Check if the method+path combination is in the permissioned routes
|
|
1516
|
+
const methods = entities.methods;
|
|
1517
|
+
if (!methods) {
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// If the path is explicitly listed, check its permission value
|
|
1522
|
+
if (methods[path]) {
|
|
1523
|
+
const permission = methods[path];
|
|
1524
|
+
// "+0" or any positive value indicates permission is granted
|
|
1525
|
+
return permission.startsWith('+');
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// If not explicitly listed, check for wildcard patterns
|
|
1529
|
+
// This is a simplified implementation - could be enhanced with proper pattern matching
|
|
1530
|
+
const wildcardPaths = Object.keys(methods).filter(p => p.includes('*'));
|
|
1531
|
+
for (const wildcardPath of wildcardPaths) {
|
|
1532
|
+
const pattern = wildcardPath.replace('*', '.*');
|
|
1533
|
+
const regex = new RegExp(pattern);
|
|
1534
|
+
if (regex.test(path)) {
|
|
1535
|
+
const permission = methods[wildcardPath];
|
|
1536
|
+
return permission.startsWith('+');
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Default to allowed if not explicitly denied
|
|
1541
|
+
return true;
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
logger.error('Error checking operation permission', {
|
|
1544
|
+
component: 'RoditClient',
|
|
1545
|
+
method: 'isOperationPermitted',
|
|
1546
|
+
error: error.message,
|
|
1547
|
+
path,
|
|
1548
|
+
httpMethod: method
|
|
1549
|
+
});
|
|
1550
|
+
// Default to allowed on error
|
|
1551
|
+
return true;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Get the complete RODiT configuration object
|
|
1557
|
+
* @returns {Promise<Object>} Complete RODiT configuration
|
|
1558
|
+
*/
|
|
1559
|
+
async getConfigOwnRodit() {
|
|
1560
|
+
const requestId = ulid();
|
|
1561
|
+
|
|
1562
|
+
logger.debug('Getting RODiT configuration', {
|
|
1563
|
+
component: 'RoditClient',
|
|
1564
|
+
method: 'getConfigOwnRodit',
|
|
1565
|
+
requestId,
|
|
1566
|
+
stateManagerExists: !!this.stateManager,
|
|
1567
|
+
stateManagerType: typeof this.stateManager
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
try {
|
|
1571
|
+
// Add detailed logging before the call
|
|
1572
|
+
logger.debug('Calling stateManager.getConfigOwnRodit()', {
|
|
1573
|
+
component: 'RoditClient',
|
|
1574
|
+
method: 'getConfigOwnRodit',
|
|
1575
|
+
requestId,
|
|
1576
|
+
stateManagerMethods: Object.getOwnPropertyNames(this.stateManager).filter(name => typeof this.stateManager[name] === 'function')
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const config_own_rodit = await this.stateManager.getConfigOwnRodit();
|
|
1580
|
+
|
|
1581
|
+
logger.debug('Retrieved RODiT configuration', {
|
|
1582
|
+
component: 'RoditClient',
|
|
1583
|
+
method: 'getConfigOwnRodit',
|
|
1584
|
+
requestId,
|
|
1585
|
+
hasConfig: !!config_own_rodit,
|
|
1586
|
+
hasOwnRodit: !!(config_own_rodit && config_own_rodit.own_rodit),
|
|
1587
|
+
configType: typeof config_own_rodit,
|
|
1588
|
+
configKeys: config_own_rodit ? Object.keys(config_own_rodit) : null,
|
|
1589
|
+
configStringified: config_own_rodit ? JSON.stringify(config_own_rodit, null, 2) : 'null'
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
return config_own_rodit;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
logger.error('Failed to get RODiT configuration', {
|
|
1595
|
+
component: 'RoditClient',
|
|
1596
|
+
method: 'getConfigOwnRodit',
|
|
1597
|
+
requestId,
|
|
1598
|
+
error: error.message,
|
|
1599
|
+
stack: error.stack
|
|
1600
|
+
});
|
|
1601
|
+
throw error;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* Get portal URL for SignPortal operations
|
|
1607
|
+
* @param {string} serviceProviderId - Service provider ID
|
|
1608
|
+
* @param {number} port - Portal port
|
|
1609
|
+
* @returns {string} Portal URL
|
|
1610
|
+
*/
|
|
1611
|
+
getPortalUrl(serviceProviderId, port) {
|
|
1612
|
+
const requestId = ulid();
|
|
1613
|
+
|
|
1614
|
+
logger.debug('Getting portal URL', {
|
|
1615
|
+
component: 'RoditClient',
|
|
1616
|
+
method: 'getPortalUrl',
|
|
1617
|
+
requestId,
|
|
1618
|
+
serviceProviderId,
|
|
1619
|
+
port
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
return stateManager.getPortalUrl(serviceProviderId, port);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Get SignPortal JWT token
|
|
1627
|
+
* @returns {string|null} SignPortal JWT token
|
|
1628
|
+
*/
|
|
1629
|
+
getSignPortalJwtToken() {
|
|
1630
|
+
const requestId = ulid();
|
|
1631
|
+
|
|
1632
|
+
return stateManager.getSignPortalJwtToken();
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Set SignPortal JWT token
|
|
1637
|
+
* @param {string} token - SignPortal JWT token
|
|
1638
|
+
* @returns {Promise<string>} Set token
|
|
1639
|
+
*/
|
|
1640
|
+
async setSignPortalJwtToken(token) {
|
|
1641
|
+
const requestId = ulid();
|
|
1642
|
+
|
|
1643
|
+
return await stateManager.setSignPortalJwtToken(token);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* Fetch with error handling for SignPortal operations
|
|
1648
|
+
* @param {string} url - URL to fetch
|
|
1649
|
+
* @param {Object} fwehspoptions - Fetch fwehspoptions
|
|
1650
|
+
* @returns {Promise<Object>} Response data
|
|
1651
|
+
*/
|
|
1652
|
+
async fetchWithErrorHandlingSignPortal(url, fwehspoptions) {
|
|
1653
|
+
const requestId = ulid();
|
|
1654
|
+
|
|
1655
|
+
logger.debug('Making SignPortal fetch request', {
|
|
1656
|
+
component: 'RoditClient',
|
|
1657
|
+
method: 'fetchWithErrorHandlingSignPortal',
|
|
1658
|
+
requestId,
|
|
1659
|
+
url,
|
|
1660
|
+
httpMethod: fwehspoptions?.method
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
return await stateManager.fetchWithErrorHandlingSignPortal(url, fwehspoptions);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Checks if a subscription is active based on token metadata dates
|
|
1668
|
+
* @returns {boolean} True if subscription is active
|
|
1669
|
+
*/
|
|
1670
|
+
isSubscriptionActive() {
|
|
1671
|
+
const config_own_rodit = this.stateManager.getConfigOwnRodit();
|
|
1672
|
+
|
|
1673
|
+
if (!config_own_rodit?.own_rodit?.metadata) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const metadata = config_own_rodit.own_rodit.metadata;
|
|
1678
|
+
const now = new Date();
|
|
1679
|
+
let isActive = true;
|
|
1680
|
+
|
|
1681
|
+
if (metadata.not_before && !isRoditUnboundedDate(metadata.not_before)) {
|
|
1682
|
+
const notBefore = new Date(metadata.not_before);
|
|
1683
|
+
if (now < notBefore) {
|
|
1684
|
+
logger.debug('Subscription not yet active', {
|
|
1685
|
+
component: 'RoditClient',
|
|
1686
|
+
method: 'isSubscriptionActive',
|
|
1687
|
+
now: now.toISOString(),
|
|
1688
|
+
notBefore: notBefore.toISOString()
|
|
1689
|
+
});
|
|
1690
|
+
isActive = false;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (metadata.not_after && !isRoditUnboundedDate(metadata.not_after)) {
|
|
1695
|
+
const notAfter = new Date(metadata.not_after);
|
|
1696
|
+
if (now > notAfter) {
|
|
1697
|
+
logger.debug('Subscription has expired', {
|
|
1698
|
+
component: 'RoditClient',
|
|
1699
|
+
method: 'isSubscriptionActive',
|
|
1700
|
+
now: now.toISOString(),
|
|
1701
|
+
notAfter: notAfter.toISOString()
|
|
1702
|
+
});
|
|
1703
|
+
isActive = false;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return isActive;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Apply rate limiting based on token configuration
|
|
1712
|
+
* @returns {Promise<void>}
|
|
1713
|
+
*/
|
|
1714
|
+
async applyRateLimit() {
|
|
1715
|
+
if (!this.rateLimitState) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const now = Date.now();
|
|
1720
|
+
const { maxRequests, windowSeconds, requestCount, windowStart } = this.rateLimitState;
|
|
1721
|
+
|
|
1722
|
+
// Reset window if it has expired
|
|
1723
|
+
if (now - windowStart > windowSeconds * 1000) {
|
|
1724
|
+
this.rateLimitState.requestCount = 0;
|
|
1725
|
+
this.rateLimitState.windowStart = now;
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Check if we've exceeded the rate limit
|
|
1730
|
+
if (requestCount >= maxRequests) {
|
|
1731
|
+
const waitTime = windowStart + (windowSeconds * 1000) - now;
|
|
1732
|
+
|
|
1733
|
+
logger.warn('Rate limit reached, waiting before next request', {
|
|
1734
|
+
component: 'RoditClient',
|
|
1735
|
+
method: 'applyRateLimit',
|
|
1736
|
+
waitTimeMs: waitTime,
|
|
1737
|
+
maxRequests,
|
|
1738
|
+
requestCount
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
// Wait until the window resets
|
|
1742
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
1743
|
+
|
|
1744
|
+
// Reset the window
|
|
1745
|
+
this.rateLimitState.requestCount = 0;
|
|
1746
|
+
this.rateLimitState.windowStart = Date.now();
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Refresh the authentication token by calling `login_server` (RODiT id flow, default `POST /api/login`).
|
|
1752
|
+
* @returns {Promise<string>} New token
|
|
1753
|
+
*/
|
|
1754
|
+
async refreshToken() {
|
|
1755
|
+
logger.debug('Refreshing authentication token', {
|
|
1756
|
+
component: 'RoditClient',
|
|
1757
|
+
method: 'refreshToken'
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
await this.login_server();
|
|
1761
|
+
const refreshedToken = await this.getSessionToken();
|
|
1762
|
+
|
|
1763
|
+
return refreshedToken;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Register a webhook callback
|
|
1768
|
+
* @param {string} event_type - Event type to subscribe to
|
|
1769
|
+
* @param {string} callbackUrl - URL to receive webhook events
|
|
1770
|
+
* @returns {Promise<Object>} Registration result
|
|
1771
|
+
*/
|
|
1772
|
+
async registerWebhook(event_type, callbackUrl) {
|
|
1773
|
+
if (!this.webhookUrl) {
|
|
1774
|
+
throw new Error('Webhook URL not configured in token metadata');
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
return this.request('POST', '/webhooks/register', {
|
|
1778
|
+
event_type,
|
|
1779
|
+
callback_url: callbackUrl
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Unregister a webhook callback
|
|
1785
|
+
* @param {string} event_type - Event type to unsubscribe from
|
|
1786
|
+
* @param {string} callbackUrl - URL that was registered
|
|
1787
|
+
* @returns {Promise<Object>} Unregistration result
|
|
1788
|
+
*/
|
|
1789
|
+
async unregisterWebhook(event_type, callbackUrl) {
|
|
1790
|
+
if (!this.webhookUrl) {
|
|
1791
|
+
throw new Error('Webhook URL not configured in token metadata');
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
return this.request('POST', '/webhooks/unregister', {
|
|
1795
|
+
event_type,
|
|
1796
|
+
callback_url: callbackUrl
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Verify a webhook signature
|
|
1802
|
+
* @param {string} payload - Webhook payload
|
|
1803
|
+
* @param {string} signature - Webhook signature
|
|
1804
|
+
* @param {number} timestamp - Webhook timestamp
|
|
1805
|
+
* @returns {Promise<boolean>} True if signature is valid
|
|
1806
|
+
*/
|
|
1807
|
+
async verifyWebhookSignature(payload, signature, timestamp) {
|
|
1808
|
+
try {
|
|
1809
|
+
// This is a placeholder - actual implementation would depend on the signature method
|
|
1810
|
+
// used by the webhook sender
|
|
1811
|
+
const crypto = require('crypto');
|
|
1812
|
+
const hmac = crypto.createHmac('sha256', await this.getWebhookSecret());
|
|
1813
|
+
|
|
1814
|
+
hmac.update(`${timestamp}.${payload}`);
|
|
1815
|
+
const expectedSignature = hmac.digest('hex');
|
|
1816
|
+
|
|
1817
|
+
return crypto.timingSafeEqual(
|
|
1818
|
+
Buffer.from(expectedSignature, 'hex'),
|
|
1819
|
+
Buffer.from(signature, 'hex')
|
|
1820
|
+
);
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
logger.error('Failed to verify webhook signature', {
|
|
1823
|
+
component: 'RoditClient',
|
|
1824
|
+
method: 'verifyWebhookSignature',
|
|
1825
|
+
error: error.message
|
|
1826
|
+
});
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// Import health check and retry functions
|
|
1833
|
+
const { healthCheckRPC, resolveHealthyNearRpcUrl, fetchWithRetry } = require('./lib/blockchain/blockchainservice');
|
|
1834
|
+
|
|
1835
|
+
// Export the RoditClient class and commonly used SDK components
|
|
1836
|
+
module.exports = {
|
|
1837
|
+
RoditClient,
|
|
1838
|
+
// Core services needed by test modules and applications
|
|
1839
|
+
logger,
|
|
1840
|
+
stateManager,
|
|
1841
|
+
roditManager,
|
|
1842
|
+
sessionManager,
|
|
1843
|
+
blockchainService,
|
|
1844
|
+
utils,
|
|
1845
|
+
config,
|
|
1846
|
+
performanceService,
|
|
1847
|
+
errorResponse,
|
|
1848
|
+
sendError,
|
|
1849
|
+
buildErrorResponse,
|
|
1850
|
+
authenticate_apicall,
|
|
1851
|
+
authenticate_logout,
|
|
1852
|
+
login_client,
|
|
1853
|
+
logout_client,
|
|
1854
|
+
login_client_withnep413,
|
|
1855
|
+
login_portal,
|
|
1856
|
+
login_server,
|
|
1857
|
+
logout_server,
|
|
1858
|
+
validate_jwt_token_be,
|
|
1859
|
+
generate_jwt_token,
|
|
1860
|
+
validatepermissions,
|
|
1861
|
+
webhookHandler,
|
|
1862
|
+
versioningMiddleware,
|
|
1863
|
+
loggingmw,
|
|
1864
|
+
ratelimitmw,
|
|
1865
|
+
versionManager,
|
|
1866
|
+
VersionManager,
|
|
1867
|
+
// Blockchain service functions
|
|
1868
|
+
nearorg_rpc_timestamp: blockchainService.nearorg_rpc_timestamp,
|
|
1869
|
+
// Startup validation and health check functions
|
|
1870
|
+
validateConfig: config.validate,
|
|
1871
|
+
healthCheckRPC,
|
|
1872
|
+
resolveHealthyNearRpcUrl,
|
|
1873
|
+
fetchWithRetry,
|
|
1874
|
+
// Export services for middleware and utilities
|
|
1875
|
+
services: {
|
|
1876
|
+
logger,
|
|
1877
|
+
sendError,
|
|
1878
|
+
buildErrorResponse,
|
|
1879
|
+
errorResponse,
|
|
1880
|
+
utils,
|
|
1881
|
+
config,
|
|
1882
|
+
performanceService
|
|
1883
|
+
}
|
|
1884
|
+
};
|