@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-based credential storage system
|
|
3
|
+
* Mirrors filecredentialstoremw.js but reads credentials JSON from an environment variable
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { ulid } = require("ulid");
|
|
9
|
+
const config = require("../../services/configsdk");
|
|
10
|
+
const logger = require("../../services/logger");
|
|
11
|
+
const { createLogContext, logErrorWithMetrics } = logger;
|
|
12
|
+
const { validateAndExtractCredentials } = require("../../services/utils");
|
|
13
|
+
|
|
14
|
+
class EnvManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.envVarName = "NEAR_CREDENTIALS_JSON_B64";
|
|
17
|
+
this.initialized = false;
|
|
18
|
+
this.credentials = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async initialize(source = {}) {
|
|
22
|
+
const context = createLogContext("EnvCredentialStore", "initialize", {
|
|
23
|
+
requestId: ulid(),
|
|
24
|
+
hasSourceValue: source && (typeof source.envCredentialsJson !== "undefined"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
logger.debugWithContext("Initializing env credential store", context);
|
|
28
|
+
|
|
29
|
+
if (this.initialized) {
|
|
30
|
+
logger.debugWithContext("Env credential store already initialized", context);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Prefer explicit source override, then config/env
|
|
36
|
+
let rawValue =
|
|
37
|
+
typeof source.envCredentialsJson !== "undefined"
|
|
38
|
+
? source.envCredentialsJson
|
|
39
|
+
: config.get(this.envVarName, process.env[this.envVarName] || null);
|
|
40
|
+
|
|
41
|
+
if (rawValue == null || (typeof rawValue === "string" && rawValue.trim() === "")) {
|
|
42
|
+
// Keep empty object credentials; downstream may handle missing credentials
|
|
43
|
+
logger.infoWithContext("Credentials env var is empty or not set", {
|
|
44
|
+
...context,
|
|
45
|
+
envVarName: this.envVarName,
|
|
46
|
+
});
|
|
47
|
+
this.credentials = {};
|
|
48
|
+
this.initialized = true;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let parsed;
|
|
53
|
+
if (typeof rawValue === "string") {
|
|
54
|
+
// Primary: base64-encoded JSON
|
|
55
|
+
const decoded = Buffer.from(rawValue, "base64").toString("utf8");
|
|
56
|
+
parsed = JSON.parse(decoded);
|
|
57
|
+
} else {
|
|
58
|
+
parsed = rawValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const validated = validateAndExtractCredentials(parsed, logger);
|
|
62
|
+
this.credentials = validated || {};
|
|
63
|
+
this.initialized = true;
|
|
64
|
+
|
|
65
|
+
logger.debugWithContext("Env credential store initialized successfully", {
|
|
66
|
+
...context,
|
|
67
|
+
hasCredentials: !!this.credentials && Object.keys(this.credentials).length > 0,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return this;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logErrorWithMetrics(
|
|
73
|
+
"Failed to initialize env credential store",
|
|
74
|
+
{
|
|
75
|
+
...context,
|
|
76
|
+
error: error.message,
|
|
77
|
+
stack: error.stack,
|
|
78
|
+
envVarName: this.envVarName,
|
|
79
|
+
},
|
|
80
|
+
error,
|
|
81
|
+
error.name === "SyntaxError" ? "env_parse_error" : "env_credential_init_error",
|
|
82
|
+
{ error_type: error.name === "SyntaxError" ? "parse_failure" : "init_failure" }
|
|
83
|
+
);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getCredentials(_source) {
|
|
89
|
+
const context = createLogContext("EnvCredentialStore", "getCredentials", {
|
|
90
|
+
requestId: ulid(),
|
|
91
|
+
envVarName: this.envVarName,
|
|
92
|
+
});
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Use cached if available
|
|
97
|
+
if (this.credentials && Object.keys(this.credentials).length > 0) {
|
|
98
|
+
logger.debugWithContext("Using cached env credentials", context);
|
|
99
|
+
return this.credentials;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fallback to reading again if not cached yet
|
|
103
|
+
let rawValue = config.get(this.envVarName, process.env[this.envVarName] || null);
|
|
104
|
+
if (rawValue == null || (typeof rawValue === "string" && rawValue.trim() === "")) {
|
|
105
|
+
logger.infoWithContext("No credentials found in environment", context);
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let parsed;
|
|
110
|
+
if (typeof rawValue === "string") {
|
|
111
|
+
try {
|
|
112
|
+
const decoded = Buffer.from(rawValue, "base64").toString("utf8");
|
|
113
|
+
parsed = JSON.parse(decoded);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
logger.infoWithContext("Falling back to raw JSON parsing for credentials env var", {
|
|
116
|
+
...context,
|
|
117
|
+
envVarName: this.envVarName,
|
|
118
|
+
reason: "b64_decode_or_parse_failed",
|
|
119
|
+
});
|
|
120
|
+
parsed = JSON.parse(rawValue);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
parsed = rawValue;
|
|
124
|
+
}
|
|
125
|
+
const result = validateAndExtractCredentials(parsed, logger);
|
|
126
|
+
|
|
127
|
+
logger.debugWithContext("Successfully processed env credentials", {
|
|
128
|
+
...context,
|
|
129
|
+
hasRequiredFields: true,
|
|
130
|
+
duration: Date.now() - startTime,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Cache for future calls
|
|
134
|
+
this.credentials = result || {};
|
|
135
|
+
return result;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logErrorWithMetrics(
|
|
138
|
+
"Error retrieving credentials from environment",
|
|
139
|
+
{
|
|
140
|
+
...context,
|
|
141
|
+
duration: Date.now() - startTime,
|
|
142
|
+
errorDetails: error.message,
|
|
143
|
+
errorType: error.name,
|
|
144
|
+
},
|
|
145
|
+
error,
|
|
146
|
+
error.name === "SyntaxError" ? "env_parse_error" : "env_credential_retrieval_error",
|
|
147
|
+
{ error_type: error.name === "SyntaxError" ? "parse_failure" : "retrieval_failure" }
|
|
148
|
+
);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// No-op to maintain interface compatibility with vaultcredentialstoremw.js
|
|
154
|
+
async setupSecretStorageTokenRenewal() {
|
|
155
|
+
const context = createLogContext("EnvCredentialStore", "setupSecretStorageTokenRenewal", {
|
|
156
|
+
requestId: ulid(),
|
|
157
|
+
});
|
|
158
|
+
logger.debugWithContext(
|
|
159
|
+
"Skipping secret storage token renewal setup (not applicable for env-based credentials)",
|
|
160
|
+
context
|
|
161
|
+
);
|
|
162
|
+
return Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create and export a singleton instance
|
|
167
|
+
const envManager = new EnvManager();
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
initializeCredentialStore: (source) => envManager.initialize(source),
|
|
171
|
+
setupSecretStorageTokenRenewal: () => envManager.setupSecretStorageTokenRenewal(),
|
|
172
|
+
getCredentials: (source) => envManager.getCredentials(source),
|
|
173
|
+
vault: null,
|
|
174
|
+
// For testing purposes
|
|
175
|
+
_envManager: envManager,
|
|
176
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based credential storage system
|
|
3
|
+
* Alternative to Vault for storing RODiT credentials
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const { ulid } = require("ulid");
|
|
10
|
+
const config = require('../../services/configsdk');
|
|
11
|
+
const logger = require("../../services/logger");
|
|
12
|
+
const { createLogContext, logErrorWithMetrics } = logger;
|
|
13
|
+
const { validateAndExtractCredentials } = require("../../services/utils");
|
|
14
|
+
class FileManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.credentialsFilePath = config.get("NEAR_CREDENTIALS_FILE_PATH");
|
|
17
|
+
this.initialized = false;
|
|
18
|
+
this.credentials = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async initialize(source = {}) {
|
|
22
|
+
const context = createLogContext("FileCredentialStore", "initialize", {
|
|
23
|
+
requestId: ulid(),
|
|
24
|
+
hasConfigPath: !!source.credentialsFilePath
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
logger.debugWithContext("Initializing file credential store", context);
|
|
28
|
+
|
|
29
|
+
if (this.initialized) {
|
|
30
|
+
logger.debugWithContext("File credential store already initialized", context);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
this.credentialsFilePath = source.credentialsFilePath || config.get('NEAR_CREDENTIALS_FILE_PATH');
|
|
36
|
+
if (!this.credentialsFilePath) {
|
|
37
|
+
throw new Error('NEAR_CREDENTIALS_FILE_PATH is not set in config or source');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ensure the directory exists
|
|
41
|
+
try {
|
|
42
|
+
await fs.mkdir(require('path').dirname(this.credentialsFilePath), { recursive: true });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code !== 'EEXIST') throw err;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const credentials = await this.getCredentials();
|
|
48
|
+
this.credentials = credentials || {}; // Ensure we always have an object
|
|
49
|
+
this.initialized = true;
|
|
50
|
+
|
|
51
|
+
logger.debugWithContext("File credential store initialized successfully", {
|
|
52
|
+
...context,
|
|
53
|
+
credentialsFilePath: this.credentialsFilePath,
|
|
54
|
+
credentialCount: this.credentials ? Object.keys(this.credentials).length : 0
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return this;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.errorWithContext("Failed to initialize file credential store", {
|
|
60
|
+
...context,
|
|
61
|
+
error: error.message,
|
|
62
|
+
stack: error.stack
|
|
63
|
+
});
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async checkReadFileAccess(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
const stats = await fs.stat(filePath);
|
|
71
|
+
await fs.access(filePath, fs.constants.R_OK);
|
|
72
|
+
return { exists: true, isReadable: true, stats };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code === 'ENOENT') {
|
|
75
|
+
return { exists: false, isReadable: false };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
exists: false,
|
|
79
|
+
isReadable: false,
|
|
80
|
+
error: error.message,
|
|
81
|
+
code: error.code
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getCredentials(source) {
|
|
87
|
+
const context = createLogContext("FileCredentialStore", "getCredentials", {
|
|
88
|
+
requestId: ulid(),
|
|
89
|
+
source: source || 'all'
|
|
90
|
+
});
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
let result = {};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Check file access and read content
|
|
96
|
+
const { exists } = await this.checkReadFileAccess(this.credentialsFilePath);
|
|
97
|
+
if (!exists) {
|
|
98
|
+
logger.infoWithContext("Credentials file does not exist", { ...context, credentialsFilePath: this.credentialsFilePath });
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Read and parse file content
|
|
103
|
+
const fileContent = await fs.readFile(this.credentialsFilePath, 'utf8');
|
|
104
|
+
if (!fileContent.trim()) {
|
|
105
|
+
logger.infoWithContext("Credentials file is empty", { ...context, credentialsFilePath: this.credentialsFilePath });
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse and validate credentials using the utility function
|
|
110
|
+
result = validateAndExtractCredentials(JSON.parse(fileContent), logger);
|
|
111
|
+
|
|
112
|
+
logger.debugWithContext("Successfully processed credentials", {
|
|
113
|
+
...context,
|
|
114
|
+
hasRequiredFields: true,
|
|
115
|
+
duration: Date.now() - startTime
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logErrorWithMetrics(
|
|
121
|
+
"Error retrieving credentials from file",
|
|
122
|
+
{
|
|
123
|
+
...context,
|
|
124
|
+
duration: Date.now() - startTime,
|
|
125
|
+
errorDetails: error.message,
|
|
126
|
+
errorType: error.name,
|
|
127
|
+
credentialsFilePath: this.credentialsFilePath,
|
|
128
|
+
stack: error.stack
|
|
129
|
+
},
|
|
130
|
+
error,
|
|
131
|
+
error.name === 'SyntaxError' ? "file_parse_error" : "file_credential_retrieval_error",
|
|
132
|
+
{ error_type: error.name === 'SyntaxError' ? "parse_failure" : "retrieval_failure" }
|
|
133
|
+
);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// No-op to maintain interface compatibility with vaultcredentialstoremw.js
|
|
139
|
+
async setupSecretStorageTokenRenewal() {
|
|
140
|
+
const context = createLogContext("FileCredentialStore", "setupSecretStorageTokenRenewal", {
|
|
141
|
+
requestId: ulid()
|
|
142
|
+
});
|
|
143
|
+
logger.debugWithContext("Skipping secret storage token renewal setup (not applicable for file-based credentials)", context);
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Create and export a singleton instance
|
|
149
|
+
const fileManager = new FileManager();
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
initializeCredentialStore: (source) => fileManager.initialize(source),
|
|
153
|
+
setupSecretStorageTokenRenewal: () => fileManager.setupSecretStorageTokenRenewal(),
|
|
154
|
+
getCredentials: (source) => fileManager.getCredentials(source),
|
|
155
|
+
vault: null,
|
|
156
|
+
// For testing purposes
|
|
157
|
+
_fileManager: fileManager
|
|
158
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request logging middleware
|
|
3
|
+
* Provides standardized request/response logging
|
|
4
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const logger = require("../../services/logger");
|
|
8
|
+
const config = require('../../services/configsdk');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Middleware for logging requests and responses
|
|
12
|
+
* This middleware should be applied after the performance middleware
|
|
13
|
+
* to leverage the request ID and timing information.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} req - Express request object
|
|
16
|
+
* @param {Object} res - Express response object
|
|
17
|
+
* @param {Function} next - Next middleware function
|
|
18
|
+
*/
|
|
19
|
+
const loggingmw = (req, res, next) => {
|
|
20
|
+
// Ensure we have a request ID (should be set by performance middleware)
|
|
21
|
+
if (!req.requestId) {
|
|
22
|
+
const { ulid } = require("ulid");
|
|
23
|
+
req.requestId = ulid();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Capture the original end function
|
|
27
|
+
const originalEnd = res.end;
|
|
28
|
+
|
|
29
|
+
// Override the end function
|
|
30
|
+
res.end = function (chunk, encoding) {
|
|
31
|
+
// Call the original end function
|
|
32
|
+
originalEnd.call(this, chunk, encoding);
|
|
33
|
+
|
|
34
|
+
// Now log after the response has been sent
|
|
35
|
+
// Use duration from performance middleware if available
|
|
36
|
+
const duration = req.duration || (req.startTime ? Date.now() - req.startTime : null);
|
|
37
|
+
|
|
38
|
+
// Standard request result log entry
|
|
39
|
+
const result = (res.statusCode >= 200 && res.statusCode < 300) ? 'success' : 'failure';
|
|
40
|
+
const reason = res.statusMessage || (result === 'success' ? 'Request completed successfully' : 'Request failed');
|
|
41
|
+
logger.infoWithContext("Request completed", {
|
|
42
|
+
component: "API",
|
|
43
|
+
method: req.method,
|
|
44
|
+
url: req.originalUrl,
|
|
45
|
+
status: res.statusCode,
|
|
46
|
+
requestId: req.requestId,
|
|
47
|
+
userId: req.user ? req.user.id : undefined,
|
|
48
|
+
authenticated: !!req.user,
|
|
49
|
+
clientIP: req.ip,
|
|
50
|
+
duration,
|
|
51
|
+
result,
|
|
52
|
+
reason
|
|
53
|
+
});
|
|
54
|
+
// Emit a metric for request completion
|
|
55
|
+
logger.metric("api_request_duration_ms", duration, {
|
|
56
|
+
component: "API",
|
|
57
|
+
method: req.method,
|
|
58
|
+
url: req.originalUrl,
|
|
59
|
+
status: res.statusCode,
|
|
60
|
+
requestId: req.requestId,
|
|
61
|
+
userId: req.user ? req.user.id : undefined,
|
|
62
|
+
result,
|
|
63
|
+
reason
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Log the incoming request (function call)
|
|
68
|
+
logger.info("Request received", {
|
|
69
|
+
component: "API",
|
|
70
|
+
method: req.method,
|
|
71
|
+
url: req.originalUrl,
|
|
72
|
+
clientIP: req.ip,
|
|
73
|
+
requestId: req.requestId,
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
result: 'call',
|
|
76
|
+
reason: 'Request received'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
next();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
module.exports = loggingmw;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the performance middleware
|
|
3
|
+
* This file demonstrates how to configure the middleware for your specific API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const performanceMw = require('./performancemw');
|
|
7
|
+
|
|
8
|
+
// Example 1: Use with default classifier (generic patterns)
|
|
9
|
+
// app.use(performanceMw());
|
|
10
|
+
|
|
11
|
+
// Example 2: Custom classifier for your API structure
|
|
12
|
+
const customClassifier = (req) => {
|
|
13
|
+
const path = req.path || req.originalUrl;
|
|
14
|
+
|
|
15
|
+
// Match your specific API routes
|
|
16
|
+
if (path.startsWith('/api/auth') || path.startsWith('/login') || path.startsWith('/token')) {
|
|
17
|
+
return 'authentication';
|
|
18
|
+
} else if (path.startsWith('/api/blockchain') || path.startsWith('/smart-contract')) {
|
|
19
|
+
return 'blockchain';
|
|
20
|
+
} else if (path.startsWith('/api/rodit')) {
|
|
21
|
+
return 'rodit';
|
|
22
|
+
} else if (path.startsWith('/api/identity')) {
|
|
23
|
+
return 'identity';
|
|
24
|
+
} else if (path.startsWith('/api/user') || path.startsWith('/profile')) {
|
|
25
|
+
return 'user';
|
|
26
|
+
} else if (path.startsWith('/api/mcp')) {
|
|
27
|
+
return 'mcp';
|
|
28
|
+
} else if (path === '/health' || path === '/status') {
|
|
29
|
+
return 'system';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return 'general';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Example 3: Configure with custom classifier and type-specific metrics
|
|
36
|
+
const performanceMiddleware = performanceMw({
|
|
37
|
+
classifier: customClassifier,
|
|
38
|
+
metricsByType: {
|
|
39
|
+
'authentication': 'authentication_duration_ms',
|
|
40
|
+
'blockchain': 'blockchain_duration_ms',
|
|
41
|
+
'rodit': 'rodit_operation_duration_ms',
|
|
42
|
+
'identity': 'identity_operation_duration_ms',
|
|
43
|
+
'mcp': 'mcp_operation_duration_ms'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// app.use(performanceMiddleware);
|
|
48
|
+
|
|
49
|
+
// Example 4: Using the exported default classifier
|
|
50
|
+
// const { defaultClassifier } = require('./performancemw');
|
|
51
|
+
// const myClassifier = (req) => {
|
|
52
|
+
// // Add custom logic first
|
|
53
|
+
// if (req.path.startsWith('/api/custom')) return 'custom';
|
|
54
|
+
// // Fall back to default
|
|
55
|
+
// return defaultClassifier(req);
|
|
56
|
+
// };
|
|
57
|
+
|
|
58
|
+
module.exports = performanceMiddleware;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance monitoring middleware
|
|
3
|
+
* Provides request tracing and performance metrics collection
|
|
4
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { ulid } = require("ulid");
|
|
8
|
+
const logger = require("../../services/logger");
|
|
9
|
+
const performanceService = require('../../services/performanceservice');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default request classifier - can be overridden by consumer
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} req - Express request object
|
|
15
|
+
* @returns {string} Request classification
|
|
16
|
+
*/
|
|
17
|
+
function defaultClassifier(req) {
|
|
18
|
+
const path = req.path || req.originalUrl;
|
|
19
|
+
|
|
20
|
+
// Generic classification based on common patterns
|
|
21
|
+
if (path.startsWith('/auth') || path.includes('/login') || path.includes('/token')) {
|
|
22
|
+
return 'authentication';
|
|
23
|
+
} else if (path.includes('/health') || path.includes('/status') || path.includes('/metrics')) {
|
|
24
|
+
return 'system';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'general';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Middleware factory for monitoring request performance
|
|
32
|
+
* This middleware should be applied before the logging middleware
|
|
33
|
+
* to ensure request IDs and timing are properly set up.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} options - Configuration options
|
|
36
|
+
* @param {Function} options.classifier - Custom function to classify requests (optional)
|
|
37
|
+
* @param {Object} options.metricsByType - Map of request types to metric names (optional)
|
|
38
|
+
* @returns {Function} Express middleware function
|
|
39
|
+
*/
|
|
40
|
+
const performanceMw = (options = {}) => {
|
|
41
|
+
const {
|
|
42
|
+
classifier = defaultClassifier,
|
|
43
|
+
metricsByType = {}
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
return (req, res, next) => {
|
|
47
|
+
// Generate request ID if not already present and make it available for other middleware
|
|
48
|
+
req.requestId = req.requestId || ulid();
|
|
49
|
+
|
|
50
|
+
// Record the start time and make it available for other middleware
|
|
51
|
+
req.startTime = Date.now();
|
|
52
|
+
|
|
53
|
+
// Log function call for performance monitoring
|
|
54
|
+
logger.infoWithContext("Performance middleware engaged", {
|
|
55
|
+
component: "PerformanceMiddleware",
|
|
56
|
+
method: req.method,
|
|
57
|
+
path: req.originalUrl,
|
|
58
|
+
requestId: req.requestId,
|
|
59
|
+
clientIP: req.ip,
|
|
60
|
+
result: 'call',
|
|
61
|
+
reason: 'Performance monitoring started'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Record the request in the performance monitoring service
|
|
65
|
+
performanceService.recordRequest(req);
|
|
66
|
+
|
|
67
|
+
// Start a trace for this request
|
|
68
|
+
const traceId = performanceService.startTrace('HTTP Request', {
|
|
69
|
+
method: req.method,
|
|
70
|
+
path: req.originalUrl,
|
|
71
|
+
requestId: req.requestId,
|
|
72
|
+
userAgent: req.get('User-Agent'),
|
|
73
|
+
clientIP: req.ip
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Store the trace ID on the request object for other middleware to use
|
|
77
|
+
req.traceId = traceId;
|
|
78
|
+
|
|
79
|
+
// Capture the original end function
|
|
80
|
+
const originalEnd = res.end;
|
|
81
|
+
|
|
82
|
+
// Override the end function
|
|
83
|
+
res.end = function(chunk, encoding) {
|
|
84
|
+
// Call the original end function
|
|
85
|
+
originalEnd.call(this, chunk, encoding);
|
|
86
|
+
|
|
87
|
+
// Calculate request duration
|
|
88
|
+
const duration = Date.now() - req.startTime;
|
|
89
|
+
|
|
90
|
+
// Store duration for other middleware to use
|
|
91
|
+
req.duration = duration;
|
|
92
|
+
|
|
93
|
+
// Classify request only when needed for metrics (performance optimization)
|
|
94
|
+
const requestType = classifier(req);
|
|
95
|
+
req.requestType = requestType;
|
|
96
|
+
|
|
97
|
+
// Record standard metrics using the logger.metric function
|
|
98
|
+
const result = (res.statusCode >= 200 && res.statusCode < 300) ? 'success' : 'failure';
|
|
99
|
+
const reason = (result === 'success') ? 'Request completed successfully' : (res.statusMessage || 'Request failed');
|
|
100
|
+
logger.metric('http_request_duration_ms', duration, {
|
|
101
|
+
method: req.method,
|
|
102
|
+
path: req.originalUrl,
|
|
103
|
+
status: res.statusCode,
|
|
104
|
+
request_type: requestType,
|
|
105
|
+
result,
|
|
106
|
+
reason
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Record error metrics if applicable
|
|
110
|
+
if (res.statusCode >= 400) {
|
|
111
|
+
logger.metric('http_errors_total', 1, {
|
|
112
|
+
method: req.method,
|
|
113
|
+
status: res.statusCode,
|
|
114
|
+
error_type: res.statusCode >= 500 ? 'server_error' : 'client_error',
|
|
115
|
+
request_type: requestType,
|
|
116
|
+
result: 'failure',
|
|
117
|
+
reason: res.statusMessage || 'Request failed'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Complete the trace with request results
|
|
122
|
+
performanceService.completeTrace(traceId, {
|
|
123
|
+
statusCode: res.statusCode,
|
|
124
|
+
success: res.statusCode < 400,
|
|
125
|
+
error: res.statusCode >= 400 ? (res.statusMessage || 'HTTP Error') : null,
|
|
126
|
+
duration,
|
|
127
|
+
responseSize: res._contentLength || 0
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Record specialized metrics based on the request type (if configured)
|
|
131
|
+
const metricName = metricsByType[requestType];
|
|
132
|
+
if (metricName) {
|
|
133
|
+
logger.metric(metricName, duration, {
|
|
134
|
+
result,
|
|
135
|
+
reason,
|
|
136
|
+
method: req.method,
|
|
137
|
+
request_type: requestType
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Always log errors regardless of load level
|
|
142
|
+
if (res.statusCode >= 500) {
|
|
143
|
+
logger.error("Server error occurred", {
|
|
144
|
+
component: "API",
|
|
145
|
+
method: req.method,
|
|
146
|
+
path: req.originalUrl,
|
|
147
|
+
statusCode: res.statusCode,
|
|
148
|
+
statusMessage: res.statusMessage,
|
|
149
|
+
duration,
|
|
150
|
+
requestId: req.requestId,
|
|
151
|
+
traceId
|
|
152
|
+
});
|
|
153
|
+
} else if (res.statusCode >= 400) {
|
|
154
|
+
logger.warn("Client error occurred", {
|
|
155
|
+
component: "API",
|
|
156
|
+
method: req.method,
|
|
157
|
+
path: req.originalUrl,
|
|
158
|
+
statusCode: res.statusCode,
|
|
159
|
+
statusMessage: res.statusMessage,
|
|
160
|
+
duration,
|
|
161
|
+
requestId: req.requestId,
|
|
162
|
+
traceId
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
next();
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
module.exports = performanceMw;
|
|
172
|
+
module.exports.defaultClassifier = defaultClassifier;
|