@onlineapps/service-validator-core 1.0.2 → 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
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
|
|
1
|
+
const ValidationProofCodec = require('./ValidationProofCodec');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ValidationProofVerifier - Verifies SHA256 validation proofs from pre-validation
|
|
5
5
|
*
|
|
6
|
-
* This
|
|
7
|
-
*
|
|
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,152 +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
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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', 'timestamp'];
|
|
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.timestamp);
|
|
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
|
-
}
|
|
24
|
+
// Delegate verification to centralized codec
|
|
25
|
+
const result = ValidationProofCodec.decode(proof, {
|
|
26
|
+
maxProofAge: this.maxProofAge
|
|
27
|
+
});
|
|
91
28
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
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)}`);
|
|
101
39
|
}
|
|
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
|
-
}
|
|
118
|
-
|
|
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
|
-
};
|
|
126
|
-
}
|
|
127
|
-
console.log(`[ValidationProofVerifier] ✓ All tests passed: ${data.testsPassed}/${data.testsRun}`);
|
|
128
|
-
|
|
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
|
-
const hashInput = JSON.stringify({
|
|
164
|
-
serviceName: data.serviceName,
|
|
165
|
-
version: data.version,
|
|
166
|
-
testsRun: data.testsRun,
|
|
167
|
-
testsPassed: data.testsPassed,
|
|
168
|
-
testsFailed: data.testsFailed,
|
|
169
|
-
validator: data.validator,
|
|
170
|
-
timestamp: data.timestamp,
|
|
171
|
-
fingerprint: data.fingerprint || ''
|
|
172
|
-
}, null, 0); // No whitespace for consistency
|
|
173
40
|
|
|
174
|
-
return
|
|
41
|
+
return result;
|
|
175
42
|
}
|
|
176
43
|
}
|
|
177
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;
|