@onlineapps/service-validator-core 1.0.3 → 1.0.5
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 +1 -1
- package/src/index.js +7 -1
- package/src/security/ValidationProofCodec.js +176 -0
- package/src/security/ValidationProofVerifier.js +18 -142
- package/src/security/certificateManager.js +2 -2
- package/src/security/tokenManager.js +2 -2
- package/src/types/ValidationProofSchema.js +212 -0
- package/src/utils/FingerprintUtils.js +109 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,6 +5,9 @@ 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');
|
|
10
|
+
const FingerprintUtils = require('./utils/FingerprintUtils');
|
|
8
11
|
|
|
9
12
|
class ValidationCore {
|
|
10
13
|
constructor(config = {}) {
|
|
@@ -209,4 +212,7 @@ class ValidationCore {
|
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
module.exports = ValidationCore;
|
|
212
|
-
module.exports.ValidationProofVerifier = ValidationProofVerifier;
|
|
215
|
+
module.exports.ValidationProofVerifier = ValidationProofVerifier;
|
|
216
|
+
module.exports.ValidationProofCodec = ValidationProofCodec;
|
|
217
|
+
module.exports.ValidationProofSchema = ValidationProofSchema;
|
|
218
|
+
module.exports.FingerprintUtils = FingerprintUtils;
|
|
@@ -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,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
|
-
//
|
|
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', '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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
+
const FingerprintUtils = require('../utils/FingerprintUtils');
|
|
2
3
|
|
|
3
4
|
class CertificateManager {
|
|
4
5
|
constructor(config = {}) {
|
|
@@ -147,8 +148,7 @@ class CertificateManager {
|
|
|
147
148
|
* @returns {string} Fingerprint
|
|
148
149
|
*/
|
|
149
150
|
generateFingerprint(data) {
|
|
150
|
-
|
|
151
|
-
return crypto.createHash('sha256').update(sortedData).digest('hex');
|
|
151
|
+
return FingerprintUtils.generateValidationFingerprint(data);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const jwt = require('jsonwebtoken');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
|
+
const FingerprintUtils = require('../utils/FingerprintUtils');
|
|
3
4
|
|
|
4
5
|
class TokenManager {
|
|
5
6
|
constructor(config = {}) {
|
|
@@ -186,8 +187,7 @@ class TokenManager {
|
|
|
186
187
|
* @returns {string} Fingerprint hash
|
|
187
188
|
*/
|
|
188
189
|
generateFingerprint(validationData) {
|
|
189
|
-
|
|
190
|
-
return crypto.createHash('sha256').update(sortedData).digest('hex');
|
|
190
|
+
return FingerprintUtils.generateValidationFingerprint(validationData);
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -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;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FingerprintUtils - Centralized fingerprint generation
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for SHA256 fingerprinting across the system.
|
|
7
|
+
* Used for configuration caching, certificate verification, and content integrity.
|
|
8
|
+
*
|
|
9
|
+
* NOT to be confused with ValidationProofCodec which is specifically for
|
|
10
|
+
* validation proof hashing (different purpose).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* - configFingerprint: Identify service configuration version
|
|
14
|
+
* - Certificate fingerprint: Verify certificate data integrity
|
|
15
|
+
* - Token fingerprint: Generate token identifiers
|
|
16
|
+
* - Content fingerprint: Verify file/storage content
|
|
17
|
+
*/
|
|
18
|
+
class FingerprintUtils {
|
|
19
|
+
/**
|
|
20
|
+
* Generate SHA256 fingerprint for any data
|
|
21
|
+
*
|
|
22
|
+
* @param {*} data - Data to fingerprint (will be JSON.stringify if object)
|
|
23
|
+
* @param {Object} options - Options
|
|
24
|
+
* @param {boolean} options.sortKeys - Sort object keys (default: true)
|
|
25
|
+
* @returns {string} SHA256 hash (64 hex characters)
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const hash = FingerprintUtils.generate({ version: '1.0', endpoints: [] });
|
|
29
|
+
* // Returns: consistent SHA256 hash for the object
|
|
30
|
+
*/
|
|
31
|
+
static generate(data, options = {}) {
|
|
32
|
+
const sortKeys = options.sortKeys !== false; // default true
|
|
33
|
+
|
|
34
|
+
let input;
|
|
35
|
+
|
|
36
|
+
// Handle different data types
|
|
37
|
+
if (Buffer.isBuffer(data)) {
|
|
38
|
+
input = data;
|
|
39
|
+
} else if (typeof data === 'string') {
|
|
40
|
+
input = data;
|
|
41
|
+
} else if (typeof data === 'object' && data !== null) {
|
|
42
|
+
// Serialize object with optional key sorting
|
|
43
|
+
const keys = sortKeys ? Object.keys(data).sort() : Object.keys(data);
|
|
44
|
+
input = JSON.stringify(data, keys);
|
|
45
|
+
} else {
|
|
46
|
+
// Primitives (number, boolean, null, undefined)
|
|
47
|
+
input = String(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate SHA256 hash
|
|
51
|
+
return crypto.createHash('sha256').update(input).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate fingerprint for service configuration
|
|
56
|
+
* Used for cache validation - skip re-validation if config unchanged
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} config - Service configuration
|
|
59
|
+
* @param {string} config.version - Service version
|
|
60
|
+
* @param {Array} config.endpoints - API endpoints
|
|
61
|
+
* @param {Object} config.metadata - Service metadata
|
|
62
|
+
* @param {string} config.health - Health check endpoint
|
|
63
|
+
* @returns {string} Configuration fingerprint
|
|
64
|
+
*/
|
|
65
|
+
static generateConfigFingerprint(config) {
|
|
66
|
+
return this.generate({
|
|
67
|
+
version: config.version,
|
|
68
|
+
endpoints: config.endpoints || [],
|
|
69
|
+
metadata: config.metadata || {},
|
|
70
|
+
health: config.health || '/health'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate fingerprint for validation results
|
|
76
|
+
* Used in certificates and tokens
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} validationResults - Validation results data
|
|
79
|
+
* @returns {string} Validation results fingerprint
|
|
80
|
+
*/
|
|
81
|
+
static generateValidationFingerprint(validationResults) {
|
|
82
|
+
return this.generate(validationResults);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate fingerprint for content (files, storage)
|
|
87
|
+
*
|
|
88
|
+
* @param {string|Buffer} content - Content to fingerprint
|
|
89
|
+
* @returns {string} Content fingerprint
|
|
90
|
+
*/
|
|
91
|
+
static generateContentFingerprint(content) {
|
|
92
|
+
return this.generate(content, { sortKeys: false });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Verify fingerprint matches data
|
|
97
|
+
*
|
|
98
|
+
* @param {*} data - Data to verify
|
|
99
|
+
* @param {string} expectedFingerprint - Expected fingerprint
|
|
100
|
+
* @param {Object} options - Options (passed to generate)
|
|
101
|
+
* @returns {boolean} True if fingerprint matches
|
|
102
|
+
*/
|
|
103
|
+
static verify(data, expectedFingerprint, options = {}) {
|
|
104
|
+
const actualFingerprint = this.generate(data, options);
|
|
105
|
+
return actualFingerprint === expectedFingerprint;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = FingerprintUtils;
|