@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/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
+ };