@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,1971 @@
1
+ /**
2
+ * Authentication service for RODiT authentication
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const { ulid } = require("ulid");
7
+ const logger = require("../../services/logger");
8
+ const { createLogContext, logErrorWithMetrics } = logger;
9
+ const nacl = require("tweetnacl");
10
+ nacl.util = require("tweetnacl-util");
11
+ const crypto = require("crypto");
12
+ const { Resolver } = require("dns").promises;
13
+ const { calculateCanonicalHash, unixTimeToDateString } = require("../../services/utils");
14
+ const stateManager = require("../blockchain/statemanager");
15
+ const borsh = require("borsh");
16
+ const {
17
+ nearorg_rpc_timestamp,
18
+ nearorg_rpc_tokenfromroditid,
19
+ nearorg_rpc_tokensfromaccountid,
20
+ nearorg_rpc_fetchpublickeybytes,
21
+ RODiT,
22
+ PayloadNEP413,
23
+ PayloadNEP413Schema,
24
+ CONSTANTS,
25
+ } = require("../blockchain/blockchainservice");
26
+
27
+ async function verify_rodit_ownership(
28
+ peerroditid,
29
+ peertimestamp,
30
+ peerroditid_base64url_signature,
31
+ peer_rodit
32
+ ) {
33
+ const requestId = ulid();
34
+ const startTime = Date.now();
35
+
36
+ // Create a base context that will be used throughout this function
37
+ const baseContext = createLogContext(
38
+ "AuthServices",
39
+ "verify_rodit_ownership",
40
+ {
41
+ requestId,
42
+ peerRoditId: peerroditid,
43
+ timestamp: peertimestamp,
44
+ }
45
+ );
46
+
47
+ logger.infoWithContext("Starting RODiT ownership verification", baseContext);
48
+
49
+ logger.debugWithContext("Verification parameters", {
50
+ ...baseContext,
51
+ signatureLength: peerroditid_base64url_signature?.length,
52
+ signatureValue: peerroditid_base64url_signature?.substring(0, 20) + '...',
53
+ });
54
+
55
+ try {
56
+ // DO NOT DELETE THE FOLLOWING COMMENT
57
+ /* Maybe for NEP413 compatibility, the following line added "NEAR" before peerroditid */
58
+
59
+ // Match legacy implementation exactly
60
+ const timeString = await unixTimeToDateString(peertimestamp);
61
+ const roditidandtimestamp = new TextEncoder().encode(
62
+ peerroditid + timeString
63
+ );
64
+
65
+ logger.debugWithContext("Encoded roditid and timestamp", {
66
+ ...baseContext,
67
+ timeString,
68
+ combinedString: peerroditid + timeString,
69
+ bufferLength: roditidandtimestamp.length,
70
+ bufferHex: Buffer.from(roditidandtimestamp).toString('hex'),
71
+ });
72
+
73
+ // Check if signature is defined before proceeding
74
+ if (!peerroditid_base64url_signature) {
75
+ const duration = Date.now() - startTime;
76
+
77
+ logger.debugWithContext("Missing signature in authentication request", {
78
+ ...baseContext,
79
+ duration
80
+ });
81
+
82
+ // Emit metrics for dashboards
83
+ logger.metric("rodit_ownership_verification_ms", duration, {
84
+ component: "AuthServices",
85
+ success: false,
86
+ error: "MISSING_SIGNATURE"
87
+ });
88
+ logger.metric("failed_verification_attempts_total", 1, {
89
+ component: "AuthServices",
90
+ reason: "MISSING_SIGNATURE"
91
+ });
92
+
93
+ logErrorWithMetrics(
94
+ "Missing signature in authentication request",
95
+ baseContext,
96
+ new Error("Missing signature in authentication request"),
97
+ "auth_error",
98
+ { error_type: "missing_signature" }
99
+ );
100
+ throw new Error("Missing signature in authentication request");
101
+ }
102
+
103
+ const bytes_ed25519_signature = new Uint8Array(
104
+ Buffer.from(peerroditid_base64url_signature, "base64url")
105
+ );
106
+
107
+ logger.debugWithContext("Decoded signature using base64url", {
108
+ ...baseContext,
109
+ signatureLength: bytes_ed25519_signature.length,
110
+ signatureHex: Buffer.from(bytes_ed25519_signature).toString('hex'),
111
+ expectedLength: 64, // Ed25519 signatures should be 64 bytes
112
+ });
113
+
114
+ const peer_bytes_ed25519_public_key =
115
+ await nearorg_rpc_fetchpublickeybytes(
116
+ peer_rodit.owner_id
117
+ );
118
+
119
+ logger.debugWithContext("Retrieved public key", {
120
+ ...baseContext,
121
+ ownerId: peer_rodit.owner_id,
122
+ keyLength: peer_bytes_ed25519_public_key?.length || 0,
123
+ keyHex: peer_bytes_ed25519_public_key ? Buffer.from(peer_bytes_ed25519_public_key).toString('hex') : 'null',
124
+ expectedLength: 32, // Ed25519 public keys should be 32 bytes
125
+ });
126
+
127
+ // Add more detailed debugging for verification inputs
128
+ logger.debugWithContext("Verification inputs", {
129
+ ...baseContext,
130
+ messageLength: roditidandtimestamp.length,
131
+ messageContent: peerroditid + timeString,
132
+ signatureLength: bytes_ed25519_signature.length,
133
+ publicKeyLength: peer_bytes_ed25519_public_key?.length,
134
+ messageHex: Buffer.from(roditidandtimestamp).toString('hex'),
135
+ signatureHex: Buffer.from(bytes_ed25519_signature).toString('hex'),
136
+ publicKeyHex: peer_bytes_ed25519_public_key ? Buffer.from(peer_bytes_ed25519_public_key).toString('hex') : 'null'
137
+ });
138
+
139
+ const isaMatch = nacl.sign.detached.verify(
140
+ roditidandtimestamp,
141
+ bytes_ed25519_signature,
142
+ peer_bytes_ed25519_public_key
143
+ );
144
+
145
+ const duration = Date.now() - startTime;
146
+
147
+ if (isaMatch) {
148
+ // Use infoWithContext for successful verification
149
+ logger.infoWithContext("Peer RODiT ownership check successful", {
150
+ ...baseContext,
151
+ duration,
152
+ ownerId: peer_rodit.owner_id,
153
+ outcome: "success"
154
+ });
155
+
156
+ // Emit metrics for dashboards
157
+ logger.metric("rodit_ownership_verification_ms", duration, {
158
+ component: "AuthServices",
159
+ success: true,
160
+ roditId: peerroditid
161
+ });
162
+ logger.metric("successful_verification_attempts_total", 1, {
163
+ component: "AuthServices",
164
+ roditId: peerroditid
165
+ });
166
+
167
+ return true;
168
+ } else {
169
+ // Use logErrorWithMetrics for failed verification
170
+ logger.warnWithContext("Peer RODiT ownership check failed", {
171
+ ...baseContext,
172
+ duration,
173
+ ownerId: peer_rodit.owner_id,
174
+ outcome: "failed"
175
+ });
176
+
177
+ // Emit metrics for dashboards
178
+ logger.metric("rodit_ownership_verification_ms", duration, {
179
+ component: "AuthServices",
180
+ success: false,
181
+ error: "SIGNATURE_VERIFICATION_FAILED",
182
+ roditId: peerroditid
183
+ });
184
+ logger.metric("failed_verification_attempts_total", 1, {
185
+ component: "AuthServices",
186
+ reason: "SIGNATURE_VERIFICATION_FAILED",
187
+ roditId: peerroditid
188
+ });
189
+
190
+ logErrorWithMetrics(
191
+ "Peer RODiT ownership check failed",
192
+ {
193
+ ...baseContext,
194
+ duration,
195
+ ownerId: peer_rodit.owner_id,
196
+ outcome: "failed"
197
+ },
198
+ new Error("Error 035: PeerEd25519SignatureVerificationFailure"),
199
+ "rodit_ownership_verification",
200
+ {
201
+ result: "failure",
202
+ peer_rodit_id: peerroditid,
203
+ duration
204
+ }
205
+ );
206
+
207
+ throw new Error("Error 035: PeerEd25519SignatureVerificationFailure");
208
+ }
209
+ } catch (error) {
210
+ const duration = Date.now() - startTime;
211
+
212
+ // Emit metrics for dashboards
213
+ logger.metric("rodit_ownership_verification_ms", duration, {
214
+ component: "AuthServices",
215
+ success: false,
216
+ error: error.name || "Unknown",
217
+ roditId: peerroditid
218
+ });
219
+ logger.metric("failed_verification_attempts_total", 1, {
220
+ component: "AuthServices",
221
+ reason: error.name || "Unknown",
222
+ roditId: peerroditid
223
+ });
224
+
225
+ logErrorWithMetrics(
226
+ "RODiT ownership verification failed",
227
+ {
228
+ ...baseContext,
229
+ duration,
230
+ peerRoditId: peerroditid
231
+ },
232
+ error,
233
+ "rodit_ownership_verification",
234
+ {
235
+ result: "error",
236
+ error_type: error.name || "Unknown",
237
+ peer_rodit_id: peerroditid,
238
+ duration
239
+ }
240
+ );
241
+
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ async function verify_rodit_ownership_withnep413(
247
+ message,
248
+ nonce,
249
+ recipient,
250
+ callbackUrl,
251
+ signature,
252
+ peer_rodit
253
+ ) {
254
+ const requestId = ulid();
255
+ const startTime = Date.now();
256
+
257
+ // Create a base context that will be used throughout this function
258
+ const baseContext = createLogContext(
259
+ "AuthServices",
260
+ "verify_rodit_ownership_withnep413",
261
+ {
262
+ requestId,
263
+ messageLength: message?.length,
264
+ recipientId: recipient
265
+ }
266
+ );
267
+
268
+ try {
269
+ logger.debugWithContext("Starting NEP-413 signature verification", baseContext);
270
+
271
+ // Ensure nonce is correctly formatted
272
+ let nonceArray;
273
+ if (typeof nonce === "string") {
274
+ // Handle base64url encoded nonce
275
+ nonceArray = new Uint8Array(Buffer.from(nonce, "base64url"));
276
+ } else if (Array.isArray(nonce)) {
277
+ nonceArray = new Uint8Array(nonce);
278
+ } else if (typeof nonce === "object" && nonce !== null) {
279
+ nonceArray = new Uint8Array(Object.values(nonce));
280
+ } else {
281
+ throw new Error(`Invalid nonce format: ${typeof nonce}`);
282
+ }
283
+
284
+ if (nonceArray.length !== 32) {
285
+ const error = new Error(`Invalid nonce length: ${nonceArray.length}, expected 32`);
286
+ logErrorWithMetrics(
287
+ "Invalid nonce length in NEP-413 verification",
288
+ { ...baseContext, nonceLength: nonceArray.length },
289
+ error,
290
+ "nep413_verification_error",
291
+ { error_type: "invalid_nonce_length" }
292
+ );
293
+ throw error;
294
+ }
295
+
296
+ const payload = new PayloadNEP413({
297
+ tag: 2147484061,
298
+ message,
299
+ nonce: nonceArray,
300
+ recipient,
301
+ callbackUrl,
302
+ });
303
+
304
+ const serializedPayload = borsh.serialize(PayloadNEP413Schema, payload);
305
+ const payloadHash = crypto
306
+ .createHash("sha256")
307
+ .update(serializedPayload)
308
+ .digest();
309
+
310
+ // Convert base64url signature to standard base64
311
+ const standardBase64 = signature
312
+ .replace(/-/g, "+")
313
+ .replace(/_/g, "/")
314
+ .padEnd(signature.length + ((4 - (signature.length % 4)) % 4), "=");
315
+ const signatureBytes = nacl.util.decodeBase64(standardBase64);
316
+
317
+ // Get public key bytes
318
+ const publicKeyBytes = await nearorg_rpc_fetchpublickeybytes(
319
+ peer_rodit.owner_id
320
+ );
321
+
322
+ // Perform verification
323
+ const isaMatch = nacl.sign.detached.verify(
324
+ payloadHash,
325
+ signatureBytes,
326
+ publicKeyBytes
327
+ );
328
+
329
+ const duration = Date.now() - startTime;
330
+
331
+ if (isaMatch) {
332
+ logger.infoWithContext("Peer RODiT possession check successful", {
333
+ ...baseContext,
334
+ duration,
335
+ outcome: "success"
336
+ });
337
+ return true;
338
+ } else {
339
+ const error = new Error("PeerEd25519SignatureVerificationFailure");
340
+ logErrorWithMetrics(
341
+ "Peer RODiT possession check failed",
342
+ {
343
+ ...baseContext,
344
+ duration,
345
+ outcome: "failed"
346
+ },
347
+ error,
348
+ "rodit_ownership_verification",
349
+ { error_type: "signature_verification_failure" }
350
+ );
351
+ throw error;
352
+ }
353
+ } catch (error) {
354
+ const duration = Date.now() - startTime;
355
+ logErrorWithMetrics(
356
+ "RODiT ownership verification failed",
357
+ {
358
+ ...baseContext,
359
+ duration,
360
+ error: error.message
361
+ },
362
+ error,
363
+ "rodit_ownership_verification",
364
+ { error_type: "verification_error" }
365
+ );
366
+ throw error;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Authenticate a webhook request
372
+ *
373
+ * @param {string} payload - Webhook payload
374
+ * @param {string} signature_hex_ofpayload - Signature of payload
375
+ * @param {number} timestamp - Request timestamp
376
+ * @param {string} server_public_key_base64url - Server's public key from RODiT in base64url format
377
+ * @returns {Promise<Object>} Authentication result
378
+ */
379
+ async function authenticate_webhook(
380
+ payload,
381
+ signature_hex_ofpayload,
382
+ timestamp,
383
+ server_public_key_base64url
384
+ ) {
385
+ const requestId = ulid();
386
+ const startTime = Date.now();
387
+
388
+ // Create a base context that will be used throughout this function
389
+ const baseContext = createLogContext(
390
+ "AuthServices",
391
+ "authenticate_webhook",
392
+ {
393
+ requestId,
394
+ timestamp
395
+ }
396
+ );
397
+
398
+ logger.debugWithContext("Starting webhook authentication", {
399
+ ...baseContext,
400
+ hasPayload: !!payload,
401
+ hasSignature: !!signature_hex_ofpayload,
402
+ hasTimestamp: !!timestamp,
403
+ hasServerPublicKey: !!server_public_key_base64url,
404
+ serverKeyLength: server_public_key_base64url?.length,
405
+ payloadLength: payload?.length || 0,
406
+ signatureLength: signature_hex_ofpayload?.length || 0,
407
+ timestampValue: timestamp,
408
+ signatureFirstChars: signature_hex_ofpayload ? signature_hex_ofpayload.substring(0, 15) + '...' : 'null',
409
+ serverKeyFirstChars: server_public_key_base64url ? server_public_key_base64url.substring(0, 15) + '...' : 'null'
410
+ });
411
+
412
+ // Only log detailed debugging info at debug level
413
+ logger.debugWithContext("Starting webhook authentication process", baseContext);
414
+
415
+ try {
416
+ const currentTime = Date.now();
417
+ const parsedTimestamp = parseInt(timestamp);
418
+ const timeThreshold = 5 * 60 * 1000; // 5 minutes
419
+
420
+ // Check if timestamp is too old
421
+ if (currentTime - parsedTimestamp > timeThreshold) {
422
+ const duration = Date.now() - startTime;
423
+
424
+ logger.warnWithContext("Webhook authentication failed - timestamp too old", {
425
+ ...baseContext,
426
+ duration,
427
+ timestampAge: (currentTime - parsedTimestamp) / 1000,
428
+ threshold: timeThreshold / 1000
429
+ });
430
+
431
+ // Emit metrics for dashboards
432
+ logger.metric("webhook_authentication_duration_ms", duration, {
433
+ component: "AuthServices",
434
+ success: false,
435
+ reason: "TIMESTAMP_EXPIRED",
436
+ });
437
+ logger.metric("webhook_authentication_failures_total", 1, {
438
+ component: "AuthServices",
439
+ reason: "TIMESTAMP_EXPIRED",
440
+ });
441
+
442
+ return {
443
+ isValid: false,
444
+ error: {
445
+ code: "TIMESTAMP_EXPIRED",
446
+ message: "Webhook timestamp is too old",
447
+ requestId,
448
+ },
449
+ };
450
+ }
451
+
452
+ logger.debugWithContext("Calculating payload hash for verification", {
453
+ ...baseContext,
454
+ payloadSize: payload.length
455
+ });
456
+
457
+ // IMPORTANT: The server normalizes the payload before signing
458
+ // We must use the raw payload as received without additional normalization
459
+
460
+ // Log the raw payload for complete visibility with detailed format information
461
+ logger.debugWithContext("Raw payload for verification", {
462
+ ...baseContext,
463
+ payload: payload, // Log the full payload
464
+ payloadSize: payload.length,
465
+ payloadType: typeof payload,
466
+ payloadIsString: typeof payload === 'string',
467
+ payloadFirstChars: payload.substring(0, 100) + (payload.length > 100 ? '...' : '')
468
+ });
469
+
470
+ // Create the string to hash: payload + timestamp (same as in send_webhook)
471
+ // Use the raw payload without normalization
472
+ const payloadWithTimestamp = payload + timestamp.toString();
473
+
474
+ logger.debugWithContext("Creating payload+timestamp string for verification", {
475
+ ...baseContext,
476
+ payloadSize: payload.length,
477
+ timestampLength: timestamp.toString().length,
478
+ combinedLength: payloadWithTimestamp.length,
479
+ wasNormalized: false,
480
+ // Check if timestamp is properly appended
481
+ endsWithTimestamp: payloadWithTimestamp.endsWith(timestamp.toString())
482
+ });
483
+
484
+ // Calculate hash of payload+timestamp
485
+ const sha256_ofpayload = crypto
486
+ .createHash("sha256")
487
+ .update(payloadWithTimestamp)
488
+ .digest();
489
+
490
+ // Log the hash in hex format for debugging
491
+ const sha256_hex = Buffer.from(sha256_ofpayload).toString('hex');
492
+ logger.debugWithContext("Calculated hash for verification", {
493
+ ...baseContext,
494
+ sha256_hex: sha256_hex,
495
+ hashLength: sha256_ofpayload.length
496
+ });
497
+
498
+
499
+
500
+ logger.debugWithContext("Converting signature to buffer", {
501
+ ...baseContext,
502
+ signatureHex: signature_hex_ofpayload,
503
+ signatureHexLength: signature_hex_ofpayload.length,
504
+ // Check if signature is valid hex (should be even length and only hex chars)
505
+ isValidHex: /^[0-9a-fA-F]+$/.test(signature_hex_ofpayload) && signature_hex_ofpayload.length % 2 === 0
506
+ });
507
+
508
+ // Convert the hex signature to a Uint8Array for verification
509
+ // This matches how signatures are created in send_webhook
510
+ const buffer_signature_ofpayload = new Uint8Array(
511
+ Buffer.from(signature_hex_ofpayload, "hex")
512
+ );
513
+
514
+ logger.debugWithContext("Signature converted to buffer", {
515
+ ...baseContext,
516
+ bufferLength: buffer_signature_ofpayload.length,
517
+ // Log first few bytes of the buffer for verification
518
+ bufferFirstBytes: Array.from(buffer_signature_ofpayload.slice(0, 4)),
519
+ // Log last few bytes of the buffer for verification
520
+ bufferLastBytes: Array.from(buffer_signature_ofpayload.slice(-4))
521
+ });
522
+
523
+ // Log the server public key before conversion for debugging
524
+ logger.debugWithContext("Server public key before conversion", {
525
+ ...baseContext,
526
+ serverKeyBase64Url: server_public_key_base64url,
527
+ serverKeyBase64UrlLength: server_public_key_base64url.length,
528
+ // Check if key is valid base64url (no +, /, or =)
529
+ isValidBase64Url: /^[A-Za-z0-9_-]*$/.test(server_public_key_base64url)
530
+ });
531
+
532
+ // Convert base64url encoded key to bytes for use with nacl
533
+ const server_public_key = new Uint8Array(
534
+ Buffer.from(server_public_key_base64url, "base64url")
535
+ );
536
+
537
+ logger.debugWithContext("Using server public key for verification", {
538
+ ...baseContext,
539
+ serverKeyLength: server_public_key.length,
540
+ // Log the key in different formats for comparison with server logs
541
+ serverKeyHex: Buffer.from(server_public_key).toString('hex'),
542
+ serverKeyBase64: Buffer.from(server_public_key).toString('base64'),
543
+ serverKeyBase64Url: Buffer.from(server_public_key).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
544
+ serverKeyHexShort: Buffer.from(server_public_key).toString('hex').substring(0, 16) + '...',
545
+ });
546
+
547
+ // Log detailed information about the verification inputs
548
+ logger.debugWithContext("Signature verification details", {
549
+ ...baseContext,
550
+ payloadHashHex: Buffer.from(sha256_ofpayload).toString('hex'),
551
+ signatureHex: signature_hex_ofpayload,
552
+ signatureLength: buffer_signature_ofpayload.length,
553
+ serverKeyHex: Buffer.from(server_public_key).toString('hex'),
554
+ serverKeyBase64: Buffer.from(server_public_key).toString('base64'),
555
+ serverKeyBase64url: server_public_key_base64url
556
+ });
557
+
558
+ // Verify signature using the server's public key
559
+ const verificationStartTime = Date.now();
560
+
561
+ // Log all verification inputs in detail with multiple encoding formats
562
+ logger.debugWithContext("Detailed verification inputs", {
563
+ ...baseContext,
564
+ // Hash in different formats
565
+ hashHex: Buffer.from(sha256_ofpayload).toString('hex'),
566
+ hashBase64: Buffer.from(sha256_ofpayload).toString('base64'),
567
+ hashBase64Url: Buffer.from(sha256_ofpayload).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
568
+ hashLength: sha256_ofpayload.length,
569
+ // Signature in different formats
570
+ signatureHex: Buffer.from(buffer_signature_ofpayload).toString('hex'),
571
+ signatureBase64: Buffer.from(buffer_signature_ofpayload).toString('base64'),
572
+ signatureBase64Url: Buffer.from(buffer_signature_ofpayload).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
573
+ signatureLength: buffer_signature_ofpayload.length,
574
+ // Public key in different formats
575
+ publicKeyHex: Buffer.from(server_public_key).toString('hex'),
576
+ publicKeyBase64: Buffer.from(server_public_key).toString('base64'),
577
+ publicKeyBase64Url: Buffer.from(server_public_key).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
578
+ publicKeyLength: server_public_key.length
579
+ });
580
+
581
+ // Perform standard signature verification
582
+ let isValid = false;
583
+
584
+ try {
585
+ // Use the standard verification method only
586
+ isValid = nacl.sign.detached.verify(
587
+ sha256_ofpayload,
588
+ buffer_signature_ofpayload,
589
+ server_public_key
590
+ );
591
+
592
+ logger.debug("Standard signature verification completed", {
593
+ component: "AuthServices",
594
+ method: "authenticate_webhook",
595
+ requestId,
596
+ isValid
597
+ });
598
+ } catch (error) {
599
+ logger.warn("Signature verification failed with error", {
600
+ component: "AuthServices",
601
+ method: "authenticate_webhook",
602
+ requestId,
603
+ error: error.message
604
+ });
605
+ isValid = false;
606
+ }
607
+
608
+ const verificationDuration = Date.now() - verificationStartTime;
609
+
610
+ // Log the verification result
611
+ logger.info("Webhook signature verification result", {
612
+ component: "AuthServices",
613
+ method: "authenticate_webhook",
614
+ requestId,
615
+ isValid,
616
+ verificationDuration
617
+ });
618
+
619
+ // Log verification metrics
620
+ logger.metric(
621
+ "signature_verification_duration_ms",
622
+ verificationDuration,
623
+ {
624
+ component: "AuthServices",
625
+ success: isValid,
626
+ }
627
+ );
628
+
629
+ if (!isValid) {
630
+ const duration = Date.now() - startTime;
631
+
632
+ logger.warn("Webhook authentication failed - invalid signature", {
633
+ component: "AuthServices",
634
+ method: "authenticate_webhook",
635
+ requestId,
636
+ duration,
637
+ verificationDuration,
638
+ });
639
+
640
+ // Emit metrics for dashboards
641
+ logger.metric("webhook_authentication_duration_ms", duration, {
642
+ component: "AuthServices",
643
+ success: false,
644
+ reason: "WEBHOOK_SIGNATURE_INVALID",
645
+ });
646
+ logger.metric("webhook_authentication_failures_total", 1, {
647
+ component: "AuthServices",
648
+ reason: "WEBHOOK_SIGNATURE_INVALID",
649
+ });
650
+
651
+ return {
652
+ isValid: false,
653
+ error: {
654
+ code: "WEBHOOK_SIGNATURE_INVALID",
655
+ message: "Webhook signature verification failed: Ed25519 proof over the webhook payload did not verify.",
656
+ requestId,
657
+ },
658
+ };
659
+ }
660
+
661
+ const duration = Date.now() - startTime;
662
+ logger.info("Webhook authentication successful", {
663
+ component: "AuthServices",
664
+ method: "authenticate_webhook",
665
+ requestId,
666
+ duration,
667
+ verificationDuration,
668
+ });
669
+
670
+ // Emit metrics for dashboards
671
+ logger.metric("webhook_authentication_duration_ms", duration, {
672
+ component: "AuthServices",
673
+ success: true,
674
+ });
675
+ logger.metric("successful_webhook_authentications_total", 1, {
676
+ component: "AuthServices",
677
+ });
678
+
679
+ return {
680
+ isValid: true,
681
+ message: "Webhook authentication successful",
682
+ requestId,
683
+ duration,
684
+ };
685
+ } catch (error) {
686
+ const duration = Date.now() - startTime;
687
+
688
+ logger.error("Webhook authentication error", {
689
+ component: "AuthServices",
690
+ method: "authenticate_webhook",
691
+ requestId,
692
+ duration,
693
+ errorMessage: error.message,
694
+ errorCode: error.code || "UNKNOWN_ERROR",
695
+ stack: error.stack,
696
+ });
697
+
698
+ // Emit metrics for dashboards
699
+ logger.metric("webhook_authentication_duration_ms", duration, {
700
+ component: "AuthServices",
701
+ success: false,
702
+ error: error.code || "UNKNOWN_ERROR",
703
+ });
704
+ logger.metric("webhook_authentication_errors_total", 1, {
705
+ component: "AuthServices",
706
+ error: error.constructor.name,
707
+ });
708
+
709
+ return {
710
+ isValid: false,
711
+ error: {
712
+ code: "AUTHENTICATION_ERROR",
713
+ message: "An unexpected error occurred during webhook authentication",
714
+ details: error.message,
715
+ requestId,
716
+ },
717
+ };
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Validates that a timestamp is not in the future
723
+ *
724
+ * @param {number} timestamp - Unix timestamp in seconds
725
+ * @param {number} maxAgeSeconds - Maximum age in seconds (not used, kept for backward compatibility)
726
+ * @returns {Promise<boolean>} True if timestamp is valid, false otherwise
727
+ */
728
+ async function validateTimestamp(timestamp, maxAgeSeconds = 300) {
729
+ const requestId = ulid();
730
+ const currentTime = Math.floor(Date.now() / 1000);
731
+ const timeDifference = currentTime - timestamp;
732
+
733
+ // Check if timestamp is in the future (with small buffer for clock skew)
734
+ if (timestamp > currentTime + 30) {
735
+ logger.warn("Authentication rejected: timestamp is in the future", {
736
+ component: "RoditAuth",
737
+ method: "validateTimestamp",
738
+ requestId,
739
+ timestamp,
740
+ currentTime,
741
+ difference: timestamp - currentTime
742
+ });
743
+ return false;
744
+ }
745
+
746
+ // We've removed the timestamp age check as it's not needed with our comprehensive validation system
747
+ // The system already validates tokens through other means
748
+
749
+ return true;
750
+ }
751
+
752
+ function roditTokenResolved(pr) {
753
+ return !!(pr && pr.token_id);
754
+ }
755
+
756
+ /**
757
+ * Resolve peer RODiT for login_client: exactly one of roditid or accountid must be non-empty (callers enforce).
758
+ * Roditid path: token by id, then 64-hex implicit fallback on that field alone. Account path: tokens by account.
759
+ *
760
+ * @param {string} roditidTrimmed - trimmed roditid or ""
761
+ * @param {string} accountidTrimmed - trimmed accountid or ""
762
+ * @returns {Promise<object|null>} Peer RODiT instance (possibly empty), or null if both identifiers non-empty
763
+ */
764
+ async function resolve_peer_rodit_for_login(roditidTrimmed, accountidTrimmed) {
765
+ const r = roditidTrimmed ? String(roditidTrimmed).trim() : "";
766
+ const a = accountidTrimmed ? String(accountidTrimmed).trim() : "";
767
+ if (r && a) {
768
+ return null;
769
+ }
770
+
771
+ let peer_rodit = null;
772
+
773
+ if (r) {
774
+ peer_rodit = await nearorg_rpc_tokenfromroditid(r);
775
+ if (roditTokenResolved(peer_rodit)) {
776
+ return peer_rodit;
777
+ }
778
+ if (/^[0-9a-f]{64}$/i.test(r)) {
779
+ peer_rodit = await nearorg_rpc_tokensfromaccountid(r.toLowerCase());
780
+ }
781
+ return peer_rodit;
782
+ }
783
+
784
+ if (a) {
785
+ peer_rodit = await nearorg_rpc_tokensfromaccountid(a.toLowerCase());
786
+ }
787
+
788
+ return peer_rodit;
789
+ }
790
+
791
+ /**
792
+ * Verify an already-resolved peer RODiT (timestamp, signature, match, live, active, trust).
793
+ *
794
+ * @param {object|null} peer_rodit - Resolved peer token from chain (or empty)
795
+ * @param {string} peerroditid - Identifier string used in verify_rodit_ownership (client-signed prefix)
796
+ * @param {number} peertimestamp - Unix seconds
797
+ * @param {string} peerroditid_base64url_signature - base64url Ed25519 signature
798
+ */
799
+ async function verify_peer_rodit(
800
+ peer_rodit,
801
+ peerroditid,
802
+ peertimestamp,
803
+ peerroditid_base64url_signature
804
+ ) {
805
+ const requestId = ulid();
806
+ const startTime = Date.now();
807
+
808
+ const config_own_rodit = await stateManager.getConfigOwnRodit();
809
+
810
+ logger.debug("Starting peer RODiT verification", {
811
+ component: "RoditAuth",
812
+ method: "verify_peer_rodit",
813
+ requestId,
814
+ peerRoditId: peerroditid,
815
+ timestamp: peertimestamp,
816
+ signatureLength: peerroditid_base64url_signature?.length,
817
+ hasOwnRodit: !!config_own_rodit,
818
+ ownRoditId: config_own_rodit?.token_id,
819
+ });
820
+
821
+ try {
822
+ const timestampValid = await validateTimestamp(peertimestamp);
823
+
824
+ if (!timestampValid) {
825
+ logger.warn("Invalid timestamp, aborting RODiT verification", {
826
+ component: "RoditAuth",
827
+ method: "verify_peer_rodit",
828
+ requestId,
829
+ roditId: peerroditid,
830
+ timestamp: peertimestamp,
831
+ currentTime: Math.floor(Date.now() / 1000),
832
+ });
833
+ return {
834
+ peer_rodit: null,
835
+ goodrodit: false,
836
+ failureReason: "LOGIN_CHALLENGE_TIMESTAMP_INVALID",
837
+ failureMessage:
838
+ "Login challenge timestamp invalid: the Unix `timestamp` from your POST body is too far in the future relative to server time (use the `timestamp` from the same GET /api/login/timestamp response as your login signing payload; check clock skew)."
839
+ };
840
+ }
841
+
842
+ logger.debug("Timestamp validation ok", {
843
+ component: "RoditAuth",
844
+ method: "verify_peer_rodit",
845
+ requestId,
846
+ timestamp: peertimestamp
847
+ });
848
+
849
+ logger.debug("Peer RODiT resolved for verification", {
850
+ requestId,
851
+ peerRoditId: peer_rodit?.token_id,
852
+ peerRoditOwnerId: peer_rodit?.owner_id,
853
+ hasPeerRoditMetadata: !!(peer_rodit && peer_rodit.metadata),
854
+ metadataKeys:
855
+ peer_rodit && peer_rodit.metadata
856
+ ? Object.keys(peer_rodit.metadata)
857
+ : [],
858
+ });
859
+
860
+ if (!roditTokenResolved(peer_rodit)) {
861
+ logger.error("Failed to retrieve peer RODiT data", {
862
+ component: "AuthServices",
863
+ method: "verify_peer_rodit",
864
+ requestId,
865
+ duration: Date.now() - startTime,
866
+ peerRoditId: peerroditid,
867
+ });
868
+ return {
869
+ peer_rodit: null,
870
+ goodrodit: false,
871
+ failureReason: "RODIT_NOT_FOUND",
872
+ failureMessage: "RODiT not found on blockchain"
873
+ };
874
+ }
875
+
876
+ if (!peer_rodit.metadata) {
877
+ logger.error("Peer RODiT missing metadata", {
878
+ component: "AuthServices",
879
+ method: "verify_peer_rodit",
880
+ requestId,
881
+ duration: Date.now() - startTime,
882
+ peerRoditId: peerroditid,
883
+ peerRoditOwnerId: peer_rodit.owner_id,
884
+ });
885
+ return {
886
+ peer_rodit: null,
887
+ goodrodit: false,
888
+ failureReason: "RODIT_MISSING_METADATA",
889
+ failureMessage: "RODiT is missing required metadata"
890
+ };
891
+ }
892
+
893
+ const ownershipStart = Date.now();
894
+ const ownershipVerified = await verify_rodit_ownership(
895
+ peerroditid,
896
+ peertimestamp,
897
+ peerroditid_base64url_signature,
898
+ peer_rodit
899
+ );
900
+ const ownershipDuration = Date.now() - ownershipStart;
901
+
902
+ logger.debug("Ownership verification completed", {
903
+ requestId,
904
+ ownershipDuration,
905
+ ownershipVerified,
906
+ });
907
+
908
+ if (!ownershipVerified) {
909
+ logger.warn("Invalid signature, aborting RODiT verification", {
910
+ requestId,
911
+ roditId: peerroditid,
912
+ });
913
+ return {
914
+ peer_rodit,
915
+ goodrodit: false,
916
+ failureReason: "LOGIN_BASE64URL_SIGNATURE_INVALID",
917
+ failureMessage:
918
+ "Login base64url signature invalid: Ed25519 verification failed for the base64url_signature over UTF-8 (roditid or accountid) + canonical timestamp_iso from the login challenge (GET /api/login/timestamp). Wrong key, wrong payload, or wrong encoding (must be base64url, not standard base64)."
919
+ };
920
+ }
921
+
922
+ const matchStart = Date.now();
923
+
924
+ if (!config_own_rodit || !config_own_rodit.own_rodit.metadata) {
925
+ logger.error("Own RODiT configuration is incomplete", {
926
+ component: "AuthServices",
927
+ method: "verify_peer_rodit",
928
+ requestId,
929
+ duration: Date.now() - startTime,
930
+ hasOwnRodit: !!config_own_rodit,
931
+ hasMetadata: config_own_rodit && !!config_own_rodit.own_rodit.metadata
932
+ });
933
+ return {
934
+ peer_rodit,
935
+ goodrodit: false,
936
+ failureReason: "SERVER_CONFIG_INCOMPLETE",
937
+ failureMessage: "Server RODiT configuration is incomplete"
938
+ };
939
+ }
940
+
941
+ const matchResult = await verify_rodit_isamatch(
942
+ config_own_rodit.own_rodit.metadata.serviceprovider_id,
943
+ peer_rodit
944
+ );
945
+ const matchDuration = Date.now() - matchStart;
946
+
947
+ logger.debug("Match verification completed", {
948
+ requestId,
949
+ matchDuration,
950
+ isMatch: matchResult.isMatch,
951
+ verificationType: matchResult.verificationType,
952
+ failureReason: matchResult.failureReason,
953
+ });
954
+
955
+ if (!matchResult.isMatch) {
956
+ logger.warn("RODiT match verification failed", {
957
+ requestId,
958
+ roditId: peerroditid,
959
+ failureReason: matchResult.failureReason,
960
+ failureMessage: matchResult.failureMessage,
961
+ });
962
+ return {
963
+ peer_rodit,
964
+ goodrodit: false,
965
+ failureReason: matchResult.failureReason,
966
+ failureMessage: matchResult.failureMessage
967
+ };
968
+ }
969
+
970
+ const liveStart = Date.now();
971
+ const isLive = await verify_rodit_islive(
972
+ peer_rodit.metadata.not_after,
973
+ peer_rodit.metadata.not_before
974
+ );
975
+ const liveDuration = Date.now() - liveStart;
976
+
977
+ logger.debug("Live verification completed", {
978
+ requestId,
979
+ liveDuration,
980
+ isLive,
981
+ });
982
+
983
+ if (!isLive) {
984
+ logger.warn("RODiT live verification failed", {
985
+ requestId,
986
+ roditId: peerroditid,
987
+ });
988
+ return {
989
+ peer_rodit,
990
+ goodrodit: false,
991
+ failureReason: "RODIT_NOT_LIVE",
992
+ failureMessage: "RODiT is expired or not yet valid"
993
+ };
994
+ }
995
+
996
+ const activeStart = Date.now();
997
+ const isActive = await verify_rodit_isactive(
998
+ peer_rodit.token_id,
999
+ config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url
1000
+ );
1001
+ const activeDuration = Date.now() - activeStart;
1002
+
1003
+ logger.debug("Active verification completed", {
1004
+ requestId,
1005
+ activeDuration,
1006
+ isActive,
1007
+ });
1008
+
1009
+ if (!isActive) {
1010
+ logger.warn("RODiT active verification failed", {
1011
+ requestId,
1012
+ roditId: peerroditid,
1013
+ });
1014
+ return {
1015
+ peer_rodit,
1016
+ goodrodit: false,
1017
+ failureReason: "RODIT_REVOKED",
1018
+ failureMessage: "RODiT has been revoked"
1019
+ };
1020
+ }
1021
+
1022
+ const trustedStart = Date.now();
1023
+ const isTrusted = await verify_rodit_istrusted_issuingsmartcontract(
1024
+ config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url
1025
+ );
1026
+ const trustedDuration = Date.now() - trustedStart;
1027
+
1028
+ logger.debug("Trust verification completed", {
1029
+ requestId,
1030
+ trustedDuration,
1031
+ isTrusted,
1032
+ });
1033
+
1034
+ if (!isTrusted) {
1035
+ logger.warn("RODiT trusted verification failed", {
1036
+ requestId,
1037
+ roditId: peerroditid,
1038
+ });
1039
+ return {
1040
+ peer_rodit,
1041
+ goodrodit: false,
1042
+ failureReason: "SMART_CONTRACT_NOT_TRUSTED",
1043
+ failureMessage: "Issuing smart contract is not trusted by this server"
1044
+ };
1045
+ }
1046
+
1047
+ const totalDuration = Date.now() - startTime;
1048
+
1049
+ logger.info("Peer RODiT verification successful", {
1050
+ component: "AuthServices",
1051
+ method: "verify_peer_rodit",
1052
+ requestId,
1053
+ duration: totalDuration,
1054
+ peerRoditId: peerroditid,
1055
+ peerOwnerId: peer_rodit.owner_id,
1056
+ });
1057
+
1058
+ return {
1059
+ peer_rodit,
1060
+ goodrodit: true,
1061
+ };
1062
+ } catch (error) {
1063
+ const duration = Date.now() - startTime;
1064
+
1065
+ logger.error("Error in verify_peer_rodit", {
1066
+ component: "AuthServices",
1067
+ method: "verify_peer_rodit",
1068
+ requestId,
1069
+ duration,
1070
+ error: {
1071
+ message: error.message,
1072
+ stack: error.stack,
1073
+ name: error.name,
1074
+ },
1075
+ });
1076
+
1077
+ logger.metric &&
1078
+ logger.metric("rodit_verification_errors", 1, {
1079
+ error_type: error.name || "Unknown",
1080
+ });
1081
+
1082
+ return {
1083
+ peer_rodit: null,
1084
+ goodrodit: false,
1085
+ failureReason: error.failureReason || error.code || error.name || "VERIFICATION_ERROR",
1086
+ failureMessage: error.failureMessage || error.message || "Unknown verification error",
1087
+ error: `Error in verify_peer_rodit: ${error.message}`
1088
+ };
1089
+ }
1090
+ }
1091
+
1092
+ async function verify_rodit_islive(peer_rodit_notafter, peer_rodit_notbefore) {
1093
+ const requestId = ulid();
1094
+ const startTime = Date.now();
1095
+
1096
+ logger.debug("Checking RODiT time validity", {
1097
+ component: "RoditAuth",
1098
+ method: "verify_rodit_islive",
1099
+ requestId,
1100
+ notAfter: peer_rodit_notafter,
1101
+ notBefore: peer_rodit_notbefore,
1102
+ });
1103
+
1104
+ function parseDate(datestring) {
1105
+ const date = new Date(datestring);
1106
+ return isNaN(date.getTime()) ? new Date(0) : date;
1107
+ }
1108
+
1109
+ const datetimenul = new Date(0);
1110
+ const datetimenotafter = parseDate(peer_rodit_notafter);
1111
+ const datetimenotbefore = parseDate(peer_rodit_notbefore);
1112
+
1113
+ logger.debug("Parsed validity dates", {
1114
+ requestId,
1115
+ parsedNotAfter: datetimenotafter.toISOString(),
1116
+ parsedNotBefore: datetimenotbefore.toISOString(),
1117
+ isNotAfterNull: datetimenotafter.getTime() === datetimenul.getTime(),
1118
+ isNotBeforeNull: datetimenotbefore.getTime() === datetimenul.getTime(),
1119
+ });
1120
+
1121
+ try {
1122
+ const rpcStart = Date.now();
1123
+ const stringtimenow = await nearorg_rpc_timestamp();
1124
+ const rpcDuration = Date.now() - rpcStart;
1125
+
1126
+ logger.debug("Retrieved blockchain timestamp", {
1127
+ requestId,
1128
+ rpcDuration,
1129
+ blockchainTimestamp: stringtimenow,
1130
+ });
1131
+
1132
+ const timestamp = parseInt(stringtimenow, 10);
1133
+
1134
+ if (isNaN(timestamp)) {
1135
+ logger.error("Failed to parse blockchain timestamp", {
1136
+ component: "AuthServices",
1137
+ requestId,
1138
+ duration: Date.now() - startTime,
1139
+ blockchainTimestamp: stringtimenow,
1140
+ });
1141
+
1142
+ // Add metrics for timestamp parsing errors
1143
+ logger.metric &&
1144
+ logger.metric("rodit_islive_errors", 1, {
1145
+ error_type: "timestamp_parse_error",
1146
+ blockchain_timestamp: stringtimenow,
1147
+ });
1148
+
1149
+ return false;
1150
+ }
1151
+
1152
+ const datetimetimestamp = new Date(timestamp / 1000000); // Convert nanoseconds to milliseconds
1153
+
1154
+ logger.debug("Converted blockchain time", {
1155
+ requestId,
1156
+ blockchainTime: datetimetimestamp.toISOString(),
1157
+ originalTimestamp: timestamp,
1158
+ });
1159
+
1160
+ const isAfterNotBefore =
1161
+ datetimetimestamp >= datetimenotbefore ||
1162
+ datetimenotbefore.getTime() === datetimenul.getTime();
1163
+
1164
+ const isBeforeNotAfter =
1165
+ datetimetimestamp <= datetimenotafter ||
1166
+ datetimenotafter.getTime() === datetimenul.getTime();
1167
+
1168
+ const isLive = isAfterNotBefore && isBeforeNotAfter;
1169
+
1170
+ const totalDuration = Date.now() - startTime;
1171
+
1172
+ if (isLive) {
1173
+ logger.info("RODiT is live", {
1174
+ component: "AuthServices",
1175
+ method: "verify_rodit_islive",
1176
+ requestId,
1177
+ duration: totalDuration,
1178
+ rpcDuration,
1179
+ currentTime: datetimetimestamp.toISOString(),
1180
+ notBefore: datetimenotbefore.toISOString(),
1181
+ notAfter: datetimenotafter.toISOString(),
1182
+ isLive: true,
1183
+ });
1184
+
1185
+ // Add metrics for live tokens
1186
+ logger.metric &&
1187
+ logger.metric("rodit_time_checks", totalDuration, {
1188
+ result: "live",
1189
+ });
1190
+
1191
+ return true;
1192
+ } else {
1193
+ logger.warn("RODiT is not live - outside valid time period", {
1194
+ component: "AuthServices",
1195
+ method: "verify_rodit_islive",
1196
+ requestId,
1197
+ duration: totalDuration,
1198
+ rpcDuration,
1199
+ currentTime: datetimetimestamp.toISOString(),
1200
+ notBefore: datetimenotbefore.toISOString(),
1201
+ notAfter: datetimenotafter.toISOString(),
1202
+ isBeforeExpiry: isBeforeNotAfter,
1203
+ isAfterStart: isAfterNotBefore,
1204
+ isLive: false,
1205
+ });
1206
+
1207
+ // Add metrics for expired or not-yet-valid tokens
1208
+ logger.metric &&
1209
+ logger.metric("rodit_time_checks", totalDuration, {
1210
+ result: "not_live",
1211
+ not_before_valid: isAfterNotBefore,
1212
+ not_after_valid: isBeforeNotAfter,
1213
+ });
1214
+
1215
+ return false;
1216
+ }
1217
+ } catch (error) {
1218
+ const duration = Date.now() - startTime;
1219
+
1220
+ logger.error("Failed to check RODiT time validity", {
1221
+ component: "AuthServices",
1222
+ method: "verify_rodit_islive",
1223
+ requestId,
1224
+ duration,
1225
+ notAfter: peer_rodit_notafter,
1226
+ notBefore: peer_rodit_notbefore,
1227
+ error: {
1228
+ message: error.message,
1229
+ stack: error.stack,
1230
+ name: error.name,
1231
+ },
1232
+ });
1233
+
1234
+ // Add metrics for validation errors
1235
+ logger.metric &&
1236
+ logger.metric("rodit_islive_errors", 1, {
1237
+ error_type: error.name || "Unknown",
1238
+ });
1239
+
1240
+ return false;
1241
+ }
1242
+ }
1243
+
1244
+ async function verify_rodit_isactive(tokenId, ownsubjectuniqueidentifier_url) {
1245
+ const requestId = ulid();
1246
+ const startTime = Date.now();
1247
+
1248
+ // WHILE DEBUGGING TEMPORARY FIX DO NOT REMOVE THIS LINE EVER WITHOUT PERMISSION
1249
+ return true;
1250
+
1251
+ logger.debug("Checking RODiT activity status", {
1252
+ component: "RoditAuth",
1253
+ method: "verify_rodit_isactive",
1254
+ requestId,
1255
+ tokenId,
1256
+ subjectUrl: ownsubjectuniqueidentifier_url,
1257
+ });
1258
+
1259
+ const domainandextensionRegex =
1260
+ /(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/i;
1261
+
1262
+ const match = ownsubjectuniqueidentifier_url.match(domainandextensionRegex);
1263
+
1264
+ if (match) {
1265
+ const domainandextension = match[1];
1266
+ const revokingDnsEntry = `${tokenId}.revoked.${domainandextension}`;
1267
+
1268
+ logger.debug("Checking DNS revocation entry", {
1269
+ requestId,
1270
+ domain: domainandextension,
1271
+ revokingDnsEntry,
1272
+ });
1273
+
1274
+ try {
1275
+ const dnsStart = Date.now();
1276
+ const resolver = new Resolver();
1277
+ await resolver.resolveTxt(revokingDnsEntry);
1278
+ const dnsDuration = Date.now() - dnsStart;
1279
+ const totalDuration = Date.now() - startTime;
1280
+
1281
+ logger.info("RODiT revocation found", {
1282
+ component: "AuthServices",
1283
+ method: "verify_rodit_isactive",
1284
+ requestId,
1285
+ duration: totalDuration,
1286
+ dnsDuration,
1287
+ tokenId,
1288
+ domain: domainandextension,
1289
+ revokingDnsEntry,
1290
+ isActive: false,
1291
+ });
1292
+
1293
+ // Add metrics for revoked tokens
1294
+ logger.metric &&
1295
+ logger.metric("rodit_revocation_checks", totalDuration, {
1296
+ result: "revoked",
1297
+ token_id: tokenId,
1298
+ });
1299
+
1300
+ return false;
1301
+ } catch (error) {
1302
+ // DNS error usually means no revocation entry found, which is good
1303
+ const dnsDuration = Date.now() - dnsStart || 0;
1304
+ const totalDuration = Date.now() - startTime;
1305
+
1306
+ logger.debug("No revocation found for RODiT", {
1307
+ requestId,
1308
+ dnsDuration,
1309
+ tokenId,
1310
+ error: error.code,
1311
+ });
1312
+
1313
+ logger.info("RODiT is active", {
1314
+ component: "AuthServices",
1315
+ method: "verify_rodit_isactive",
1316
+ requestId,
1317
+ duration: totalDuration,
1318
+ dnsDuration,
1319
+ tokenId,
1320
+ domain: domainandextension,
1321
+ isActive: true,
1322
+ });
1323
+
1324
+ // Add metrics for active tokens
1325
+ logger.metric &&
1326
+ logger.metric("rodit_revocation_checks", totalDuration, {
1327
+ result: "active",
1328
+ token_id: tokenId,
1329
+ });
1330
+
1331
+ return true;
1332
+ }
1333
+ } else {
1334
+ const duration = Date.now() - startTime;
1335
+
1336
+ logger.warn("Unable to parse domain from URL", {
1337
+ component: "AuthServices",
1338
+ method: "verify_rodit_isactive",
1339
+ requestId,
1340
+ duration,
1341
+ tokenId,
1342
+ subjectUrl: ownsubjectuniqueidentifier_url,
1343
+ });
1344
+
1345
+ // Add metrics for parsing errors
1346
+ logger.metric &&
1347
+ logger.metric("rodit_revocation_checks", duration, {
1348
+ result: "parse_error",
1349
+ token_id: tokenId,
1350
+ });
1351
+
1352
+ // Default to allowing the token if domain parsing fails
1353
+ return true;
1354
+ }
1355
+ }
1356
+
1357
+ async function verify_rodit_istrusted_issuingsmartcontract(
1358
+ ownsubjectuniqueidentifier_url
1359
+ ) {
1360
+ const requestId = ulid();
1361
+ const startTime = Date.now();
1362
+
1363
+ logger.debug("Verifying smart contract trust", {
1364
+ component: "RoditAuth",
1365
+ method: "verify_rodit_istrusted_issuingsmartcontract",
1366
+ requestId,
1367
+ url: ownsubjectuniqueidentifier_url,
1368
+ smartContract: CONSTANTS.NEAR_CONTRACT_ID,
1369
+ });
1370
+
1371
+ try {
1372
+ const smartcontract = CONSTANTS.NEAR_CONTRACT_ID;
1373
+ // Remove .near suffix
1374
+ const smartontractnonear = smartcontract.replace(/\.near$/, "");
1375
+
1376
+ logger.debug("Prepared smart contract identifiers", {
1377
+ requestId,
1378
+ originalContract: smartcontract,
1379
+ nonearContract: smartontractnonear,
1380
+ });
1381
+
1382
+ const domainRegex =
1383
+ /(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/i;
1384
+
1385
+ const maindomainmatch = domainRegex.exec(ownsubjectuniqueidentifier_url);
1386
+
1387
+ if (!maindomainmatch) {
1388
+ logger.error("Failed to parse domain from URL", {
1389
+ component: "AuthServices",
1390
+ requestId,
1391
+ duration: Date.now() - startTime,
1392
+ url: ownsubjectuniqueidentifier_url,
1393
+ });
1394
+
1395
+ // Add metrics for domain parsing failures
1396
+ logger.metric &&
1397
+ logger.metric("rodit_trust_errors", 1, {
1398
+ error_type: "domain_parse_error",
1399
+ url: ownsubjectuniqueidentifier_url,
1400
+ });
1401
+
1402
+ throw new Error(
1403
+ `Domain can't be parsed from URL: ${ownsubjectuniqueidentifier_url}`
1404
+ );
1405
+ }
1406
+
1407
+ const extractedDomain = maindomainmatch[1];
1408
+ const enablingdnsentry = `${smartontractnonear}.smartcontract.${extractedDomain}`;
1409
+
1410
+ logger.debug("Checking DNS trust entry", {
1411
+ requestId,
1412
+ extractedDomain,
1413
+ enablingDnsEntry: enablingdnsentry,
1414
+ });
1415
+
1416
+ try {
1417
+ const dnsStart = Date.now();
1418
+ const resolver = new Resolver();
1419
+ const cfgresponse = await resolver.resolveTxt(enablingdnsentry);
1420
+ const dnsDuration = Date.now() - dnsStart;
1421
+
1422
+ logger.debug("DNS response received", {
1423
+ requestId,
1424
+ dnsDuration,
1425
+ recordCount: cfgresponse?.length || 0,
1426
+ });
1427
+
1428
+ if (cfgresponse.length > 0) {
1429
+ const totalDuration = Date.now() - startTime;
1430
+
1431
+ logger.info("Smart contract is trusted", {
1432
+ component: "AuthServices",
1433
+ method: "verify_rodit_istrusted_issuingsmartcontract",
1434
+ requestId,
1435
+ duration: totalDuration,
1436
+ dnsDuration,
1437
+ smartContract: smartontractnonear,
1438
+ domain: extractedDomain,
1439
+ dnsEntry: enablingdnsentry,
1440
+ recordCount: cfgresponse.length,
1441
+ isTrusted: true,
1442
+ });
1443
+
1444
+ // Add metrics for trusted contracts
1445
+ logger.metric &&
1446
+ logger.metric("rodit_trust_checks", totalDuration, {
1447
+ result: "trusted",
1448
+ domain: extractedDomain,
1449
+ });
1450
+
1451
+ return true;
1452
+ } else {
1453
+ const totalDuration = Date.now() - startTime;
1454
+
1455
+ logger.warn("Smart contract not trusted - empty DNS record", {
1456
+ component: "AuthServices",
1457
+ method: "verify_rodit_istrusted_issuingsmartcontract",
1458
+ requestId,
1459
+ duration: totalDuration,
1460
+ dnsDuration,
1461
+ smartContract: smartontractnonear,
1462
+ domain: extractedDomain,
1463
+ dnsEntry: enablingdnsentry,
1464
+ isTrusted: false,
1465
+ });
1466
+
1467
+ // Add metrics for untrusted contracts
1468
+ logger.metric &&
1469
+ logger.metric("rodit_trust_checks", totalDuration, {
1470
+ result: "empty_dns",
1471
+ domain: extractedDomain,
1472
+ });
1473
+
1474
+ return false;
1475
+ }
1476
+ } catch (error) {
1477
+ const totalDuration = Date.now() - startTime;
1478
+
1479
+ logger.warn("Smart contract not trusted - DNS lookup failed", {
1480
+ component: "AuthServices",
1481
+ method: "verify_rodit_istrusted_issuingsmartcontract",
1482
+ requestId,
1483
+ duration: totalDuration,
1484
+ smartContract: smartontractnonear,
1485
+ domain: extractedDomain,
1486
+ dnsEntry: enablingdnsentry,
1487
+ dnsError: error.code,
1488
+ isTrusted: false,
1489
+ });
1490
+
1491
+ // Add metrics for DNS errors
1492
+ logger.metric &&
1493
+ logger.metric("rodit_trust_checks", totalDuration, {
1494
+ result: "dns_error",
1495
+ domain: extractedDomain,
1496
+ error_code: error.code,
1497
+ });
1498
+
1499
+ return false;
1500
+ }
1501
+ } catch (error) {
1502
+ const duration = Date.now() - startTime;
1503
+
1504
+ logger.error("Trust verification failed", {
1505
+ component: "AuthServices",
1506
+ method: "verify_rodit_istrusted_issuingsmartcontract",
1507
+ requestId,
1508
+ duration,
1509
+ url: ownsubjectuniqueidentifier_url,
1510
+ error: {
1511
+ message: error.message,
1512
+ stack: error.stack,
1513
+ name: error.name,
1514
+ },
1515
+ });
1516
+
1517
+ // Add metrics for verification errors
1518
+ logger.metric &&
1519
+ logger.metric("rodit_trust_errors", 1, {
1520
+ error_type: error.name || "Unknown",
1521
+ message: error.message,
1522
+ });
1523
+
1524
+ return false;
1525
+ }
1526
+ }
1527
+
1528
+ async function verify_rodit_isamatch(own_service_provider_id, peer_rodit) {
1529
+ const requestId = ulid();
1530
+ const startTime = Date.now();
1531
+
1532
+ // Get login mode configuration
1533
+ const config = require("../../services/configsdk");
1534
+ const loginMode = config.get("SECURITY_OPTIONS.LOGIN_MODE", "partner").toLowerCase();
1535
+
1536
+ // Track the last rejection reason for better error reporting
1537
+ let lastRejectionReason = null;
1538
+ let lastVerificationType = null;
1539
+
1540
+ logger.debug("Starting RODiT match verification", {
1541
+ component: "RoditAuth",
1542
+ method: "verify_rodit_isamatch",
1543
+ requestId,
1544
+ ownServiceProviderId: own_service_provider_id,
1545
+ peerRoditId: peer_rodit?.token_id,
1546
+ loginMode,
1547
+ });
1548
+
1549
+ try {
1550
+ const own_provider_components = own_service_provider_id.split(";");
1551
+
1552
+ logger.debug("Split provider components", {
1553
+ requestId,
1554
+ componentCount: own_provider_components.length,
1555
+ components: own_provider_components,
1556
+ });
1557
+
1558
+ // Get blockchain and contract parts
1559
+ const bcPart = own_provider_components.find((part) =>
1560
+ part.startsWith("bc=")
1561
+ );
1562
+ const scPart = own_provider_components.find((part) =>
1563
+ part.startsWith("sc=")
1564
+ );
1565
+
1566
+ // Find all ID components
1567
+ const idComponents = own_provider_components.filter(
1568
+ (part) =>
1569
+ part.startsWith("id=") &&
1570
+ !part.startsWith("bc=") &&
1571
+ !part.startsWith("sc=")
1572
+ );
1573
+
1574
+ if (!bcPart || !scPart || idComponents.length < 1) {
1575
+ logger.error("Invalid provider ID format", {
1576
+ component: "AuthServices",
1577
+ requestId,
1578
+ duration: Date.now() - startTime,
1579
+ providerId: own_service_provider_id,
1580
+ components: own_provider_components,
1581
+ hasBlockchain: !!bcPart,
1582
+ hasSmartContract: !!scPart,
1583
+ idCount: idComponents.length,
1584
+ });
1585
+
1586
+ // Add metrics for format errors
1587
+ logger.metric &&
1588
+ logger.metric("rodit_match_format_errors", 1, {
1589
+ error_type: "invalid_provider_id",
1590
+ bc_part_present: !!bcPart,
1591
+ sc_part_present: !!scPart,
1592
+ id_count: idComponents.length,
1593
+ });
1594
+
1595
+ return false;
1596
+ }
1597
+
1598
+ // Construct the base prefix
1599
+ const base_prefix = `${bcPart};${scPart}`;
1600
+ logger.debug("Constructed base prefix", {
1601
+ requestId,
1602
+ basePrefix: base_prefix,
1603
+ });
1604
+
1605
+ // Extract the peer's service provider IDs for comparison
1606
+ const peer_service_provider_id = peer_rodit.metadata.serviceprovider_id;
1607
+ const peer_provider_components = peer_service_provider_id.split(";");
1608
+ const peer_idComponents = peer_provider_components.filter(
1609
+ (part) => part.startsWith("id=") && !part.startsWith("bc=") && !part.startsWith("sc=")
1610
+ );
1611
+
1612
+ logger.debug("Peer service provider analysis", {
1613
+ requestId,
1614
+ peerServiceProviderId: peer_service_provider_id,
1615
+ peerIdComponents: peer_idComponents,
1616
+ ownIdComponents: idComponents,
1617
+ });
1618
+
1619
+ // Try verification with each ID component
1620
+ for (let i = 0; i < idComponents.length; i++) {
1621
+ const idPosition = i + 1;
1622
+ const signing_token_id = `${base_prefix};${idComponents[i]}`;
1623
+ const current_own_id = idComponents[i];
1624
+
1625
+ // Determine verification type based on service provider ID comparison
1626
+ // PARTNER: Different service provider IDs (client-server relationship)
1627
+ // PEER: Same service provider ID (peer-to-peer relationship)
1628
+ const isSignedBySameProvider = peer_idComponents.includes(current_own_id);
1629
+ const verificationType = isSignedBySameProvider ? "PARTNER":"PEER";
1630
+ const isPartnerVerification = !isSignedBySameProvider;
1631
+ const isPeerVerification = isSignedBySameProvider;
1632
+
1633
+ logger.debug(
1634
+ `Trying ${verificationType} verification with ID [${idPosition}/${idComponents.length}]`,
1635
+ {
1636
+ requestId,
1637
+ idPosition,
1638
+ verificationType,
1639
+ totalIds: idComponents.length,
1640
+ signingTokenId: signing_token_id,
1641
+ currentOwnId: current_own_id,
1642
+ peerIdComponents: peer_idComponents,
1643
+ isSignedBySameProvider,
1644
+ relationshipType: isSignedBySameProvider ? "peer-to-peer" : "client-to-server"
1645
+ }
1646
+ );
1647
+
1648
+ logger.debug("About to fetch signing RODiT", {
1649
+ requestId,
1650
+ idPosition,
1651
+ verificationType,
1652
+ signingTokenId: signing_token_id,
1653
+ expectedAccount: current_own_id.replace('id=', ''),
1654
+ peerServiceProviderId: peer_service_provider_id
1655
+ });
1656
+
1657
+ const tokenFetchStart = Date.now();
1658
+ const signing_rodit = await nearorg_rpc_tokenfromroditid(
1659
+ signing_token_id
1660
+ );
1661
+ const tokenFetchDuration = Date.now() - tokenFetchStart;
1662
+
1663
+ logger.debug("Retrieved signing RODiT - DETAILED DEBUG", {
1664
+ requestId,
1665
+ idPosition,
1666
+ verificationType,
1667
+ tokenFetchDuration,
1668
+ tokenId: signing_rodit?.token_id,
1669
+ ownerId: signing_rodit?.owner_id,
1670
+ ownerIdExpected: current_own_id.replace('id=', ''),
1671
+ ownerIdMatches: signing_rodit?.owner_id === current_own_id.replace('id=', ''),
1672
+ signingTokenIdRequested: signing_token_id,
1673
+ hasMetadata: !!signing_rodit?.metadata,
1674
+ metadataServiceProviderId: signing_rodit?.metadata?.serviceprovider_id
1675
+ });
1676
+
1677
+ // Process the owner ID
1678
+ try {
1679
+ // Add detailed logging for debugging
1680
+ logger.debug("Processing owner ID for signing verification", {
1681
+ requestId,
1682
+ idPosition,
1683
+ verificationType,
1684
+ ownerIdType: typeof signing_rodit.owner_id,
1685
+ ownerIdValue: signing_rodit.owner_id,
1686
+ ownerIdLength: signing_rodit.owner_id?.length,
1687
+ isValidHex: signing_rodit.owner_id && /^[0-9a-fA-F]+$/.test(signing_rodit.owner_id),
1688
+ peerSignature: peer_rodit.metadata.serviceprovider_signature,
1689
+ });
1690
+
1691
+ const bytes_signing_owner_id = new Uint8Array(
1692
+ Buffer.from(signing_rodit.owner_id, "hex")
1693
+ );
1694
+
1695
+ logger.debug("Hex conversion result", {
1696
+ requestId,
1697
+ idPosition,
1698
+ verificationType,
1699
+ resultLength: bytes_signing_owner_id.length,
1700
+ expectedLength: CONSTANTS.RODIT_ID_PK_SZ,
1701
+ bufferFirst4Bytes: Array.from(bytes_signing_owner_id.slice(0, 4)),
1702
+ });
1703
+
1704
+ if (bytes_signing_owner_id.length !== CONSTANTS.RODIT_ID_PK_SZ) {
1705
+ logger.warn(`Invalid signing key length for ${verificationType} verification (ID position: ${idPosition})`, {
1706
+ requestId,
1707
+ verificationType,
1708
+ actual: bytes_signing_owner_id.length,
1709
+ expected: CONSTANTS.RODIT_ID_PK_SZ,
1710
+ ownerIdValue: signing_rodit.owner_id,
1711
+ ownerIdType: typeof signing_rodit.owner_id,
1712
+ });
1713
+ continue; // Try the next ID
1714
+ }
1715
+
1716
+ // Process the signature
1717
+ const base64urlSignature =
1718
+ peer_rodit.metadata.serviceprovider_signature;
1719
+ const base64Signature = base64urlSignature
1720
+ .replace(/-/g, "+")
1721
+ .replace(/_/g, "/")
1722
+ .padEnd(
1723
+ base64urlSignature.length +
1724
+ ((4 - (base64urlSignature.length % 4)) % 4),
1725
+ "="
1726
+ );
1727
+
1728
+ const signatureBytes = new Uint8Array(
1729
+ Buffer.from(base64Signature, "base64")
1730
+ );
1731
+
1732
+ if (signatureBytes.length !== CONSTANTS.RODIT_ID_SIGNATURE_SZ) {
1733
+ logger.warn(`Invalid signature length for ${verificationType} verification (ID position: ${idPosition})`, {
1734
+ requestId,
1735
+ verificationType,
1736
+ actual: signatureBytes.length,
1737
+ expected: CONSTANTS.RODIT_ID_SIGNATURE_SZ,
1738
+ });
1739
+ continue; // Try the next ID
1740
+ }
1741
+
1742
+ // Prepare the hash input - MUST exactly match verifyRoditSignature function format
1743
+ const hashInput = {
1744
+ token_id: peer_rodit.token_id,
1745
+ openapijson_url: peer_rodit.metadata.openapijson_url,
1746
+ not_after: peer_rodit.metadata.not_after,
1747
+ not_before: peer_rodit.metadata.not_before,
1748
+ max_requests: String(peer_rodit.metadata.max_requests),
1749
+ maxrq_window: String(peer_rodit.metadata.maxrq_window),
1750
+ webhook_cidr: peer_rodit.metadata.webhook_cidr,
1751
+ allowed_cidr: peer_rodit.metadata.allowed_cidr,
1752
+ allowed_iso3166list: peer_rodit.metadata.allowed_iso3166list,
1753
+ jwt_duration: peer_rodit.metadata.jwt_duration,
1754
+ permissioned_routes: peer_rodit.metadata.permissioned_routes,
1755
+ serviceprovider_id: peer_rodit.metadata.serviceprovider_id,
1756
+ subjectuniqueidentifier_url: peer_rodit.metadata.subjectuniqueidentifier_url,
1757
+ };
1758
+
1759
+ // Debug logging to compare with working frontend verification
1760
+ logger.debug("Backend hash input structure for verification", {
1761
+ requestId,
1762
+ idPosition,
1763
+ verificationType,
1764
+ hashInput: JSON.stringify(hashInput, null, 2),
1765
+ peerSignature: peer_rodit.metadata.serviceprovider_signature,
1766
+ signingOwnerId: bytes_signing_owner_id ? Buffer.from(bytes_signing_owner_id).toString('hex') : 'null',
1767
+ });
1768
+
1769
+ const hashStart = Date.now();
1770
+ const hashHex = calculateCanonicalHash(hashInput);
1771
+ const hashBytes = new Uint8Array(Buffer.from(hashHex, "hex"));
1772
+ const hashDuration = Date.now() - hashStart;
1773
+
1774
+ logger.debug("Hash calculation completed", {
1775
+ requestId,
1776
+ idPosition,
1777
+ verificationType,
1778
+ hashHex: hashHex.substring(0, 32) + '...',
1779
+ hashLength: hashHex.length,
1780
+ hashDuration,
1781
+ });
1782
+
1783
+ logger.debug("Calculated hash for verification", {
1784
+ requestId,
1785
+ idPosition,
1786
+ verificationType,
1787
+ hashDuration,
1788
+ hashLength: hashBytes.length,
1789
+ });
1790
+
1791
+ // Verify the signature
1792
+ const verifyStart = Date.now();
1793
+ const is_valid = nacl.sign.detached.verify(
1794
+ hashBytes,
1795
+ signatureBytes,
1796
+ bytes_signing_owner_id
1797
+ );
1798
+ const verifyDuration = Date.now() - verifyStart;
1799
+
1800
+ logger.debug("Signature verification result", {
1801
+ requestId,
1802
+ idPosition,
1803
+ verificationType,
1804
+ verifyDuration,
1805
+ isValid: is_valid,
1806
+ });
1807
+
1808
+ if (is_valid) {
1809
+ const totalDuration = Date.now() - startTime;
1810
+
1811
+ // Enforce login mode policy
1812
+ const shouldAccept = (
1813
+ loginMode === "promiscuous" || // Accept all
1814
+ (loginMode === "partner" && verificationType === "PARTNER") || // Accept only Partner
1815
+ (loginMode === "p2p" && verificationType === "PEER") // Accept only Peer
1816
+ );
1817
+
1818
+ if (!shouldAccept) {
1819
+ logger.warn(`${verificationType} login rejected by LOGIN_MODE policy`, {
1820
+ component: "AuthServices",
1821
+ method: "verify_rodit_isamatch",
1822
+ requestId,
1823
+ duration: totalDuration,
1824
+ verificationType,
1825
+ loginMode,
1826
+ idPosition,
1827
+ policyReason: `LOGIN_MODE=${loginMode} does not accept ${verificationType} logins`
1828
+ });
1829
+
1830
+ // Add metrics for policy rejection
1831
+ logger.metric &&
1832
+ logger.metric("rodit_match_verification", totalDuration, {
1833
+ result: "policy_rejected",
1834
+ verification_type: verificationType.toLowerCase(),
1835
+ login_mode: loginMode,
1836
+ });
1837
+
1838
+ // Track this rejection for error reporting
1839
+ lastRejectionReason = "LOGIN_MODE_POLICY_REJECTED";
1840
+ lastVerificationType = verificationType;
1841
+
1842
+ // Continue to try next ID component
1843
+ continue;
1844
+ }
1845
+
1846
+ // Log based on verification type
1847
+ logger.info(`${verificationType} login verified successfully`, {
1848
+ component: "AuthServices",
1849
+ method: "verify_rodit_isamatch",
1850
+ requestId,
1851
+ duration: totalDuration,
1852
+ verificationType,
1853
+ loginMode,
1854
+ idPosition,
1855
+ partnerVerification: isPartnerVerification,
1856
+ peerVerification: isPeerVerification
1857
+ });
1858
+
1859
+ // Add metrics for successful matching
1860
+ logger.metric &&
1861
+ logger.metric("rodit_match_verification", totalDuration, {
1862
+ result: "success",
1863
+ verification_type: verificationType.toLowerCase(),
1864
+ login_mode: loginMode,
1865
+ });
1866
+
1867
+ return {
1868
+ isMatch: true,
1869
+ verificationType,
1870
+ loginMode
1871
+ };
1872
+ }
1873
+
1874
+ logger.debug(`${verificationType} verification failed (ID position: ${idPosition})`, {
1875
+ requestId,
1876
+ verificationType
1877
+ });
1878
+ } catch (verifyError) {
1879
+ logger.warn(`Error during ${verificationType} verification (ID position: ${idPosition})`, {
1880
+ requestId,
1881
+ verificationType,
1882
+ error: verifyError.message,
1883
+ stack: verifyError.stack,
1884
+ });
1885
+ }
1886
+ }
1887
+
1888
+ // If we get here, all verification attempts failed
1889
+ const totalDuration = Date.now() - startTime;
1890
+
1891
+ // Determine the failure reason and message
1892
+ let failureReason, failureMessage;
1893
+
1894
+ if (lastRejectionReason === "LOGIN_MODE_POLICY_REJECTED") {
1895
+ // Include both the policy and what was rejected in the error code
1896
+ failureReason = `LOGIN_MODE_POLICY_REJECTED_${lastVerificationType}_BY_${loginMode.toUpperCase()}`;
1897
+ failureMessage = `${lastVerificationType} login rejected: LOGIN_MODE=${loginMode} does not accept ${lastVerificationType} logins`;
1898
+ } else {
1899
+ failureReason = "RODIT_FAMILY_MISMATCH";
1900
+ failureMessage = "RODiT does not belong to the same family as the server";
1901
+ }
1902
+
1903
+ logger.error("All verification attempts failed", {
1904
+ component: "AuthServices",
1905
+ method: "verify_rodit_isamatch",
1906
+ requestId,
1907
+ duration: totalDuration,
1908
+ ownServiceProviderId: own_service_provider_id,
1909
+ peerRoditId: peer_rodit?.token_id,
1910
+ attemptCount: idComponents.length,
1911
+ failureReason,
1912
+ lastRejectionReason,
1913
+ lastVerificationType,
1914
+ });
1915
+
1916
+ // Add metrics for failed matching
1917
+ logger.metric &&
1918
+ logger.metric("rodit_match_verification", totalDuration, {
1919
+ result: "failure",
1920
+ attempts: idComponents.length,
1921
+ failure_reason: failureReason,
1922
+ });
1923
+
1924
+ return {
1925
+ isMatch: false,
1926
+ failureReason,
1927
+ failureMessage
1928
+ };
1929
+ } catch (error) {
1930
+ const duration = Date.now() - startTime;
1931
+
1932
+ logger.error("RODiT match verification error", {
1933
+ component: "AuthServices",
1934
+ method: "verify_rodit_isamatch",
1935
+ requestId,
1936
+ duration,
1937
+ ownServiceProviderId: own_service_provider_id,
1938
+ peerRoditId: peer_rodit?.token_id,
1939
+ error: {
1940
+ message: error.message,
1941
+ stack: error.stack,
1942
+ name: error.name,
1943
+ },
1944
+ });
1945
+
1946
+ // Add metrics for verification errors
1947
+ logger.metric &&
1948
+ logger.metric("rodit_match_errors", 1, {
1949
+ error_type: error.name || "Unknown",
1950
+ });
1951
+
1952
+ return {
1953
+ isMatch: false,
1954
+ failureReason: "VERIFICATION_ERROR",
1955
+ failureMessage: `RODiT match verification error: ${error.message}`
1956
+ };
1957
+ }
1958
+ }
1959
+
1960
+ module.exports = {
1961
+ verify_rodit_ownership,
1962
+ verify_rodit_ownership_withnep413,
1963
+ resolve_peer_rodit_for_login,
1964
+ verify_peer_rodit,
1965
+ validateTimestamp,
1966
+ verify_rodit_isactive,
1967
+ verify_rodit_isamatch,
1968
+ verify_rodit_islive,
1969
+ verify_rodit_istrusted_issuingsmartcontract,
1970
+ authenticate_webhook
1971
+ };