@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.
@@ -0,0 +1,627 @@
1
+ /**
2
+ * RODIT manager for handling RODIT-specific operations
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const { ulid } = require("ulid");
7
+ const config = require("../../services/configsdk");
8
+ const logger = require("../../services/logger");
9
+ const { createLogContext, logErrorWithMetrics } = logger;
10
+ // Dynamically select credential store based on config/env (flat key only)
11
+ const RODIT_NEAR_CREDENTIALS_SOURCE = config.get("RODIT_NEAR_CREDENTIALS_SOURCE");
12
+ logger.debugWithContext(
13
+ "Selecting credential store",
14
+ createLogContext("RoditManager", "credentialStoreSelect", {
15
+ source: RODIT_NEAR_CREDENTIALS_SOURCE,
16
+ })
17
+ );
18
+
19
+ let credentialStoreModule;
20
+ if (RODIT_NEAR_CREDENTIALS_SOURCE === "file") {
21
+ credentialStoreModule = require("../middleware/filecredentialstoremw");
22
+ } else if (RODIT_NEAR_CREDENTIALS_SOURCE === "env") {
23
+ credentialStoreModule = require("../middleware/environcredentialstoremw");
24
+ } else {
25
+ credentialStoreModule = require("../middleware/vaultcredentialstoremw");
26
+ }
27
+
28
+ // const credentialStoreModule = require("../middleware/filecredentialstoremw");
29
+
30
+ const {
31
+ initializeCredentialStore,
32
+ setupSecretStorageTokenRenewal,
33
+ getCredentials,
34
+ vault,
35
+ } = credentialStoreModule;
36
+ const {
37
+ nearorg_rpc_state,
38
+ nearorg_rpc_tokensfromaccountid,
39
+ } = require("../blockchain/blockchainservice");
40
+ const stateManager = require("../blockchain/statemanager");
41
+
42
+ const baseModuleContext = createLogContext("ModuleLoader", "RoditManager", {
43
+ loadedAt: new Date().toISOString(),
44
+ });
45
+
46
+ logger.debugWithContext("Loading roditmanager.js module", baseModuleContext);
47
+ /**
48
+ * RoditManager class
49
+ * Singleton class for managing RODiT configurations and credentials
50
+ */
51
+ class RoditManager {
52
+ constructor() {
53
+ const instanceId = ulid();
54
+
55
+ const constructorContext = createLogContext("RoditManager", "constructor", {
56
+ instanceId,
57
+ hasExistingInstance: !!RoditManager.instance,
58
+ existingInstanceId: RoditManager.instance
59
+ ? RoditManager.instance._instanceId
60
+ : null,
61
+ });
62
+
63
+ logger.debugWithContext(
64
+ "RoditManager constructor called",
65
+ constructorContext
66
+ );
67
+ if (RoditManager.instance) {
68
+ logger.debugWithContext("Returning existing RoditManager instance", {
69
+ ...constructorContext,
70
+ instanceId: RoditManager.instance._instanceId,
71
+ });
72
+ return RoditManager.instance;
73
+ }
74
+
75
+ this._instanceId = instanceId; // Store the instance ID
76
+ logger.debug("Creating new RoditManager instance", {
77
+ component: "RoditManager",
78
+ instanceId: this._instanceId,
79
+ });
80
+
81
+ this.stateManager = stateManager;
82
+
83
+ RoditManager.instance = this;
84
+ }
85
+
86
+ async initializeCredentialsStore() {
87
+ const requestId = ulid();
88
+
89
+ logger.debug("Initializing CredentialManager", {
90
+ component: "RoditManager",
91
+ method: "initializeCredentialsStore",
92
+ requestId,
93
+ instanceId: this._instanceId,
94
+ });
95
+
96
+ try {
97
+ const credentialstoreInstance =
98
+ await initializeCredentialStore();
99
+ await setupSecretStorageTokenRenewal(credentialstoreInstance);
100
+
101
+ logger.debug(
102
+ "CredentialStore initialization completed through CredentialManager",
103
+ {
104
+ component: "RoditManager",
105
+ method: "initializeCredentialsStore",
106
+ requestId,
107
+ instanceId: this._instanceId,
108
+ }
109
+ );
110
+
111
+ return credentialstoreInstance;
112
+ } catch (error) {
113
+ logger.error("Error during CredentialStore initialization", {
114
+ component: "RoditManager",
115
+ method: "initializeCredentialsStore",
116
+ requestId,
117
+ errorMessage: error.message,
118
+ errorCode: error.code || "UNKNOWN_ERROR",
119
+ stack: error.stack,
120
+ });
121
+
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ async initializeRoditConfig(type, targetStateManager = null) {
127
+ const requestId = ulid();
128
+ const startTime = Date.now();
129
+
130
+ // Use the provided stateManager or fall back to the singleton
131
+ const stateManagerToUse = targetStateManager || this.stateManager;
132
+
133
+ // Create a base context for this method
134
+ const baseContext = createLogContext(
135
+ "RoditManager",
136
+ "initializeRoditConfig",
137
+ {
138
+ requestId,
139
+ configType: type,
140
+ usingTestStateManager: !!targetStateManager,
141
+ }
142
+ );
143
+
144
+ logger.infoWithContext("Starting RODiT config initialization", baseContext);
145
+
146
+ try {
147
+ logger.debugWithContext("Getting credentials", {
148
+ ...baseContext,
149
+ step: "fetchCredentials",
150
+ });
151
+
152
+ let credentials = await getCredentials(type);
153
+
154
+ if (!credentials) {
155
+ logErrorWithMetrics({
156
+ error: new Error(`Credentials not available for ${type}`),
157
+ context: {
158
+ ...baseContext,
159
+ step: "credentialCheck",
160
+ },
161
+ metrics: [
162
+ {
163
+ name: "credential_retrieval_failures",
164
+ value: 1,
165
+ tags: { configType: type },
166
+ },
167
+ ],
168
+ });
169
+ throw new Error(`Credentials not available for ${type}`);
170
+ }
171
+
172
+ // Handle case where credentials might be in an object with account_id as key
173
+ if (
174
+ typeof credentials === "object" &&
175
+ Object.keys(credentials).length === 1
176
+ ) {
177
+ credentials = Object.values(credentials)[0];
178
+ }
179
+
180
+ // Require implicit_account_id
181
+ const account_id = credentials.implicit_account_id;
182
+ if (!account_id) {
183
+ throw new Error("Credentials must contain implicit_account_id");
184
+ }
185
+
186
+ logger.infoWithContext("Using account for initialization", {
187
+ ...baseContext,
188
+ accountId: account_id,
189
+ step: "accountSetup",
190
+ });
191
+
192
+ logger.debugWithContext("Checking account state on blockchain", {
193
+ ...baseContext,
194
+ accountId: account_id,
195
+ step: "blockchainCheck",
196
+ });
197
+
198
+ const accountState = await nearorg_rpc_state(account_id);
199
+
200
+ if (!accountState) {
201
+ logger.warnWithContext("Account has no balance in network", {
202
+ ...baseContext,
203
+ accountId: account_id,
204
+ step: "blockchainCheck",
205
+ });
206
+ } else {
207
+ logger.infoWithContext("Account state verified on blockchain", {
208
+ ...baseContext,
209
+ accountId: account_id,
210
+ step: "blockchainCheck",
211
+ });
212
+ }
213
+
214
+ logger.debugWithContext("Fetching RODiT tokens for account", {
215
+ ...baseContext,
216
+ accountId: account_id,
217
+ step: "tokenFetch",
218
+ });
219
+
220
+ const own_rodit = await nearorg_rpc_tokensfromaccountid(account_id);
221
+
222
+ // Check if we have a real RODiT token
223
+ if (!own_rodit || !own_rodit.token_id) {
224
+ logger.warnWithContext(
225
+ "No RODiT instances found, proceeding with partial initialization",
226
+ {
227
+ ...baseContext,
228
+ accountId: account_id,
229
+ step: "tokenCheck",
230
+ }
231
+ );
232
+
233
+ // Create a minimal configuration for signroot
234
+ const minimalConfig = {
235
+ own_rodit: {
236
+ token_id: "",
237
+ owner_id: account_id,
238
+ metadata: {
239
+ subjectuniqueidentifier_url: "api-url-not-set.example.com",
240
+ serviceprovider_id: "",
241
+ not_after: "2030-01-01",
242
+ not_before: "2020-01-01",
243
+ },
244
+ },
245
+ own_rodit_bytes_private_key: credentials.signing_bytes_key,
246
+ apiEndpoint: "localhost",
247
+ port: "",
248
+ iso639: config.get("API_DEFAULT_OPTIONS.ISO639"),
249
+ iso3166: config.get("API_DEFAULT_OPTIONS.ISO3166"),
250
+ iso15924: config.get("API_DEFAULT_OPTIONS.ISO15924"),
251
+ timeoptions: config.get("API_DEFAULT_OPTIONS.TIMEOPTIONS"),
252
+ };
253
+
254
+ await stateManagerToUse.setConfigOwnRodit(minimalConfig);
255
+
256
+ const session_base64url_jwk_public_key = Buffer.from(
257
+ account_id,
258
+ "hex"
259
+ ).toString("base64url");
260
+
261
+ logger.debugWithContext("Converting implicit account ID to base64url", {
262
+ ...baseContext,
263
+ step: "keyConversion",
264
+ });
265
+
266
+ logger.debugWithContext("Setting session base64url JWK public key", {
267
+ ...baseContext,
268
+ step: "setSessionKey",
269
+ });
270
+
271
+ await stateManagerToUse.setOwnBase64urlJwkPublicKey(
272
+ session_base64url_jwk_public_key
273
+ );
274
+
275
+ const duration = Date.now() - startTime;
276
+ logger.infoWithContext(
277
+ "RODiT config initialized with minimal configuration",
278
+ {
279
+ ...baseContext,
280
+ duration,
281
+ configLevel: "partial",
282
+ step: "complete",
283
+ }
284
+ );
285
+
286
+ // Emit metrics for dashboards
287
+ logger.metric("rodit_initialization_duration_ms", duration, {
288
+ success: true,
289
+ configType: type,
290
+ configLevel: "partial",
291
+ component: "RoditManager",
292
+ });
293
+
294
+ return minimalConfig;
295
+ }
296
+
297
+ logger.infoWithContext("RODiT config initialized successfully", {
298
+ ...baseContext,
299
+ roditId: own_rodit.token_id,
300
+ duration: Date.now() - startTime,
301
+ });
302
+
303
+ // Port configuration removed as requested
304
+
305
+ if (
306
+ !own_rodit.metadata ||
307
+ !own_rodit.metadata.subjectuniqueidentifier_url
308
+ ) {
309
+ logger.errorWithContext("Missing required metadata in RODiT", {
310
+ ...baseContext,
311
+ missingField: "subjectuniqueidentifier_url",
312
+ step: "metadataCheck",
313
+ });
314
+
315
+ throw new Error(
316
+ "Missing required metadata: subjectuniqueidentifier_url"
317
+ );
318
+ }
319
+
320
+ const apiendpoint = own_rodit.metadata.subjectuniqueidentifier_url;
321
+
322
+ logger.debugWithContext("Constructed API endpoint", {
323
+ ...baseContext,
324
+ api_ep: apiendpoint,
325
+ step: "apiEndpointCreation",
326
+ });
327
+
328
+ logger.infoWithContext("Building full configuration object", {
329
+ ...baseContext,
330
+ step: "fullConfigCreation",
331
+ });
332
+
333
+ // Validate private key format before storing in config object
334
+ let privateKeyToUse = credentials.signing_bytes_key;
335
+
336
+ // Validate private key format (sensitive data not logged per security policy)
337
+
338
+ // Detailed private key format validation and logging
339
+ logger.debugWithContext("Private key format validation", {
340
+ ...baseContext,
341
+ keyType: typeof privateKeyToUse,
342
+ isUint8Array: privateKeyToUse instanceof Uint8Array,
343
+ isBuffer: Buffer.isBuffer(privateKeyToUse),
344
+ length: privateKeyToUse?.length,
345
+ step: "privateKeyValidation",
346
+ });
347
+
348
+ // Ensure private key is in the correct format (Uint8Array)
349
+ if (privateKeyToUse && !(privateKeyToUse instanceof Uint8Array)) {
350
+ if (Buffer.isBuffer(privateKeyToUse)) {
351
+ logger.debugWithContext(
352
+ "Converting Buffer to Uint8Array for private key",
353
+ {
354
+ ...baseContext,
355
+ step: "privateKeyConversion",
356
+ bufferLength: privateKeyToUse.length,
357
+ }
358
+ );
359
+
360
+ // Store original buffer for comparison
361
+ const originalBuffer = Buffer.from(privateKeyToUse);
362
+
363
+ // Convert to Uint8Array
364
+ privateKeyToUse = new Uint8Array(privateKeyToUse);
365
+
366
+ // Verify conversion was successful
367
+ logger.debugWithContext("Buffer to Uint8Array conversion result", {
368
+ ...baseContext,
369
+ step: "privateKeyConversionResult",
370
+ originalType: "Buffer",
371
+ convertedType: privateKeyToUse.constructor.name,
372
+ isUint8Array: privateKeyToUse instanceof Uint8Array,
373
+ originalLength: originalBuffer.length,
374
+ convertedLength: privateKeyToUse.length,
375
+ });
376
+ } else if (
377
+ typeof privateKeyToUse === "object" &&
378
+ privateKeyToUse !== null
379
+ ) {
380
+ // Try to recover from a JSON-serialized Uint8Array or similar object
381
+ logger.warnWithContext(
382
+ "Attempting to recover private key from non-standard format",
383
+ {
384
+ ...baseContext,
385
+ recoveryAttempt: true,
386
+ objectKeys: Object.keys(privateKeyToUse).join(","),
387
+ hasLength: privateKeyToUse.length !== undefined,
388
+ lengthType: typeof privateKeyToUse.length,
389
+ }
390
+ );
391
+
392
+ try {
393
+ // If it's an array-like object, try to convert it to Uint8Array
394
+ if (
395
+ Array.isArray(privateKeyToUse) ||
396
+ (privateKeyToUse.length !== undefined &&
397
+ typeof privateKeyToUse.length === "number")
398
+ ) {
399
+ const originalData = privateKeyToUse;
400
+ privateKeyToUse = new Uint8Array(
401
+ Array.isArray(privateKeyToUse)
402
+ ? privateKeyToUse
403
+ : Array.from(privateKeyToUse)
404
+ );
405
+
406
+ logger.infoWithContext(
407
+ "Successfully recovered private key from array-like object",
408
+ {
409
+ ...baseContext,
410
+ recoveredKeyLength: privateKeyToUse.length,
411
+ recoveredIsUint8Array: privateKeyToUse instanceof Uint8Array,
412
+ originalType: originalData.constructor.name,
413
+ }
414
+ );
415
+ } else {
416
+ throw new Error("Cannot recover key - not an array-like object");
417
+ }
418
+ } catch (recoveryError) {
419
+ logErrorWithMetrics({
420
+ error: new Error(
421
+ `Private key recovery failed: ${recoveryError.message}`
422
+ ),
423
+ context: {
424
+ ...baseContext,
425
+ keyType: typeof privateKeyToUse,
426
+ step: "privateKeyRecoveryFailed",
427
+ recoveryError: recoveryError.message,
428
+ },
429
+ metrics: [
430
+ {
431
+ name: "private_key_recovery_failures",
432
+ value: 1,
433
+ tags: { configType: type },
434
+ },
435
+ ],
436
+ });
437
+ throw new Error("Private key must be a Uint8Array or Buffer");
438
+ }
439
+ } else {
440
+ logErrorWithMetrics({
441
+ error: new Error("Private key must be a Uint8Array or Buffer"),
442
+ context: {
443
+ ...baseContext,
444
+ keyType: typeof privateKeyToUse,
445
+ step: "privateKeyValidation",
446
+ },
447
+ metrics: [
448
+ {
449
+ name: "private_key_format_errors",
450
+ value: 1,
451
+ tags: { configType: type },
452
+ },
453
+ ],
454
+ });
455
+ throw new Error("Private key must be a Uint8Array or Buffer");
456
+ }
457
+ }
458
+
459
+ // Private key validated and ready for storage (sensitive data not logged per security policy)
460
+
461
+ const roditClient = {
462
+ own_rodit,
463
+ own_rodit_bytes_private_key: privateKeyToUse, // Use validated private key
464
+ apiendpoint,
465
+ port: "",
466
+ iso639: config.get("API_DEFAULT_OPTIONS.ISO639"),
467
+ iso3166: config.get("API_DEFAULT_OPTIONS.ISO3166"),
468
+ iso15924: config.get("API_DEFAULT_OPTIONS.ISO15924"),
469
+ timeoptions: config.get("API_DEFAULT_OPTIONS.TIMEOPTIONS"),
470
+ };
471
+
472
+ logger.debugWithContext("Using RODiT token for configuration", {
473
+ ...baseContext,
474
+ roditId: own_rodit.token_id,
475
+ accountId: account_id,
476
+ step: "tokenUse",
477
+ });
478
+
479
+ logger.debugWithContext("Storing configuration in state manager", {
480
+ ...baseContext,
481
+ step: "storeConfig",
482
+ });
483
+
484
+ await stateManagerToUse.setConfigOwnRodit(roditClient);
485
+
486
+ logger.infoWithContext("Configuration stored successfully", {
487
+ ...baseContext,
488
+ step: "configStored",
489
+ });
490
+
491
+ logger.debugWithContext("Converting implicit account ID to base64url", {
492
+ ...baseContext,
493
+ step: "keyConversion",
494
+ });
495
+
496
+ const session_base64url_jwk_public_key = Buffer.from(
497
+ account_id,
498
+ "hex"
499
+ ).toString("base64url");
500
+
501
+ logger.debugWithContext("Setting session base64url JWK public key", {
502
+ ...baseContext,
503
+ step: "setSessionKey",
504
+ });
505
+
506
+ // Set the client's own public key from the implicit account ID
507
+ await stateManagerToUse.setOwnBase64urlJwkPublicKey(
508
+ session_base64url_jwk_public_key
509
+ );
510
+
511
+ // Note: The server's public key should be set separately when it's received
512
+ // during the handshake or authentication process
513
+
514
+ const duration = Date.now() - startTime;
515
+ logger.infoWithContext("RODiT configuration completed", {
516
+ ...baseContext,
517
+ duration,
518
+ configLevel: "full",
519
+ step: "complete",
520
+ });
521
+
522
+ // Emit metrics for dashboards
523
+ logger.metric("rodit_initialization_duration_ms", duration, {
524
+ success: true,
525
+ configType: type,
526
+ configLevel: "full",
527
+ component: "RoditManager",
528
+ });
529
+
530
+ return roditClient;
531
+ } catch (error) {
532
+ const duration = Date.now() - startTime;
533
+
534
+ logErrorWithMetrics({
535
+ error,
536
+ context: {
537
+ ...baseContext,
538
+ duration,
539
+ },
540
+ metrics: [
541
+ {
542
+ name: "rodit_config_initialization_errors",
543
+ value: 1,
544
+ tags: { configType: type, errorType: error.name || "Unknown" },
545
+ },
546
+ ],
547
+ });
548
+
549
+ // Emit metrics for dashboards
550
+ logger.metric("rodit_initialization_duration_ms", duration, {
551
+ success: false,
552
+ configType: type,
553
+ component: "RoditManager",
554
+ errorType: error.code || "UNKNOWN_ERROR",
555
+ });
556
+ logger.metric("rodit_initialization_errors_total", 1, {
557
+ errorType: error.code || "UNKNOWN_ERROR",
558
+ configType: type,
559
+ component: "RoditManager",
560
+ step: error.step || "unknown",
561
+ });
562
+
563
+ throw error;
564
+ }
565
+ }
566
+
567
+ // Initialize RODiT SDK with the specified role
568
+ async initializeRoditSdk(roles = {}) {
569
+ const role = roles.role || "client";
570
+
571
+ try {
572
+ // Initialize vault and configuration using SDK
573
+ await this.initializeCredentialsStore();
574
+
575
+ // Initialize RODiT configuration for the specified role
576
+ await this.initializeRoditConfig(role);
577
+
578
+ logger.info(
579
+ `Credentials (${RODIT_NEAR_CREDENTIALS_SOURCE}) initialized and RODiT configuration loaded for role: ${role}`
580
+ );
581
+
582
+ // Get and validate the configuration
583
+ const roditClient = await stateManager.getConfigOwnRodit();
584
+ if (!roditClient) {
585
+ throw new Error(
586
+ "Failed to initialize RODiT configuration: No configuration returned"
587
+ );
588
+ }
589
+
590
+ // Apply rate limiting if configured
591
+ const { own_rodit } = roditClient;
592
+ if (
593
+ own_rodit?.metadata?.max_requests &&
594
+ own_rodit?.metadata?.maxrq_window
595
+ ) {
596
+ // This function should be provided by the application
597
+ if (typeof stateManager.updateRateLimit === "function") {
598
+ stateManager.updateRateLimit(
599
+ own_rodit.metadata.max_requests,
600
+ own_rodit.metadata.maxrq_window
601
+ );
602
+ }
603
+ }
604
+
605
+ return roditClient;
606
+ } catch (error) {
607
+ logger.error(`Failed to initialize RODiT SDK: ${error.message}`, {
608
+ error,
609
+ });
610
+ throw new Error(`SDK initialization failed: ${error.message}`);
611
+ }
612
+ }
613
+ /**
614
+ * Get credentials for a specific type
615
+ * @param {string} type - The credential type (e.g., 'sanctum', 'portal')
616
+ * @returns {Promise<Object>} The credentials object
617
+ */
618
+ async getCredentials(type) {
619
+ return await getCredentials(type);
620
+ }
621
+ }
622
+
623
+ // Create and export a singleton instance
624
+ const roditManager = new RoditManager();
625
+
626
+ // Export the singleton instance directly to avoid any issues with destructuring
627
+ module.exports = roditManager;