@rodit/rodit-auth-be 9.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
|
@@ -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
|
+
};
|