@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.
Files changed (35) hide show
  1. package/README.md +127 -0
  2. package/coverage/clover.xml +468 -0
  3. package/coverage/coverage-final.json +8 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +146 -0
  8. package/coverage/lcov-report/prettify.css +1 -0
  9. package/coverage/lcov-report/prettify.js +2 -0
  10. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  11. package/coverage/lcov-report/sorter.js +210 -0
  12. package/coverage/lcov-report/src/index.html +116 -0
  13. package/coverage/lcov-report/src/index.js.html +643 -0
  14. package/coverage/lcov-report/src/security/certificateManager.js.html +799 -0
  15. package/coverage/lcov-report/src/security/index.html +131 -0
  16. package/coverage/lcov-report/src/security/tokenManager.js.html +622 -0
  17. package/coverage/lcov-report/src/validators/connectorValidator.js.html +787 -0
  18. package/coverage/lcov-report/src/validators/endpointValidator.js.html +577 -0
  19. package/coverage/lcov-report/src/validators/healthValidator.js.html +655 -0
  20. package/coverage/lcov-report/src/validators/index.html +161 -0
  21. package/coverage/lcov-report/src/validators/openApiValidator.js.html +517 -0
  22. package/coverage/lcov.info +982 -0
  23. package/jest.config.js +21 -0
  24. package/package.json +31 -0
  25. package/src/index.js +212 -0
  26. package/src/security/ValidationProofVerifier.js +178 -0
  27. package/src/security/certificateManager.js +239 -0
  28. package/src/security/tokenManager.js +194 -0
  29. package/src/validators/connectorValidator.js +235 -0
  30. package/src/validators/endpointValidator.js +165 -0
  31. package/src/validators/healthValidator.js +191 -0
  32. package/src/validators/openApiValidator.js +145 -0
  33. package/test/component/validation-flow.test.js +353 -0
  34. package/test/integration/real-validation.test.js +548 -0
  35. 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;