@onlineapps/service-validator-core 1.0.2
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/README.md +127 -0
- package/coverage/clover.xml +468 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/index.js.html +643 -0
- package/coverage/lcov-report/src/security/certificateManager.js.html +799 -0
- package/coverage/lcov-report/src/security/index.html +131 -0
- package/coverage/lcov-report/src/security/tokenManager.js.html +622 -0
- package/coverage/lcov-report/src/validators/connectorValidator.js.html +787 -0
- package/coverage/lcov-report/src/validators/endpointValidator.js.html +577 -0
- package/coverage/lcov-report/src/validators/healthValidator.js.html +655 -0
- package/coverage/lcov-report/src/validators/index.html +161 -0
- package/coverage/lcov-report/src/validators/openApiValidator.js.html +517 -0
- package/coverage/lcov.info +982 -0
- package/jest.config.js +21 -0
- package/package.json +31 -0
- package/src/index.js +212 -0
- package/src/security/ValidationProofVerifier.js +178 -0
- package/src/security/certificateManager.js +239 -0
- package/src/security/tokenManager.js +194 -0
- package/src/validators/connectorValidator.js +235 -0
- package/src/validators/endpointValidator.js +165 -0
- package/src/validators/healthValidator.js +191 -0
- package/src/validators/openApiValidator.js +145 -0
- package/test/component/validation-flow.test.js +353 -0
- package/test/integration/real-validation.test.js +548 -0
- package/test/unit/ValidationCore.test.js +320 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
class TokenManager {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.secret = config.secret || process.env.VALIDATION_TOKEN_SECRET || this.generateSecret();
|
|
7
|
+
this.expiresIn = config.expiresIn || '24h';
|
|
8
|
+
this.algorithm = config.algorithm || 'HS256';
|
|
9
|
+
this.issuer = config.issuer || 'validation-core';
|
|
10
|
+
this.usedTokens = new Set(); // Track used tokens for single-use enforcement
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a secure secret if none provided
|
|
15
|
+
* @returns {string} Generated secret
|
|
16
|
+
*/
|
|
17
|
+
generateSecret() {
|
|
18
|
+
return crypto.randomBytes(64).toString('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a pre-validation token with a new random secret
|
|
23
|
+
* @param {object} payload - Token payload
|
|
24
|
+
* @returns {Promise<object>} Generated token and secret
|
|
25
|
+
*/
|
|
26
|
+
async generateToken(payload) {
|
|
27
|
+
const tokenId = crypto.randomBytes(16).toString('hex');
|
|
28
|
+
|
|
29
|
+
// Generate a random secret for this specific token
|
|
30
|
+
const tokenSecret = crypto.randomBytes(64).toString('hex');
|
|
31
|
+
|
|
32
|
+
const tokenPayload = {
|
|
33
|
+
...payload,
|
|
34
|
+
jti: tokenId, // JWT ID for tracking single use
|
|
35
|
+
iat: Math.floor(Date.now() / 1000),
|
|
36
|
+
iss: this.issuer,
|
|
37
|
+
type: payload.type || 'pre-validation'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const options = {
|
|
41
|
+
expiresIn: this.expiresIn,
|
|
42
|
+
algorithm: this.algorithm
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const token = jwt.sign(tokenPayload, tokenSecret, options);
|
|
46
|
+
|
|
47
|
+
// Return both token and secret
|
|
48
|
+
return {
|
|
49
|
+
token,
|
|
50
|
+
secret: tokenSecret,
|
|
51
|
+
tokenId
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Verify and decode a token
|
|
57
|
+
* @param {string} token - Token to verify
|
|
58
|
+
* @param {string} secret - Secret to verify with (optional, uses provided secret)
|
|
59
|
+
* @returns {Promise<object>} Decoded payload
|
|
60
|
+
*/
|
|
61
|
+
async verifyToken(token, secret = null) {
|
|
62
|
+
try {
|
|
63
|
+
// Use provided secret or fall back to instance secret
|
|
64
|
+
const verifySecret = secret || this.secret;
|
|
65
|
+
|
|
66
|
+
// Verify token signature and expiration
|
|
67
|
+
const decoded = jwt.verify(token, verifySecret, {
|
|
68
|
+
algorithms: [this.algorithm],
|
|
69
|
+
issuer: this.issuer
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Check if token has already been used (single-use enforcement)
|
|
73
|
+
if (decoded.jti && this.usedTokens.has(decoded.jti)) {
|
|
74
|
+
throw new Error('Token has already been used');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Mark token as used
|
|
78
|
+
if (decoded.jti) {
|
|
79
|
+
this.usedTokens.add(decoded.jti);
|
|
80
|
+
|
|
81
|
+
// Clean up old tokens after expiry (prevent memory leak)
|
|
82
|
+
if (this.usedTokens.size > 10000) {
|
|
83
|
+
this.cleanupUsedTokens();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return decoded;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error.name === 'TokenExpiredError') {
|
|
90
|
+
throw new Error('Token has expired');
|
|
91
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
92
|
+
throw new Error('Invalid token');
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate a validation request token (for service-to-validator communication)
|
|
100
|
+
* @param {object} preValidationResult - Pre-validation results
|
|
101
|
+
* @param {string} serviceName - Service requesting validation
|
|
102
|
+
* @returns {Promise<string>} Request token
|
|
103
|
+
*/
|
|
104
|
+
async generateRequestToken(preValidationResult, serviceName) {
|
|
105
|
+
if (!preValidationResult.success) {
|
|
106
|
+
throw new Error('Cannot generate request token for failed pre-validation');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.generateToken({
|
|
110
|
+
type: 'validation-request',
|
|
111
|
+
serviceName,
|
|
112
|
+
preValidation: {
|
|
113
|
+
timestamp: preValidationResult.timestamp,
|
|
114
|
+
checks: preValidationResult.checks
|
|
115
|
+
},
|
|
116
|
+
expiresIn: '1h' // Shorter expiry for request tokens
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verify a validation request token
|
|
122
|
+
* @param {string} token - Request token
|
|
123
|
+
* @returns {Promise<object>} Token payload
|
|
124
|
+
*/
|
|
125
|
+
async verifyRequestToken(token) {
|
|
126
|
+
const decoded = await this.verifyToken(token);
|
|
127
|
+
|
|
128
|
+
if (decoded.type !== 'validation-request') {
|
|
129
|
+
throw new Error('Invalid token type for validation request');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!decoded.preValidation) {
|
|
133
|
+
throw new Error('Missing pre-validation data in request token');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return decoded;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Clean up expired tokens from used tokens set
|
|
141
|
+
*/
|
|
142
|
+
cleanupUsedTokens() {
|
|
143
|
+
// In a production environment, this would check expiry times
|
|
144
|
+
// For now, we'll clear half of the oldest tokens
|
|
145
|
+
const tokensToKeep = Array.from(this.usedTokens).slice(-5000);
|
|
146
|
+
this.usedTokens = new Set(tokensToKeep);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Revoke a token (add to blacklist)
|
|
151
|
+
* @param {string} tokenOrJti - Token or JWT ID to revoke
|
|
152
|
+
*/
|
|
153
|
+
revokeToken(tokenOrJti) {
|
|
154
|
+
try {
|
|
155
|
+
// Try to decode if it's a full token
|
|
156
|
+
const decoded = jwt.decode(tokenOrJti);
|
|
157
|
+
if (decoded && decoded.jti) {
|
|
158
|
+
this.usedTokens.add(decoded.jti);
|
|
159
|
+
} else {
|
|
160
|
+
// Assume it's a JTI directly
|
|
161
|
+
this.usedTokens.add(tokenOrJti);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// If decode fails, assume it's a JTI
|
|
165
|
+
this.usedTokens.add(tokenOrJti);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if a token is revoked/used
|
|
171
|
+
* @param {string} token - Token to check
|
|
172
|
+
* @returns {boolean} True if revoked/used
|
|
173
|
+
*/
|
|
174
|
+
isTokenRevoked(token) {
|
|
175
|
+
try {
|
|
176
|
+
const decoded = jwt.decode(token);
|
|
177
|
+
return decoded && decoded.jti && this.usedTokens.has(decoded.jti);
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generate a fingerprint for validation data
|
|
185
|
+
* @param {object} validationData - Data to fingerprint
|
|
186
|
+
* @returns {string} Fingerprint hash
|
|
187
|
+
*/
|
|
188
|
+
generateFingerprint(validationData) {
|
|
189
|
+
const sortedData = JSON.stringify(validationData, Object.keys(validationData).sort());
|
|
190
|
+
return crypto.createHash('sha256').update(sortedData).digest('hex');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = TokenManager;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
class ConnectorValidator {
|
|
2
|
+
constructor(requiredConnectors = []) {
|
|
3
|
+
this.requiredConnectors = requiredConnectors.length > 0 ? requiredConnectors : [
|
|
4
|
+
'connector-logger',
|
|
5
|
+
'connector-storage',
|
|
6
|
+
'connector-registry-client',
|
|
7
|
+
'connector-mq-client',
|
|
8
|
+
'connector-cookbook'
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validates connector usage in service
|
|
14
|
+
* @param {object} metadata - Service metadata containing connector info
|
|
15
|
+
* @returns {Promise<object>} Validation result
|
|
16
|
+
*/
|
|
17
|
+
async validate(metadata) {
|
|
18
|
+
const result = {
|
|
19
|
+
valid: true,
|
|
20
|
+
errors: [],
|
|
21
|
+
warnings: [],
|
|
22
|
+
info: {
|
|
23
|
+
connectors: {},
|
|
24
|
+
missingRequired: [],
|
|
25
|
+
additionalConnectors: []
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const connectors = metadata.connectors || {};
|
|
31
|
+
|
|
32
|
+
// Check for required connectors
|
|
33
|
+
for (const connectorName of this.requiredConnectors) {
|
|
34
|
+
const hasConnector = connectors[connectorName] !== undefined;
|
|
35
|
+
result.info.connectors[connectorName] = hasConnector;
|
|
36
|
+
|
|
37
|
+
if (!hasConnector) {
|
|
38
|
+
result.warnings.push(`Missing required connector: ${connectorName}`);
|
|
39
|
+
result.info.missingRequired.push(connectorName);
|
|
40
|
+
} else {
|
|
41
|
+
// Validate connector configuration
|
|
42
|
+
const connectorConfig = connectors[connectorName];
|
|
43
|
+
|
|
44
|
+
if (typeof connectorConfig === 'object') {
|
|
45
|
+
// Check for version
|
|
46
|
+
if (!connectorConfig.version) {
|
|
47
|
+
result.warnings.push(`Connector ${connectorName} missing version information`);
|
|
48
|
+
} else {
|
|
49
|
+
// Validate version format
|
|
50
|
+
if (!this.isValidVersion(connectorConfig.version)) {
|
|
51
|
+
result.warnings.push(`Connector ${connectorName} has invalid version format: ${connectorConfig.version}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if enabled
|
|
56
|
+
if (connectorConfig.enabled === false) {
|
|
57
|
+
result.warnings.push(`Connector ${connectorName} is disabled`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for configuration
|
|
61
|
+
if (!connectorConfig.config && connectorConfig.enabled !== false) {
|
|
62
|
+
result.info.connectors[connectorName] = {
|
|
63
|
+
present: true,
|
|
64
|
+
configured: false
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
result.info.connectors[connectorName] = {
|
|
68
|
+
present: true,
|
|
69
|
+
configured: true,
|
|
70
|
+
version: connectorConfig.version
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for additional connectors (not in required list)
|
|
78
|
+
Object.keys(connectors).forEach(connectorName => {
|
|
79
|
+
if (!this.requiredConnectors.includes(connectorName)) {
|
|
80
|
+
result.info.additionalConnectors.push(connectorName);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Special checks for specific connectors
|
|
85
|
+
this.validateSpecialConnectors(connectors, result);
|
|
86
|
+
|
|
87
|
+
// Check health endpoint
|
|
88
|
+
if (!metadata.health && !metadata.healthEndpoint) {
|
|
89
|
+
result.warnings.push('Service should define a health check endpoint');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for workflow support
|
|
93
|
+
if (!connectors['connector-cookbook']?.enabled) {
|
|
94
|
+
result.warnings.push('Service should support workflow processing via connector-cookbook');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for logging
|
|
98
|
+
if (!connectors['connector-logger']) {
|
|
99
|
+
result.errors.push('connector-logger is critical for service observability');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Determine overall validity
|
|
103
|
+
if (result.info.missingRequired.length > 0) {
|
|
104
|
+
result.valid = false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (result.errors.length > 0) {
|
|
108
|
+
result.valid = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
result.valid = false;
|
|
113
|
+
result.errors.push(`Connector validation error: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate special connector requirements
|
|
121
|
+
* @param {object} connectors - Connectors configuration
|
|
122
|
+
* @param {object} result - Validation result object to update
|
|
123
|
+
*/
|
|
124
|
+
validateSpecialConnectors(connectors, result) {
|
|
125
|
+
// MQ Client specific checks
|
|
126
|
+
if (connectors['connector-mq-client']) {
|
|
127
|
+
const mqConfig = connectors['connector-mq-client'];
|
|
128
|
+
if (mqConfig.config) {
|
|
129
|
+
if (!mqConfig.config.url && !mqConfig.config.host) {
|
|
130
|
+
result.warnings.push('connector-mq-client should have URL or host configured');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Storage connector checks
|
|
136
|
+
if (connectors['connector-storage']) {
|
|
137
|
+
const storageConfig = connectors['connector-storage'];
|
|
138
|
+
if (storageConfig.config) {
|
|
139
|
+
if (!storageConfig.config.bucket && !storageConfig.config.defaultBucket) {
|
|
140
|
+
result.warnings.push('connector-storage should have bucket configured');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Registry client checks
|
|
146
|
+
if (connectors['connector-registry-client']) {
|
|
147
|
+
const registryConfig = connectors['connector-registry-client'];
|
|
148
|
+
if (registryConfig.config) {
|
|
149
|
+
if (!registryConfig.config.serviceName) {
|
|
150
|
+
result.errors.push('connector-registry-client must have serviceName configured');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Logger checks
|
|
156
|
+
if (connectors['connector-logger']) {
|
|
157
|
+
const loggerConfig = connectors['connector-logger'];
|
|
158
|
+
if (loggerConfig.config) {
|
|
159
|
+
if (!loggerConfig.config.level) {
|
|
160
|
+
result.warnings.push('connector-logger should have log level configured');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if version string is valid semver
|
|
168
|
+
* @param {string} version - Version string to validate
|
|
169
|
+
* @returns {boolean} Validation result
|
|
170
|
+
*/
|
|
171
|
+
isValidVersion(version) {
|
|
172
|
+
// Simple semver check
|
|
173
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w\.]+)?(\+[\w\.]+)?$/;
|
|
174
|
+
return semverRegex.test(version);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generate connector report
|
|
179
|
+
* @param {object} metadata - Service metadata
|
|
180
|
+
* @returns {object} Connector report
|
|
181
|
+
*/
|
|
182
|
+
generateReport(metadata) {
|
|
183
|
+
const connectors = metadata.connectors || {};
|
|
184
|
+
const report = {
|
|
185
|
+
summary: {
|
|
186
|
+
total: Object.keys(connectors).length,
|
|
187
|
+
required: this.requiredConnectors.length,
|
|
188
|
+
missing: 0,
|
|
189
|
+
disabled: 0
|
|
190
|
+
},
|
|
191
|
+
details: []
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Check each required connector
|
|
195
|
+
this.requiredConnectors.forEach(name => {
|
|
196
|
+
const connector = connectors[name];
|
|
197
|
+
const detail = {
|
|
198
|
+
name,
|
|
199
|
+
required: true,
|
|
200
|
+
present: !!connector,
|
|
201
|
+
enabled: connector ? connector.enabled !== false : false,
|
|
202
|
+
version: connector?.version || null,
|
|
203
|
+
configured: connector?.config ? true : false
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (!detail.present) {
|
|
207
|
+
report.summary.missing++;
|
|
208
|
+
}
|
|
209
|
+
if (detail.present && !detail.enabled) {
|
|
210
|
+
report.summary.disabled++;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
report.details.push(detail);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Add additional connectors
|
|
217
|
+
Object.keys(connectors).forEach(name => {
|
|
218
|
+
if (!this.requiredConnectors.includes(name)) {
|
|
219
|
+
const connector = connectors[name];
|
|
220
|
+
report.details.push({
|
|
221
|
+
name,
|
|
222
|
+
required: false,
|
|
223
|
+
present: true,
|
|
224
|
+
enabled: connector.enabled !== false,
|
|
225
|
+
version: connector.version || null,
|
|
226
|
+
configured: connector.config ? true : false
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return report;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = ConnectorValidator;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
class EndpointValidator {
|
|
2
|
+
/**
|
|
3
|
+
* Validates service endpoints
|
|
4
|
+
* @param {Array} endpoints - Array of endpoint definitions
|
|
5
|
+
* @returns {Promise<object>} Validation result
|
|
6
|
+
*/
|
|
7
|
+
async validate(endpoints) {
|
|
8
|
+
const result = {
|
|
9
|
+
valid: true,
|
|
10
|
+
errors: [],
|
|
11
|
+
warnings: [],
|
|
12
|
+
info: {}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Check if endpoints is an array
|
|
17
|
+
if (!Array.isArray(endpoints)) {
|
|
18
|
+
result.valid = false;
|
|
19
|
+
result.errors.push('Endpoints must be an array');
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check for at least one endpoint
|
|
24
|
+
if (endpoints.length === 0) {
|
|
25
|
+
result.valid = false;
|
|
26
|
+
result.errors.push('Service must define at least one endpoint');
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
result.info.count = endpoints.length;
|
|
31
|
+
const paths = new Set();
|
|
32
|
+
const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
33
|
+
|
|
34
|
+
// Validate each endpoint
|
|
35
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
36
|
+
const endpoint = endpoints[i];
|
|
37
|
+
const endpointPrefix = `Endpoint[${i}]`;
|
|
38
|
+
|
|
39
|
+
// Validate required fields
|
|
40
|
+
if (!endpoint.path) {
|
|
41
|
+
result.errors.push(`${endpointPrefix}: Missing required "path" field`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!endpoint.method) {
|
|
46
|
+
result.errors.push(`${endpointPrefix} ${endpoint.path}: Missing required "method" field`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate path format
|
|
51
|
+
if (!endpoint.path.startsWith('/')) {
|
|
52
|
+
result.errors.push(`${endpointPrefix}: Path "${endpoint.path}" must start with /`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate method
|
|
56
|
+
const method = endpoint.method.toUpperCase();
|
|
57
|
+
if (!validMethods.includes(method)) {
|
|
58
|
+
result.errors.push(`${endpointPrefix} ${endpoint.path}: Invalid method "${endpoint.method}"`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for duplicates
|
|
62
|
+
const key = `${method} ${endpoint.path}`;
|
|
63
|
+
if (paths.has(key)) {
|
|
64
|
+
result.errors.push(`${endpointPrefix}: Duplicate endpoint definition: ${key}`);
|
|
65
|
+
}
|
|
66
|
+
paths.add(key);
|
|
67
|
+
|
|
68
|
+
// Validate handler
|
|
69
|
+
if (endpoint.handler) {
|
|
70
|
+
if (typeof endpoint.handler !== 'string' && typeof endpoint.handler !== 'function') {
|
|
71
|
+
result.errors.push(`${endpointPrefix} ${endpoint.path}: Handler must be a string or function`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate middleware (if present)
|
|
76
|
+
if (endpoint.middleware) {
|
|
77
|
+
if (!Array.isArray(endpoint.middleware)) {
|
|
78
|
+
result.warnings.push(`${endpointPrefix} ${endpoint.path}: Middleware should be an array`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for authentication
|
|
83
|
+
if (endpoint.auth === undefined) {
|
|
84
|
+
result.warnings.push(`${endpointPrefix} ${endpoint.path}: Consider explicitly setting auth requirement`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate rate limiting
|
|
88
|
+
if (endpoint.rateLimit && typeof endpoint.rateLimit !== 'object') {
|
|
89
|
+
result.warnings.push(`${endpointPrefix} ${endpoint.path}: Rate limit should be an object`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Additional checks
|
|
94
|
+
const hasMethods = {
|
|
95
|
+
GET: false,
|
|
96
|
+
POST: false,
|
|
97
|
+
PUT: false,
|
|
98
|
+
PATCH: false,
|
|
99
|
+
DELETE: false
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
endpoints.forEach(ep => {
|
|
103
|
+
if (ep.method) {
|
|
104
|
+
hasMethods[ep.method.toUpperCase()] = true;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Check for REST compliance
|
|
109
|
+
if (hasMethods.POST && !hasMethods.GET) {
|
|
110
|
+
result.warnings.push('REST API should provide GET endpoints for resources that can be created');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (hasMethods.DELETE && !hasMethods.GET) {
|
|
114
|
+
result.warnings.push('REST API should provide GET endpoints for resources that can be deleted');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result.info.methods = Object.keys(hasMethods).filter(m => hasMethods[m]);
|
|
118
|
+
|
|
119
|
+
if (result.errors.length > 0) {
|
|
120
|
+
result.valid = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
result.valid = false;
|
|
125
|
+
result.errors.push(`Endpoint validation error: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate endpoint documentation
|
|
133
|
+
* @param {Array} endpoints - Array of endpoint definitions
|
|
134
|
+
* @returns {object} Generated documentation
|
|
135
|
+
*/
|
|
136
|
+
generateDocumentation(endpoints) {
|
|
137
|
+
const docs = {
|
|
138
|
+
endpoints: [],
|
|
139
|
+
summary: {
|
|
140
|
+
total: endpoints.length,
|
|
141
|
+
byMethod: {}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const methodCounts = {};
|
|
146
|
+
|
|
147
|
+
endpoints.forEach(endpoint => {
|
|
148
|
+
const method = endpoint.method?.toUpperCase() || 'UNKNOWN';
|
|
149
|
+
methodCounts[method] = (methodCounts[method] || 0) + 1;
|
|
150
|
+
|
|
151
|
+
docs.endpoints.push({
|
|
152
|
+
method: method,
|
|
153
|
+
path: endpoint.path,
|
|
154
|
+
description: endpoint.description || 'No description provided',
|
|
155
|
+
auth: endpoint.auth !== undefined ? endpoint.auth : 'Not specified',
|
|
156
|
+
handler: endpoint.handler || 'Not specified'
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
docs.summary.byMethod = methodCounts;
|
|
161
|
+
return docs;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = EndpointValidator;
|