@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,1024 @@
1
+ /**
2
+ * Utility functions for RODiT Authentication
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const { ulid } = require("ulid");
7
+ const { createLogContext, logErrorWithMetrics } = require("./logger");
8
+ const logger = require("./logger");
9
+ const bs58 = require("bs58");
10
+ const nacl = require("tweetnacl");
11
+ nacl.util = require("tweetnacl-util");
12
+ const { decodeUTF8 } = require("tweetnacl-util");
13
+
14
+ // Dynamic import for ESM 'jose' in CommonJS context
15
+ let _josePromise;
16
+ async function getJose() {
17
+ if (!_josePromise) {
18
+ _josePromise = import("jose");
19
+ }
20
+ return _josePromise;
21
+ }
22
+
23
+ /**
24
+ * Test-specific fetch with error handling for API calls
25
+ * This function is specifically designed for test modules and should not be confused with SDK HTTP methods
26
+ * @param {string} url - URL to fetch
27
+ * @param {Object} fetchoptions - Fetch fetchoptions
28
+ * @returns {Promise<Object>} - Response data
29
+ */
30
+ async function testFetchWithErrorHandling(url, fetchoptions = {}) {
31
+ const requestId = ulid();
32
+ const startTime = Date.now();
33
+
34
+ try {
35
+ // Debug: Log headers being sent
36
+ const finalHeaders = {
37
+ "Content-Type": "application/json",
38
+ ...fetchoptions.headers
39
+ };
40
+
41
+ logger.debug(`Test fetch: ${url}`, {
42
+ component: "TestFetchHandler",
43
+ requestId,
44
+ url,
45
+ method: fetchoptions.method || "GET",
46
+ hasAuthHeader: !!finalHeaders.Authorization,
47
+ allHeaders: Object.keys(finalHeaders)
48
+ });
49
+
50
+ logger.debug(`API request initiated`, {
51
+ component: "APIClient",
52
+ method: "fetchWithErrorHandling",
53
+ requestId,
54
+ url: url.split('/').pop(), // Just the endpoint part
55
+ operation: fetchoptions.method || "GET",
56
+ retryCount: 0
57
+ });
58
+
59
+ const response = await fetch(url, {
60
+ ...fetchoptions,
61
+ headers: finalHeaders
62
+ });
63
+
64
+ const duration = Date.now() - startTime;
65
+
66
+ if (!response.ok) {
67
+ const errorText = await response.text();
68
+
69
+ logger.error(`Test fetch error: ${response.status} ${response.statusText}`, {
70
+ component: "TestFetchHandler",
71
+ requestId,
72
+ url,
73
+ method: fetchoptions.method || "GET",
74
+ status: response.status,
75
+ statusText: response.statusText,
76
+ duration,
77
+ errorText
78
+ });
79
+
80
+ throw new Error(`HTTP error ${response.status}: ${errorText}`);
81
+ }
82
+
83
+ const data = await response.json();
84
+
85
+ logger.debug(`API request completed`, {
86
+ component: "APIClient",
87
+ method: "fetchWithErrorHandling",
88
+ requestId,
89
+ url: url.split('/').pop(), // Just the endpoint part
90
+ statusCode: response.status,
91
+ duration
92
+ });
93
+
94
+ return data;
95
+ } catch (error) {
96
+ const duration = Date.now() - startTime;
97
+
98
+ logger.error(`Test fetch exception: ${error.message}`, {
99
+ component: "TestFetchHandler",
100
+ requestId,
101
+ url,
102
+ method: fetchoptions.method || "GET",
103
+ duration,
104
+ error: error.message,
105
+ stack: error.stack
106
+ });
107
+
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Sentinel date for RODiT metadata: unbounded not_before / not_after (no expiration / no start bound).
114
+ * Matches validateAndSetDate default and on-chain metadata convention.
115
+ */
116
+ const RODIT_UNBOUNDED_DATE = "1970-01-01";
117
+
118
+ /**
119
+ * True when a RODiT date field means "unbounded" (not a real calendar end/start).
120
+ *
121
+ * @param {string|number|null|undefined} value - Metadata date or unix seconds
122
+ * @returns {boolean}
123
+ */
124
+ function isRoditUnboundedDate(value) {
125
+ if (value == null) {
126
+ return true;
127
+ }
128
+ const trimmed = String(value).trim();
129
+ if (trimmed === "" || trimmed === "0") {
130
+ return true;
131
+ }
132
+ if (trimmed === RODIT_UNBOUNDED_DATE) {
133
+ return true;
134
+ }
135
+ const parsed = new Date(trimmed);
136
+ if (!Number.isNaN(parsed.getTime()) && parsed.getTime() === 0) {
137
+ return true;
138
+ }
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * Converts a date string to Unix timestamp
144
+ *
145
+ * @param {string} datestring - Date string in ISO format
146
+ * @returns {Promise<number>} Unix timestamp in seconds
147
+ */
148
+ async function dateStringToUnixTime(datestring) {
149
+ const date = new Date(datestring);
150
+ const unixTimeMs = date.getTime();
151
+ const unixTimeSec = Math.floor(unixTimeMs / 1000);
152
+ return unixTimeSec;
153
+ }
154
+
155
+ /**
156
+ * Unix seconds for RODiT not_after used as a JWT/session cap, or null when unbounded.
157
+ *
158
+ * @param {string|null|undefined} datestring - RODiT metadata not_after
159
+ * @returns {Promise<number|null>}
160
+ */
161
+ async function roditNotAfterUnixCap(datestring) {
162
+ if (isRoditUnboundedDate(datestring)) {
163
+ return null;
164
+ }
165
+ return dateStringToUnixTime(datestring);
166
+ }
167
+
168
+ /**
169
+ * Converts a Unix timestamp to a date string
170
+ * This function is used by both the API and test suite
171
+ *
172
+ * @param {number|string} unixTimeSec - Unix timestamp in seconds
173
+ * @returns {Promise<string>} Date string in ISO format
174
+ */
175
+ async function unixTimeToDateString(unixTimeSec) {
176
+ const unixTimeMs = unixTimeSec * 1000;
177
+ const date = new Date(unixTimeMs);
178
+ return date.toISOString();
179
+ }
180
+
181
+ /**
182
+ * Ensures a URL has a protocol prefix (http:// or https://)
183
+ * @param {string} url - URL to ensure has a protocol
184
+ * @returns {string} URL with protocol prefix
185
+ */
186
+ function ensureProtocol(url) {
187
+ if (!url) {
188
+ logger.warn("Empty URL provided to ensureProtocol", {
189
+ component: "Utils",
190
+ function: "ensureProtocol",
191
+ });
192
+ return "";
193
+ }
194
+
195
+ if (url.startsWith("http://") || url.startsWith("https://")) {
196
+ return url;
197
+ }
198
+
199
+ return "https://" + url;
200
+ }
201
+
202
+ /**
203
+ * Converts base64 to base64url format
204
+ *
205
+ * @param {string} base64 - Base64 string
206
+ * @returns {string} Base64url string
207
+ */
208
+ function base64ToBase64Url(base64) {
209
+ const result = base64
210
+ .replace(/\+/g, "-")
211
+ .replace(/\//g, "_")
212
+ .replace(/=/g, "");
213
+
214
+ logger.debug("Converting base64 to base64url", {
215
+ component: "Transformer",
216
+ method: "base64ToBase64Url",
217
+ inputLength: base64.length,
218
+ outputLength: result.length,
219
+ });
220
+
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * Canonicalizes an object for consistent hash generation
226
+ *
227
+ * @param {Object|Array|any} obj - Object to canonicalize
228
+ * @returns {Object|Array|any} Canonicalized object
229
+ */
230
+ function canonicalizeObject(obj) {
231
+ const startTime = Date.now();
232
+ const requestId = ulid();
233
+
234
+ if (typeof obj !== "object" || obj === null) {
235
+ // Commented out unnecessary canonicalization logging
236
+ // logger.info("Skipping canonicalization for non-object", {
237
+ // component: "Transformer",
238
+ // method: "canonicalizeObject",
239
+ // requestId,
240
+ // valueType: typeof obj,
241
+ // });
242
+ return obj;
243
+ }
244
+
245
+ let result;
246
+ if (Array.isArray(obj)) {
247
+ logger.debug("Canonicalizing array", {
248
+ component: "Transformer",
249
+ method: "canonicalizeObject",
250
+ requestId,
251
+ arrayLength: obj.length,
252
+ });
253
+ result = obj.map(canonicalizeObject);
254
+ } else {
255
+ logger.debug("Canonicalizing object", {
256
+ component: "Transformer",
257
+ method: "canonicalizeObject",
258
+ requestId,
259
+ keyCount: Object.keys(obj).length,
260
+ });
261
+ result = Object.fromEntries(
262
+ Object.entries(obj)
263
+ .sort()
264
+ .map(([key, value]) => [key, canonicalizeObject(value)])
265
+ );
266
+ }
267
+
268
+ const duration = Date.now() - startTime;
269
+ logger.debug("Object canonicalization complete", {
270
+ component: "Transformer",
271
+ method: "canonicalizeObject",
272
+ requestId,
273
+ duration,
274
+ });
275
+
276
+ // Emit metrics for dashboards if operation took significant time
277
+ if (duration > 50) {
278
+ logger.metric("canonicalization_duration_ms", duration, {
279
+ component: "Transformer",
280
+ objectType: Array.isArray(obj) ? "array" : "object",
281
+ size: Array.isArray(obj) ? obj.length : Object.keys(obj).length,
282
+ });
283
+ }
284
+
285
+ return result;
286
+ }
287
+
288
+ /**
289
+ * Calculates a canonical hash for an object
290
+ *
291
+ * @param {Object|Array|any} variable - Variable to hash
292
+ * @returns {string} Hex hash string
293
+ */
294
+ function calculateCanonicalHash(variable) {
295
+ const startTime = Date.now();
296
+ const requestId = ulid();
297
+
298
+ logger.debug("Calculating canonical hash", {
299
+ component: "Transformer",
300
+ method: "calculateCanonicalHash",
301
+ requestId,
302
+ variableType: typeof variable,
303
+ });
304
+
305
+ try {
306
+ const canonicalObj = canonicalizeObject(variable);
307
+ const canonicalJson = JSON.stringify(canonicalObj);
308
+
309
+ logger.debug("Canonicalized JSON created", {
310
+ component: "Transformer",
311
+ method: "calculateCanonicalHash",
312
+ requestId,
313
+ jsonLength: canonicalJson.length,
314
+ });
315
+
316
+ const messageUint8 = decodeUTF8(canonicalJson);
317
+
318
+ logger.debug("Decoded to Uint8Array for hashing", {
319
+ component: "Transformer",
320
+ method: "calculateCanonicalHash",
321
+ requestId,
322
+ bytesLength: messageUint8.length,
323
+ });
324
+
325
+ const hashUint8 = nacl.hash(messageUint8);
326
+
327
+ logger.debug("Hash calculated", {
328
+ component: "Transformer",
329
+ method: "calculateCanonicalHash",
330
+ requestId,
331
+ hashLength: hashUint8.length,
332
+ });
333
+
334
+ const hexResult = Array.from(hashUint8)
335
+ .map((b) => b.toString(16).padStart(2, "0"))
336
+ .join("");
337
+
338
+ const duration = Date.now() - startTime;
339
+ logger.debug("Canonical hash calculation complete", {
340
+ component: "Transformer",
341
+ method: "calculateCanonicalHash",
342
+ requestId,
343
+ duration,
344
+ hashLength: hexResult.length,
345
+ });
346
+
347
+ // Emit metrics for dashboards
348
+ logger.metric("hash_calculation_duration_ms", duration, {
349
+ component: "Transformer",
350
+ jsonLength: canonicalJson.length,
351
+ });
352
+
353
+ return hexResult;
354
+ } catch (error) {
355
+ const duration = Date.now() - startTime;
356
+
357
+ logger.error("Hash calculation failed", {
358
+ component: "Transformer",
359
+ method: "calculateCanonicalHash",
360
+ requestId,
361
+ duration,
362
+ errorMessage: error.message,
363
+ stack: error.stack,
364
+ });
365
+
366
+ // Emit metrics for dashboards
367
+ logger.metric("hash_calculation_duration_ms", duration, {
368
+ component: "Transformer",
369
+ success: false,
370
+ error: error.constructor.name,
371
+ });
372
+ logger.metric("hash_calculation_errors_total", 1, {
373
+ component: "Transformer",
374
+ errorType: error.constructor.name,
375
+ });
376
+
377
+ throw error;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Validates and sets a URL value
383
+ *
384
+ * @param {string} value - URL to validate
385
+ * @param {string} field - Field name for error messages
386
+ * @param {Object} obj - Object to set the value on
387
+ * @returns {string|null} Validated URL or null if invalid
388
+ */
389
+ const validateAndSetUrl = (value, field, obj = null) => {
390
+ const requestId = ulid();
391
+ const startTime = Date.now();
392
+
393
+ logger.debug("Validating URL", {
394
+ component: "Validator",
395
+ method: "validateAndSetUrl",
396
+ requestId,
397
+ field,
398
+ });
399
+
400
+ if (value == null) {
401
+ logger.debug("URL validation skipped, value is null", {
402
+ component: "Validator",
403
+ method: "validateAndSetUrl",
404
+ requestId,
405
+ field,
406
+ });
407
+ return null;
408
+ }
409
+
410
+ // First remove any existing protocol
411
+ let normalizedUrl = value.replace(/^(https?:\/\/)/, "");
412
+
413
+ // Remove whitespace for testing
414
+ const testUrl = normalizedUrl.replace(/\s+/g, "");
415
+
416
+ // Updated regex to properly handle ports and domain names
417
+ const urlRegex =
418
+ /^(localhost(:[0-9]{1,5})?|([\da-z][\da-z-]*[\da-z]\.)*[\da-z][\da-z-]*[\da-z]\.[a-z\.]{2,6}(:[0-9]{1,5})?|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:[0-9]{1,5})?)(\/[\w\.-]*)*\/?$/i;
419
+
420
+ if (urlRegex.test(testUrl)) {
421
+ const result = `https://${normalizedUrl}`;
422
+
423
+ const duration = Date.now() - startTime;
424
+ logger.debug("URL validation successful", {
425
+ component: "Validator",
426
+ method: "validateAndSetUrl",
427
+ requestId,
428
+ field,
429
+ duration,
430
+ isValid: true,
431
+ });
432
+
433
+ // Emit metrics for dashboards
434
+ logger.metric("validation_duration_ms", duration, {
435
+ validationType: "url",
436
+ field,
437
+ success: true,
438
+ component: "Validator",
439
+ });
440
+
441
+ if (obj && typeof obj === "object") {
442
+ obj[field] = result;
443
+ }
444
+ return result;
445
+ }
446
+
447
+ const duration = Date.now() - startTime;
448
+ logger.warn("URL validation failed", {
449
+ component: "Validator",
450
+ method: "validateAndSetUrl",
451
+ requestId,
452
+ field,
453
+ duration,
454
+ isValid: false,
455
+ value,
456
+ });
457
+
458
+ // Emit metrics for dashboards
459
+ logger.metric("validation_duration_ms", duration, {
460
+ validationType: "url",
461
+ field,
462
+ success: false,
463
+ component: "Validator",
464
+ });
465
+ logger.metric("validation_errors_total", 1, {
466
+ validationType: "url",
467
+ field,
468
+ component: "Validator",
469
+ });
470
+
471
+ throw new Error(`Invalid URL for ${field}: ${value}`);
472
+ };
473
+
474
+ /**
475
+ * Validates and sets a date value
476
+ *
477
+ * @param {string} value - Date to validate
478
+ * @param {string} field - Field name for error messages
479
+ * @param {Object} obj - Object to set the value on
480
+ * @returns {string} Validated date string
481
+ */
482
+ const validateAndSetDate = (value, field, obj = null) => {
483
+ const requestId = ulid();
484
+ const startTime = Date.now();
485
+
486
+ logger.debug("Validating date", {
487
+ component: "Validator",
488
+ method: "validateAndSetDate",
489
+ requestId,
490
+ field,
491
+ value,
492
+ });
493
+
494
+ if (value == null || value === "0" || value === "") {
495
+ logger.debug("Date validation defaulted to RODIT_UNBOUNDED_DATE (no calendar bound)", {
496
+ component: "Validator",
497
+ method: "validateAndSetDate",
498
+ requestId,
499
+ field,
500
+ reason: "Empty or null value",
501
+ });
502
+ const defaultDate = RODIT_UNBOUNDED_DATE;
503
+
504
+ if (obj && typeof obj === "object") {
505
+ obj[field] = defaultDate;
506
+ }
507
+ return defaultDate;
508
+ }
509
+
510
+ const date = new Date(value);
511
+ if (isNaN(date.getTime()) || date < new Date("1970-01-01")) {
512
+ const duration = Date.now() - startTime;
513
+ logger.warn("Date validation failed", {
514
+ component: "Validator",
515
+ method: "validateAndSetDate",
516
+ requestId,
517
+ field,
518
+ duration,
519
+ isValid: false,
520
+ value,
521
+ reason: isNaN(date.getTime())
522
+ ? "Invalid date format"
523
+ : "Date before 1970-01-01",
524
+ });
525
+
526
+ // Emit metrics for dashboards
527
+ logger.metric("validation_duration_ms", duration, {
528
+ validationType: "date",
529
+ field,
530
+ success: false,
531
+ component: "Validator",
532
+ });
533
+ logger.metric("validation_errors_total", 1, {
534
+ validationType: "date",
535
+ field,
536
+ component: "Validator",
537
+ reason: isNaN(date.getTime()) ? "invalid_format" : "before_epoch",
538
+ });
539
+
540
+ throw new Error(
541
+ `Invalid date for ${field}: ${value}. Must be YYYY-MM-DD and no earlier than 1970-01-01`
542
+ );
543
+ }
544
+
545
+ const duration = Date.now() - startTime;
546
+ logger.debug("Date validation successful", {
547
+ component: "Validator",
548
+ method: "validateAndSetDate",
549
+ requestId,
550
+ field,
551
+ duration,
552
+ isValid: true,
553
+ });
554
+
555
+ // Emit metrics for dashboards
556
+ logger.metric("validation_duration_ms", duration, {
557
+ validationType: "date",
558
+ field,
559
+ success: true,
560
+ component: "Validator",
561
+ });
562
+
563
+ if (obj && typeof obj === "object") {
564
+ obj[field] = value;
565
+ }
566
+ return value;
567
+ };
568
+
569
+ /**
570
+ * Validates and sets a JSON value
571
+ *
572
+ * @param {string|Object} value - JSON string or object to validate
573
+ * @param {string} field - Field name for error messages
574
+ * @param {Object} obj - Object to set the value on
575
+ * @returns {string|null} Validated JSON string or null
576
+ */
577
+ const validateAndSetJson = (value, field, obj = null) => {
578
+ const requestId = ulid();
579
+ const startTime = Date.now();
580
+
581
+ logger.debug("Validating JSON", {
582
+ component: "Validator",
583
+ method: "validateAndSetJson",
584
+ requestId,
585
+ field,
586
+ });
587
+
588
+ if (value == null) {
589
+ logger.debug("JSON validation skipped, value is null", {
590
+ component: "Validator",
591
+ method: "validateAndSetJson",
592
+ requestId,
593
+ field,
594
+ });
595
+ return null;
596
+ }
597
+
598
+ let jsonString;
599
+ try {
600
+ if (typeof value === "object") {
601
+ jsonString = JSON.stringify(value);
602
+ logger.debug("Converted object to JSON string", {
603
+ component: "Validator",
604
+ method: "validateAndSetJson",
605
+ requestId,
606
+ field,
607
+ objectType: value.constructor.name,
608
+ });
609
+ } else if (typeof value === "string") {
610
+ JSON.parse(value); // Just to validate, we don't use the result
611
+ jsonString = value;
612
+ logger.debug("Validated JSON string", {
613
+ component: "Validator",
614
+ method: "validateAndSetJson",
615
+ requestId,
616
+ field,
617
+ });
618
+ } else {
619
+ const duration = Date.now() - startTime;
620
+ logger.warn("JSON validation failed - invalid type", {
621
+ component: "Validator",
622
+ method: "validateAndSetJson",
623
+ requestId,
624
+ field,
625
+ duration,
626
+ actualType: typeof value,
627
+ isValid: false,
628
+ });
629
+
630
+ // Emit metrics for dashboards
631
+ logger.metric("validation_duration_ms", duration, {
632
+ validationType: "json",
633
+ field,
634
+ success: false,
635
+ component: "Validator",
636
+ reason: "invalid_type",
637
+ });
638
+ logger.metric("validation_errors_total", 1, {
639
+ validationType: "json",
640
+ field,
641
+ component: "Validator",
642
+ reason: "invalid_type",
643
+ });
644
+
645
+ throw new Error(`Invalid type for ${field}: ${typeof value}`);
646
+ }
647
+ } catch (e) {
648
+ const duration = Date.now() - startTime;
649
+ logger.warn("JSON validation failed - parsing error", {
650
+ component: "Validator",
651
+ method: "validateAndSetJson",
652
+ requestId,
653
+ field,
654
+ duration,
655
+ errorMessage: e.message,
656
+ isValid: false,
657
+ valuePreview: typeof value === "string" ? value.substring(0, 100) : null,
658
+ });
659
+
660
+ // Emit metrics for dashboards
661
+ logger.metric("validation_duration_ms", duration, {
662
+ validationType: "json",
663
+ field,
664
+ success: false,
665
+ component: "Validator",
666
+ reason: "parse_error",
667
+ });
668
+ logger.metric("validation_errors_total", 1, {
669
+ validationType: "json",
670
+ field,
671
+ component: "Validator",
672
+ reason: "parse_error",
673
+ });
674
+
675
+ throw new Error(`Invalid JSON for ${field}: ${e.message}`);
676
+ }
677
+
678
+ const duration = Date.now() - startTime;
679
+ logger.debug("JSON validation successful", {
680
+ component: "Validator",
681
+ method: "validateAndSetJson",
682
+ requestId,
683
+ field,
684
+ duration,
685
+ isValid: true,
686
+ jsonLength: jsonString.length,
687
+ });
688
+
689
+ // Emit metrics for dashboards
690
+ logger.metric("validation_duration_ms", duration, {
691
+ validationType: "json",
692
+ field,
693
+ success: true,
694
+ component: "Validator",
695
+ });
696
+
697
+ if (obj && typeof obj === "object") {
698
+ obj[field] = jsonString;
699
+ }
700
+ return jsonString;
701
+ };
702
+
703
+ /**
704
+ * Validates and extracts credentials from parsed data
705
+ *
706
+ * @param {Object} parsedData - The parsed credential data
707
+ * @param {Object} logger - Logger instance
708
+ * @returns {Object} The validated and extracted credentials
709
+ * @throws {Error} If the credential data is invalid
710
+ */
711
+ function validateAndExtractCredentials(parsedData, logger) {
712
+ const requestId = ulid();
713
+ const context = createLogContext("CredentialManager", "validateAndExtractCredentials", { requestId });
714
+
715
+ if (logger && logger.debugWithContext) {
716
+ logger.debugWithContext("Validating credential data", context);
717
+ }
718
+
719
+ const { implicit_account_id, private_key, public_key } = parsedData;
720
+
721
+ // Validate required fields
722
+ if (!implicit_account_id || typeof implicit_account_id !== "string") {
723
+ throw new Error("Error 244: Invalid or missing implicit_account_id value");
724
+ }
725
+
726
+ if (!private_key || typeof private_key !== "string") {
727
+ throw new Error("Error 043: Invalid or missing private_key value");
728
+ }
729
+
730
+ // Process private key
731
+ const privateKeyStr = stripEd25519Prefix(private_key);
732
+ const signing_bytes_key = new Uint8Array(bs58.decode(privateKeyStr));
733
+
734
+ // Log key processing
735
+ if (logger && logger.debugWithContext) {
736
+ logger.debugWithContext("Processed credentials", {
737
+ ...context,
738
+ accountId: implicit_account_id,
739
+ keyLength: privateKeyStr.length,
740
+ isUint8Array: true
741
+ });
742
+ }
743
+
744
+ return {
745
+ account_id: implicit_account_id,
746
+ implicit_account_id,
747
+ private_key: privateKeyStr,
748
+ signing_bytes_key
749
+ };
750
+ }
751
+
752
+ /**
753
+ * Strips the 'ed25519:' prefix from a key if present
754
+ *
755
+ * @param {string} key - The key to strip the prefix from
756
+ * @returns {string} The key without the 'ed25519:' prefix
757
+ * @throws {Error} If the key is not a string or is empty
758
+ */
759
+ function stripEd25519Prefix(key) {
760
+ const requestId = ulid();
761
+
762
+ // Create a base context for this method
763
+ const baseContext = createLogContext("Utils", "stripEd25519Prefix", {
764
+ requestId,
765
+ keyType: typeof key,
766
+ hasPrefix: key && typeof key === "string" && key.startsWith("ed25519:"),
767
+ });
768
+
769
+ logger.debugWithContext("Stripping ed25519 prefix from key", baseContext);
770
+
771
+ if (!key || typeof key !== "string") {
772
+ const error = new Error("Error 053: Invalid key format");
773
+ logErrorWithMetrics(
774
+ "Invalid key format for prefix stripping",
775
+ baseContext,
776
+ error,
777
+ "key_processing_error",
778
+ { error_type: "invalid_key_format" }
779
+ );
780
+ throw error;
781
+ }
782
+
783
+ return key.replace("ed25519:", "");
784
+ }
785
+
786
+ /**
787
+ * Converts a public key to an implicit account ID according to NEAR protocol
788
+ *
789
+ * @param {string} publicKey - The public key to convert (with or without ed25519: prefix)
790
+ * @param {string} outputFormat - The output format ('hex' or 'base58')
791
+ * @returns {string} The implicit account ID
792
+ * @throws {Error} If the public key is invalid or conversion fails
793
+ */
794
+ function publicKeyToImplicitId(publicKey, outputFormat = "hex") {
795
+ const requestId = ulid();
796
+
797
+ // Create a base context for this method
798
+ const baseContext = createLogContext("Utils", "publicKeyToImplicitId", {
799
+ requestId,
800
+ keyType: typeof publicKey,
801
+ outputFormat,
802
+ });
803
+
804
+ logger.debugWithContext("Converting public key to implicit ID", baseContext);
805
+
806
+ if (!publicKey || typeof publicKey !== "string") {
807
+ const error = new Error("Error 054: Invalid public key format");
808
+ logErrorWithMetrics(
809
+ "Invalid public key format",
810
+ baseContext,
811
+ error,
812
+ "key_processing_error",
813
+ { error_type: "invalid_public_key_format" }
814
+ );
815
+ throw error;
816
+ }
817
+
818
+ try {
819
+ // Use the shared implementation from utils.js
820
+ const keyWithoutPrefix = stripEd25519Prefix(publicKey);
821
+
822
+ // Decode the base58 public key
823
+ const publicKeyBytes = bs58.decode(keyWithoutPrefix);
824
+
825
+ // The first byte is the key type (0xED for ed25519), the rest is the actual key
826
+ const publicKeyData = publicKeyBytes.slice(1);
827
+
828
+ // Convert to the requested output format
829
+ let result;
830
+ if (outputFormat === "hex") {
831
+ result = Buffer.from(publicKeyData).toString("hex");
832
+ } else if (outputFormat === "base58") {
833
+ result = bs58.encode(publicKeyData);
834
+ } else {
835
+ throw new Error(`Unsupported output format: ${outputFormat}`);
836
+ }
837
+
838
+ logger.debugWithContext(
839
+ "Successfully converted public key to implicit ID",
840
+ {
841
+ ...baseContext,
842
+ outputFormat,
843
+ idLength: result.length,
844
+ }
845
+ );
846
+
847
+ return result;
848
+ } catch (error) {
849
+ logErrorWithMetrics(
850
+ "Error converting public key to implicit ID",
851
+ baseContext,
852
+ error,
853
+ "key_processing_error",
854
+ { error_type: "conversion_error" }
855
+ );
856
+ throw error;
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Converts a base64url string to a base64 string
862
+ *
863
+ * @param {string} base64url - Base64url string
864
+ * @returns {string} Base64 string
865
+ */
866
+ function base64urlToBase64(base64url) {
867
+ return base64url
868
+ .replace(/-/g, "+")
869
+ .replace(/_/g, "/")
870
+ .padEnd(base64url.length + ((4 - (base64url.length % 4)) % 4), "=");
871
+ }
872
+
873
+ /**
874
+ * Converts a base64url encoded public key to a JWK (JSON Web Key) format
875
+ *
876
+ * @param {string} base64url_public_key - Base64url encoded public key
877
+ * @returns {Object} JWK formatted public key using jose's importJWK
878
+ */
879
+ async function base64url2jwk_public_key(base64url_public_key) {
880
+ try {
881
+ // Check if the input is a valid base64url string
882
+ const validBase64UrlRegex = /^[A-Za-z0-9_-]*$/;
883
+ const isValidFormat = validBase64UrlRegex.test(base64url_public_key);
884
+
885
+ // Create the JWK object
886
+ const jwk_public_key = {
887
+ kty: "OKP",
888
+ crv: "Ed25519",
889
+ x: base64url_public_key,
890
+ use: "sig",
891
+ };
892
+ // Let's also try to decode the base64url to see if it's the right length for Ed25519
893
+ try {
894
+ const bytes = bufferUtils.base64urlToUint8Array(base64url_public_key);
895
+ // Validate bytes length silently
896
+ } catch (decodeError) {
897
+ logger.error("Error decoding base64url public key");
898
+ }
899
+
900
+ // Import the JWK
901
+ const { importJWK } = await getJose();
902
+ const session_jwk_public_key = await importJWK(jwk_public_key, "EdDSA");
903
+ return session_jwk_public_key;
904
+ } catch (error) {
905
+ logger.errorWithContext(
906
+ "Failed converting base64url public key to JWK",
907
+ { message: error.message },
908
+ error
909
+ );
910
+ throw error;
911
+ }
912
+ }
913
+
914
+
915
+ /**
916
+ * Buffer utility functions for data conversion
917
+ */
918
+ const bufferUtils = {
919
+ /**
920
+ * Converts a hex string to Uint8Array
921
+ *
922
+ * @param {string} hexString - Hex string to convert
923
+ * @returns {Uint8Array} Converted bytes
924
+ */
925
+ hexToUint8Array: (hexString) => {
926
+ const matches = hexString.match(/.{1,2}/g) || [];
927
+ return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
928
+ },
929
+
930
+ /**
931
+ * Converts a base64url string to Uint8Array
932
+ *
933
+ * @param {string} base64url - Base64url string to convert
934
+ * @returns {Uint8Array} Converted bytes
935
+ */
936
+ base64urlToUint8Array: (base64url) => {
937
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
938
+ const padded = base64.padEnd(
939
+ base64.length + ((4 - (base64.length % 4)) % 4),
940
+ "="
941
+ );
942
+ const binary = atob(padded);
943
+ const bytes = new Uint8Array(binary.length);
944
+ for (let i = 0; i < binary.length; i++) {
945
+ bytes[i] = binary.charCodeAt(i);
946
+ }
947
+ return bytes;
948
+ },
949
+
950
+ /**
951
+ * Converts a Uint8Array to base64url string
952
+ *
953
+ * @param {Uint8Array} uint8Array - Bytes to convert
954
+ * @returns {string} Base64url string
955
+ */
956
+ uint8ArrayToBase64url: (uint8Array) => {
957
+ const binary = String.fromCharCode(...uint8Array);
958
+ const base64 = btoa(binary);
959
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
960
+ },
961
+ };
962
+
963
+ /**
964
+ * Checks if a subscription is active based on token metadata dates
965
+ *
966
+ * @param {Object} metadata - Token metadata with not_before and not_after dates
967
+ * @returns {boolean} True if subscription is active
968
+ */
969
+ // isSubscriptionActive has been moved to RoditClient class
970
+
971
+ /**
972
+ * Validates if a string is a valid CIDR IP range
973
+ *
974
+ * @param {string} cidr - CIDR notation IP range to validate
975
+ * @returns {boolean} True if valid CIDR range
976
+ */
977
+ function isValidIpRange(cidr) {
978
+ if (!cidr || typeof cidr !== "string") return false;
979
+
980
+ // Simple CIDR validation regex
981
+ const cidrRegex =
982
+ /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
983
+ return cidrRegex.test(cidr);
984
+ }
985
+
986
+ // isValidEndpoint function removed - endpoint comes from RODiT token and is correct by definition
987
+
988
+ /**
989
+ * Parses a JSON string safely with fallback to default value
990
+ *
991
+ * @param {string} json - JSON string to parse
992
+ * @param {Object} defaultValue - Default value if parsing fails
993
+ * @returns {Object} Parsed JSON or default value
994
+ */
995
+ function parseMetadataJson(json, defaultValue = {}) {
996
+ if (!json || typeof json !== "string") return defaultValue;
997
+
998
+ try {
999
+ return JSON.parse(json);
1000
+ } catch (e) {
1001
+ return defaultValue;
1002
+ }
1003
+ }
1004
+
1005
+ module.exports = {
1006
+ RODIT_UNBOUNDED_DATE,
1007
+ base64url2jwk_public_key,
1008
+ base64urlToBase64,
1009
+ calculateCanonicalHash,
1010
+ canonicalizeObject,
1011
+ dateStringToUnixTime,
1012
+ isRoditUnboundedDate,
1013
+ roditNotAfterUnixCap,
1014
+ ensureProtocol,
1015
+ isValidIpRange,
1016
+ parseMetadataJson,
1017
+ publicKeyToImplicitId,
1018
+ testFetchWithErrorHandling,
1019
+ unixTimeToDateString,
1020
+ validateAndExtractCredentials,
1021
+ validateAndSetDate,
1022
+ validateAndSetJson,
1023
+ validateAndSetUrl,
1024
+ };