@onlineapps/service-validator-core 1.0.3 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-validator-core",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Core validation logic for microservices",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -5,6 +5,8 @@ const HealthValidator = require('./validators/healthValidator');
5
5
  const TokenManager = require('./security/tokenManager');
6
6
  const CertificateManager = require('./security/certificateManager');
7
7
  const ValidationProofVerifier = require('./security/ValidationProofVerifier');
8
+ const ValidationProofCodec = require('./security/ValidationProofCodec');
9
+ const ValidationProofSchema = require('./types/ValidationProofSchema');
8
10
 
9
11
  class ValidationCore {
10
12
  constructor(config = {}) {
@@ -209,4 +211,6 @@ class ValidationCore {
209
211
  }
210
212
 
211
213
  module.exports = ValidationCore;
212
- module.exports.ValidationProofVerifier = ValidationProofVerifier;
214
+ module.exports.ValidationProofVerifier = ValidationProofVerifier;
215
+ module.exports.ValidationProofCodec = ValidationProofCodec;
216
+ module.exports.ValidationProofSchema = ValidationProofSchema;
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const ValidationProofSchema = require('../types/ValidationProofSchema');
5
+
6
+ /**
7
+ * ValidationProofCodec - Centralized encode/decode for validation proofs
8
+ *
9
+ * Single source of truth for encoding and decoding validation proofs.
10
+ * Both Generator and Verifier MUST use this codec to ensure consistency.
11
+ *
12
+ * Key principles:
13
+ * - Encoding and decoding use EXACTLY the same algorithm
14
+ * - Hash is SHA256 of JSON.stringify(data, sorted keys)
15
+ * - Data structure is validated against ValidationProofSchema
16
+ */
17
+ class ValidationProofCodec {
18
+ /**
19
+ * Encode validation data into hash proof
20
+ *
21
+ * @param {Object} validationData - Validation data to encode
22
+ * @returns {Object} { validationProof: string, validationData: Object }
23
+ * @throws {Error} If validation data doesn't match schema
24
+ */
25
+ static encode(validationData) {
26
+ // 1. Validate data structure
27
+ const validation = ValidationProofSchema.validate(validationData);
28
+ if (!validation.valid) {
29
+ const errorDetails = validation.errors.map(e => ` - ${e.field}: ${e.message}`).join('\n');
30
+ throw new Error(`Invalid validation data structure:\n${errorDetails}`);
31
+ }
32
+
33
+ // 2. Generate hash
34
+ const hash = this._generateHash(validationData);
35
+
36
+ // 3. Return proof object
37
+ return {
38
+ validationProof: hash,
39
+ validationData: validationData
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Decode and verify validation proof
45
+ *
46
+ * @param {Object} proof - Proof object { validationProof, validationData }
47
+ * @param {Object} options - Verification options
48
+ * @param {number} options.maxProofAge - Maximum proof age in milliseconds (default 7 days)
49
+ * @returns {Object} Verification result { valid, reason, details, data }
50
+ */
51
+ static decode(proof, options = {}) {
52
+ const maxProofAge = options.maxProofAge || 7 * 24 * 60 * 60 * 1000; // 7 days
53
+
54
+ // 1. Check proof structure
55
+ if (!proof || typeof proof !== 'object') {
56
+ return this._error('INVALID_STRUCTURE', 'Proof must be an object');
57
+ }
58
+
59
+ if (!proof.validationProof || typeof proof.validationProof !== 'string') {
60
+ return this._error('MISSING_HASH', 'validationProof field is required');
61
+ }
62
+
63
+ if (!proof.validationData || typeof proof.validationData !== 'object') {
64
+ return this._error('MISSING_DATA', 'validationData field is required');
65
+ }
66
+
67
+ // 2. Check hash format (SHA256 = 64 hex characters)
68
+ if (!/^[a-f0-9]{64}$/i.test(proof.validationProof)) {
69
+ return this._error('INVALID_HASH_FORMAT', 'Hash must be 64-character hex string (SHA256)');
70
+ }
71
+
72
+ // 3. Validate data structure against schema
73
+ const data = proof.validationData;
74
+ const schemaValidation = ValidationProofSchema.validate(data);
75
+ if (!schemaValidation.valid) {
76
+ const errorDetails = schemaValidation.errors.map(e => `${e.field}: ${e.message}`).join('; ');
77
+ return this._error('SCHEMA_VALIDATION_FAILED', errorDetails);
78
+ }
79
+
80
+ // 4. Verify hash integrity
81
+ const recomputedHash = this._generateHash(data);
82
+ if (recomputedHash !== proof.validationProof) {
83
+ return this._error('HASH_MISMATCH',
84
+ 'Validation data does not match hash (data may have been tampered with)',
85
+ { expected: proof.validationProof, computed: recomputedHash }
86
+ );
87
+ }
88
+
89
+ // 5. Check proof age
90
+ const proofDate = new Date(data.validatedAt);
91
+ const now = new Date();
92
+ const age = now - proofDate;
93
+
94
+ if (age > maxProofAge) {
95
+ const maxDays = Math.floor(maxProofAge / (24 * 60 * 60 * 1000));
96
+ const ageDays = Math.floor(age / (24 * 60 * 60 * 1000));
97
+ return this._error('PROOF_EXPIRED', `Proof is ${ageDays} days old (max ${maxDays} days)`);
98
+ }
99
+
100
+ // 6. Check test results
101
+ if (data.testsRun <= 0) {
102
+ return this._error('NO_TESTS', 'At least one test must be run');
103
+ }
104
+
105
+ if (data.testsFailed > 0) {
106
+ return this._error('TESTS_FAILED', `${data.testsFailed} out of ${data.testsRun} tests failed`);
107
+ }
108
+
109
+ if (data.testsPassed !== data.testsRun) {
110
+ return this._error('TEST_COUNT_MISMATCH',
111
+ `Passed (${data.testsPassed}) + Failed (${data.testsFailed}) ≠ Total (${data.testsRun})`
112
+ );
113
+ }
114
+
115
+ // 7. All checks passed
116
+ return {
117
+ valid: true,
118
+ reason: 'VALID',
119
+ details: {
120
+ serviceName: data.serviceName,
121
+ version: data.version,
122
+ testsRun: data.testsRun,
123
+ testsPassed: data.testsPassed,
124
+ validator: data.validator,
125
+ validatedAt: data.validatedAt,
126
+ durationMs: data.durationMs
127
+ },
128
+ data: data
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Generate SHA256 hash from validation data
134
+ *
135
+ * CRITICAL: This algorithm MUST be identical in encode() and decode()
136
+ *
137
+ * @private
138
+ * @param {Object} data - Validation data
139
+ * @returns {string} SHA256 hash (64 hex characters)
140
+ */
141
+ static _generateHash(data) {
142
+ // Stringify with SORTED keys to ensure deterministic output
143
+ const hashInput = JSON.stringify(data, Object.keys(data).sort());
144
+ return crypto.createHash('sha256').update(hashInput).digest('hex');
145
+ }
146
+
147
+ /**
148
+ * Create error result object
149
+ * @private
150
+ */
151
+ static _error(reason, message, details = null) {
152
+ return {
153
+ valid: false,
154
+ reason,
155
+ details: details || message
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Verify that encode/decode are symmetric (for testing)
161
+ *
162
+ * @param {Object} validationData - Test data
163
+ * @returns {boolean} True if encode->decode returns valid
164
+ */
165
+ static testSymmetry(validationData) {
166
+ try {
167
+ const encoded = this.encode(validationData);
168
+ const decoded = this.decode(encoded);
169
+ return decoded.valid;
170
+ } catch (error) {
171
+ return false;
172
+ }
173
+ }
174
+ }
175
+
176
+ module.exports = ValidationProofCodec;
@@ -1,15 +1,10 @@
1
- const crypto = require('crypto');
1
+ const ValidationProofCodec = require('./ValidationProofCodec');
2
2
 
3
3
  /**
4
4
  * ValidationProofVerifier - Verifies SHA256 validation proofs from pre-validation
5
5
  *
6
- * This class verifies validation proofs generated by ValidationProofGenerator
7
- * from the conn-e2e-testing package. It validates:
8
- * - Hash integrity (SHA256)
9
- * - Proof structure and format
10
- * - Test results (passed/failed counts)
11
- * - Proof age (max 7 days)
12
- * - Validator identity
6
+ * This is now a THIN WRAPPER around ValidationProofCodec.
7
+ * All decoding and verification logic is delegated to the centralized codec.
13
8
  */
14
9
  class ValidationProofVerifier {
15
10
  constructor(options = {}) {
@@ -26,143 +21,24 @@ class ValidationProofVerifier {
26
21
  console.log('[ValidationProofVerifier] Starting proof verification');
27
22
  console.log('[ValidationProofVerifier] Proof hash:', proof.validationProof?.substring(0, 16) + '...');
28
23
 
29
- // 1. Check proof structure
30
- if (!proof || typeof proof !== 'object') {
31
- console.error('[ValidationProofVerifier] ❌ Invalid proof structure: not an object');
32
- return { valid: false, reason: 'INVALID_STRUCTURE', details: 'Proof must be an object' };
33
- }
34
-
35
- if (!proof.validationProof || typeof proof.validationProof !== 'string') {
36
- console.error('[ValidationProofVerifier] ❌ Missing or invalid validationProof field');
37
- return { valid: false, reason: 'MISSING_HASH', details: 'validationProof field is required' };
38
- }
39
-
40
- if (!proof.validationData || typeof proof.validationData !== 'object') {
41
- console.error('[ValidationProofVerifier] ❌ Missing or invalid validationData field');
42
- return { valid: false, reason: 'MISSING_DATA', details: 'validationData field is required' };
43
- }
44
-
45
- // 2. Check hash format (SHA256 = 64 hex characters)
46
- if (!/^[a-f0-9]{64}$/i.test(proof.validationProof)) {
47
- console.error('[ValidationProofVerifier] ❌ Invalid hash format:', proof.validationProof);
48
- return { valid: false, reason: 'INVALID_HASH_FORMAT', details: 'Hash must be 64-character hex string (SHA256)' };
49
- }
50
- console.log('[ValidationProofVerifier] ✓ Hash format valid (SHA256)');
51
-
52
- // 3. Verify hash integrity
53
- const data = proof.validationData;
54
- const recomputedHash = this._generateHash(data);
55
-
56
- if (recomputedHash !== proof.validationProof) {
57
- console.error('[ValidationProofVerifier] ❌ Hash mismatch');
58
- console.error('[ValidationProofVerifier] Expected:', proof.validationProof);
59
- console.error('[ValidationProofVerifier] Computed:', recomputedHash);
60
- return {
61
- valid: false,
62
- reason: 'HASH_MISMATCH',
63
- details: 'Validation data does not match hash (data may have been tampered with)'
64
- };
65
- }
66
- console.log('[ValidationProofVerifier] ✓ Hash integrity verified');
67
-
68
- // 4. Check required validationData fields
69
- const requiredFields = ['serviceName', 'version', 'testsRun', 'testsPassed', 'testsFailed', 'validator', 'validatedAt'];
70
- for (const field of requiredFields) {
71
- if (data[field] === undefined || data[field] === null) {
72
- console.error(`[ValidationProofVerifier] ❌ Missing required field: ${field}`);
73
- return {
74
- valid: false,
75
- reason: 'MISSING_FIELD',
76
- details: `Required field missing: ${field}`
77
- };
78
- }
79
- }
80
- console.log('[ValidationProofVerifier] ✓ All required fields present');
81
-
82
- // 5. Check proof age
83
- const proofDate = new Date(data.validatedAt);
84
- const now = new Date();
85
- const age = now - proofDate;
86
-
87
- if (isNaN(proofDate.getTime())) {
88
- console.error('[ValidationProofVerifier] ❌ Invalid timestamp:', data.timestamp);
89
- return { valid: false, reason: 'INVALID_TIMESTAMP', details: 'Timestamp is not a valid date' };
90
- }
91
-
92
- if (age > this.maxProofAge) {
93
- const maxDays = Math.floor(this.maxProofAge / (24 * 60 * 60 * 1000));
94
- const ageDays = Math.floor(age / (24 * 60 * 60 * 1000));
95
- console.error(`[ValidationProofVerifier] ❌ Proof too old: ${ageDays} days (max ${maxDays} days)`);
96
- return {
97
- valid: false,
98
- reason: 'PROOF_EXPIRED',
99
- details: `Proof is ${ageDays} days old (max ${maxDays} days)`
100
- };
101
- }
102
- console.log(`[ValidationProofVerifier] ✓ Proof age valid: ${Math.floor(age / (60 * 60 * 1000))} hours old`);
103
-
104
- // 6. Check test results
105
- if (data.testsRun <= 0) {
106
- console.error('[ValidationProofVerifier] ❌ No tests were run');
107
- return { valid: false, reason: 'NO_TESTS', details: 'At least one test must be run' };
108
- }
109
-
110
- if (data.testsFailed > 0) {
111
- console.error(`[ValidationProofVerifier] ❌ Some tests failed: ${data.testsFailed}/${data.testsRun}`);
112
- return {
113
- valid: false,
114
- reason: 'TESTS_FAILED',
115
- details: `${data.testsFailed} out of ${data.testsRun} tests failed`
116
- };
117
- }
24
+ // Delegate verification to centralized codec
25
+ const result = ValidationProofCodec.decode(proof, {
26
+ maxProofAge: this.maxProofAge
27
+ });
118
28
 
119
- if (data.testsPassed !== data.testsRun) {
120
- console.error('[ValidationProofVerifier] ❌ Test count mismatch');
121
- return {
122
- valid: false,
123
- reason: 'TEST_COUNT_MISMATCH',
124
- details: `Passed (${data.testsPassed}) + Failed (${data.testsFailed}) ≠ Total (${data.testsRun})`
125
- };
29
+ // Log results
30
+ if (result.valid) {
31
+ console.log('[ValidationProofVerifier] ✅ Proof verification successful');
32
+ console.log(`[ValidationProofVerifier] Service: ${result.details.serviceName} v${result.details.version}`);
33
+ console.log(`[ValidationProofVerifier] Tests: ${result.details.testsPassed}/${result.details.testsRun} passed`);
34
+ console.log(`[ValidationProofVerifier] Validator: ${result.details.validator}`);
35
+ console.log(`[ValidationProofVerifier] Timestamp: ${result.details.validatedAt}`);
36
+ } else {
37
+ console.error(`[ValidationProofVerifier] ❌ Verification failed: ${result.reason}`);
38
+ console.error(`[ValidationProofVerifier] Details: ${JSON.stringify(result.details)}`);
126
39
  }
127
- console.log(`[ValidationProofVerifier] ✓ All tests passed: ${data.testsPassed}/${data.testsRun}`);
128
40
 
129
- // 7. Check validator identity
130
- if (!data.validator || typeof data.validator !== 'string') {
131
- console.error('[ValidationProofVerifier] ❌ Invalid validator identity');
132
- return { valid: false, reason: 'INVALID_VALIDATOR', details: 'Validator identity is required' };
133
- }
134
- console.log('[ValidationProofVerifier] ✓ Validator:', data.validator);
135
-
136
- // 8. All checks passed
137
- console.log('[ValidationProofVerifier] ✅ Proof verification successful');
138
- console.log(`[ValidationProofVerifier] Service: ${data.serviceName} v${data.version}`);
139
- console.log(`[ValidationProofVerifier] Tests: ${data.testsPassed}/${data.testsRun} passed`);
140
- console.log(`[ValidationProofVerifier] Validator: ${data.validator}`);
141
- console.log(`[ValidationProofVerifier] Timestamp: ${data.timestamp}`);
142
-
143
- return {
144
- valid: true,
145
- reason: 'VALID',
146
- details: {
147
- serviceName: data.serviceName,
148
- version: data.version,
149
- testsRun: data.testsRun,
150
- testsPassed: data.testsPassed,
151
- validator: data.validator,
152
- timestamp: data.timestamp,
153
- fingerprint: data.fingerprint
154
- }
155
- };
156
- }
157
-
158
- /**
159
- * Generate SHA256 hash from validation data (must match ValidationProofGenerator)
160
- * @private
161
- */
162
- _generateHash(data) {
163
- // Match ValidationProofGenerator algorithm: stringify with sorted keys
164
- const hashInput = JSON.stringify(data, Object.keys(data).sort());
165
- return crypto.createHash('sha256').update(hashInput).digest('hex');
41
+ return result;
166
42
  }
167
43
  }
168
44
 
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ValidationProofSchema - Single source of truth for validation proof format
5
+ *
6
+ * This schema defines the EXACT structure and requirements for validation proofs.
7
+ * Both Generator and Verifier MUST comply with this specification.
8
+ *
9
+ * @specification v1.0.0
10
+ */
11
+ class ValidationProofSchema {
12
+ /**
13
+ * Get validation data schema definition
14
+ *
15
+ * @returns {Object} Schema definition with field specifications
16
+ */
17
+ static getSchema() {
18
+ return {
19
+ // Service identification
20
+ serviceName: {
21
+ type: 'string',
22
+ required: true,
23
+ description: 'Name of the service being validated',
24
+ example: 'hello-service'
25
+ },
26
+ version: {
27
+ type: 'string',
28
+ required: true,
29
+ description: 'Service version (semver)',
30
+ example: '1.0.0'
31
+ },
32
+
33
+ // Validator identification
34
+ validator: {
35
+ type: 'string',
36
+ required: true,
37
+ description: 'Name of the validator that generated the proof',
38
+ example: '@onlineapps/conn-e2e-testing'
39
+ },
40
+ validatorVersion: {
41
+ type: 'string',
42
+ required: true,
43
+ description: 'Validator version',
44
+ example: '1.0.0'
45
+ },
46
+
47
+ // Timestamp
48
+ validatedAt: {
49
+ type: 'string',
50
+ required: true,
51
+ format: 'ISO8601',
52
+ description: 'Timestamp when validation was performed',
53
+ example: '2025-10-02T10:30:45.123Z'
54
+ },
55
+
56
+ // Test results
57
+ testsRun: {
58
+ type: 'number',
59
+ required: true,
60
+ min: 0,
61
+ description: 'Total number of tests executed',
62
+ example: 42
63
+ },
64
+ testsPassed: {
65
+ type: 'number',
66
+ required: true,
67
+ min: 0,
68
+ description: 'Number of tests that passed',
69
+ example: 42
70
+ },
71
+ testsFailed: {
72
+ type: 'number',
73
+ required: true,
74
+ min: 0,
75
+ description: 'Number of tests that failed',
76
+ example: 0
77
+ },
78
+
79
+ // Performance
80
+ durationMs: {
81
+ type: 'number',
82
+ required: true,
83
+ min: 0,
84
+ description: 'Test execution duration in milliseconds',
85
+ example: 1234
86
+ },
87
+
88
+ // Dependencies (optional)
89
+ dependencies: {
90
+ type: 'object',
91
+ required: false,
92
+ description: 'Service dependencies with versions',
93
+ example: { 'express': '^4.18.2' }
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Get list of required fields
100
+ *
101
+ * @returns {Array<string>} Array of required field names
102
+ */
103
+ static getRequiredFields() {
104
+ const schema = this.getSchema();
105
+ return Object.entries(schema)
106
+ .filter(([_, spec]) => spec.required)
107
+ .map(([field, _]) => field);
108
+ }
109
+
110
+ /**
111
+ * Get list of all fields in canonical order (for hash generation)
112
+ *
113
+ * @returns {Array<string>} Array of field names in alphabetical order
114
+ */
115
+ static getFieldsInOrder() {
116
+ return Object.keys(this.getSchema()).sort();
117
+ }
118
+
119
+ /**
120
+ * Validate data structure against schema
121
+ *
122
+ * @param {Object} data - Data to validate
123
+ * @returns {Object} Validation result { valid: boolean, errors: Array }
124
+ */
125
+ static validate(data) {
126
+ const errors = [];
127
+ const schema = this.getSchema();
128
+
129
+ // Check all fields
130
+ for (const [fieldName, fieldSpec] of Object.entries(schema)) {
131
+ const value = data[fieldName];
132
+
133
+ // Required check
134
+ if (fieldSpec.required && (value === undefined || value === null)) {
135
+ errors.push({
136
+ field: fieldName,
137
+ error: 'MISSING_REQUIRED_FIELD',
138
+ message: `Required field '${fieldName}' is missing`
139
+ });
140
+ continue;
141
+ }
142
+
143
+ // Skip type checking if field is optional and not present
144
+ if (!fieldSpec.required && (value === undefined || value === null)) {
145
+ continue;
146
+ }
147
+
148
+ // Type check
149
+ const actualType = typeof value;
150
+ if (actualType !== fieldSpec.type) {
151
+ errors.push({
152
+ field: fieldName,
153
+ error: 'INVALID_TYPE',
154
+ message: `Field '${fieldName}' must be ${fieldSpec.type}, got ${actualType}`
155
+ });
156
+ continue;
157
+ }
158
+
159
+ // Number min check
160
+ if (fieldSpec.type === 'number' && fieldSpec.min !== undefined && value < fieldSpec.min) {
161
+ errors.push({
162
+ field: fieldName,
163
+ error: 'VALUE_TOO_SMALL',
164
+ message: `Field '${fieldName}' must be >= ${fieldSpec.min}, got ${value}`
165
+ });
166
+ }
167
+
168
+ // ISO8601 format check
169
+ if (fieldSpec.format === 'ISO8601') {
170
+ const date = new Date(value);
171
+ if (isNaN(date.getTime())) {
172
+ errors.push({
173
+ field: fieldName,
174
+ error: 'INVALID_FORMAT',
175
+ message: `Field '${fieldName}' must be valid ISO8601 timestamp`
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ return {
182
+ valid: errors.length === 0,
183
+ errors
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Get human-readable schema documentation
189
+ *
190
+ * @returns {string} Formatted schema documentation
191
+ */
192
+ static getDocumentation() {
193
+ const schema = this.getSchema();
194
+ let doc = 'Validation Proof Data Format Specification v1.0.0\n';
195
+ doc += '='.repeat(60) + '\n\n';
196
+
197
+ for (const [field, spec] of Object.entries(schema)) {
198
+ doc += `${field}:\n`;
199
+ doc += ` Type: ${spec.type}\n`;
200
+ doc += ` Required: ${spec.required ? 'YES' : 'NO'}\n`;
201
+ if (spec.format) doc += ` Format: ${spec.format}\n`;
202
+ if (spec.min !== undefined) doc += ` Min value: ${spec.min}\n`;
203
+ doc += ` Description: ${spec.description}\n`;
204
+ doc += ` Example: ${JSON.stringify(spec.example)}\n`;
205
+ doc += '\n';
206
+ }
207
+
208
+ return doc;
209
+ }
210
+ }
211
+
212
+ module.exports = ValidationProofSchema;