@onlineapps/conn-orch-validator 2.0.27 → 2.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -88,20 +88,52 @@ services/my-service/
|
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
**Proof Lifecycle:**
|
|
91
|
-
- **Valid for:**
|
|
91
|
+
- **Valid for:** 7 days OR until service fingerprint changes
|
|
92
92
|
- **Fingerprint:** SHA256 hash of (version + operations + dependencies + config)
|
|
93
93
|
- **Revalidation:** Automatic if proof missing/invalid/expired
|
|
94
94
|
- **Registry:** Accepts service with valid proof (skips Tier 2 validation)
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
98
|
+
## Implementation Standard Levels
|
|
99
|
+
|
|
100
|
+
The validator evaluates each service against cumulative implementation standards. Levels are ordered — each requires all previous to pass:
|
|
101
|
+
|
|
102
|
+
| Level | Name | Checks | Since |
|
|
103
|
+
|-------|------|--------|-------|
|
|
104
|
+
| **v1.0** | Base Service Standard | `conn-config/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
|
|
105
|
+
| **v1.1** | Multitenancy Standard | `wrapper.tenantContext` configured in `config.json` | 2026-03 |
|
|
106
|
+
| **v1.2** | Business Error Handling | `@onlineapps/service-common` dep + `businessErrorHandler` in `src/app.js` | 2026-03 |
|
|
107
|
+
|
|
108
|
+
**Key properties:**
|
|
109
|
+
- **Cumulative** — v1.2 requires v1.0 + v1.1 to also pass
|
|
110
|
+
- **Baked into validator** — older validator versions naturally know fewer levels (backward compatible)
|
|
111
|
+
- **Warnings** — next unsatisfied level generates `STANDARD_LEVEL_GAP` warnings with specific missing checks
|
|
112
|
+
- **Exposed in `/info`** — ServiceWrapper's `GET /info` endpoint returns the highest satisfied level
|
|
113
|
+
- **In validation results** — `validate()` returns `standardLevel` and `standardDetails`
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
// Programmatic access
|
|
117
|
+
const { ServiceStructureValidator } = require('@onlineapps/conn-orch-validator/src/validators/ServiceStructureValidator');
|
|
118
|
+
const validator = new ServiceStructureValidator('/path/to/service');
|
|
119
|
+
const { level, details } = validator.determineStandardLevel();
|
|
120
|
+
// level = 'v1.2', details = [{ level: 'v1.0', passed: true, checks: [...] }, ...]
|
|
121
|
+
|
|
122
|
+
// List all known levels
|
|
123
|
+
ServiceStructureValidator.getStandardLevels();
|
|
124
|
+
// [{ level: 'v1.0', name: 'Base Service Standard', since: '2025-06-01' }, ...]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Related:** [Service Endpoints — Standard Levels](/docs/standards/SERVICE_ENDPOINTS.md#implementation-standard-levels), [Error Handling Standard](/docs/standards/ERROR_HANDLING.md#business-service-error-standard)
|
|
128
|
+
|
|
98
129
|
## Related Documentation
|
|
99
130
|
|
|
100
131
|
- [SERVICE_REGISTRATION_FLOW.md](/services/hello-service/docs/SERVICE_REGISTRATION_FLOW.md)
|
|
101
132
|
- [/docs/architecture/validator.md](/docs/architecture/validator.md)
|
|
102
133
|
- [/docs/standards/OPERATIONS.md](/docs/standards/OPERATIONS.md)
|
|
134
|
+
- [/docs/standards/ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md)
|
|
103
135
|
- [@onlineapps/service-validator-core](/shared/service-validator-core/README.md)
|
|
104
136
|
|
|
105
137
|
---
|
|
106
138
|
|
|
107
|
-
*Last updated:
|
|
139
|
+
*Last updated: 2026-03-24*
|
package/jest.config.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
process.env.OA_VALIDATION_TENANT_ID = process.env.OA_VALIDATION_TENANT_ID || '99';
|
|
4
|
+
process.env.OA_VALIDATION_WORKSPACE_ID = process.env.OA_VALIDATION_WORKSPACE_ID || '200';
|
|
5
|
+
|
|
3
6
|
module.exports = {
|
|
4
7
|
testEnvironment: 'node',
|
|
5
8
|
coverageDirectory: 'coverage',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.29",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -157,11 +157,11 @@ class CookbookTestRunner {
|
|
|
157
157
|
// Resolve operation endpoint from operations.json
|
|
158
158
|
const endpoint = await this.resolveOperation(step.service, step.operation);
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
|
|
161
|
+
const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
|
|
162
|
+
if (!validationTenantId || !validationWorkspaceId) {
|
|
163
|
+
throw new Error('[CookbookTestRunner] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
|
|
164
|
+
}
|
|
165
165
|
const request = {
|
|
166
166
|
method: endpoint.method,
|
|
167
167
|
url: `${this.serviceUrl}${endpoint.path}`,
|
|
@@ -169,7 +169,8 @@ class CookbookTestRunner {
|
|
|
169
169
|
timeout: testConfig.timeout || this.timeout,
|
|
170
170
|
headers: {
|
|
171
171
|
'x-validation-request': 'true',
|
|
172
|
-
'
|
|
172
|
+
'x-tenant-id': validationTenantId,
|
|
173
|
+
'x-workspace-id': validationWorkspaceId,
|
|
173
174
|
...resolveHeaders(endpoint.headers),
|
|
174
175
|
...resolveHeaders(step.headers)
|
|
175
176
|
}
|
|
@@ -215,13 +215,15 @@ class ServiceReadinessValidator {
|
|
|
215
215
|
// Generate test input based on schema
|
|
216
216
|
const testInput = this.generateTestInput(operation.input);
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
|
|
219
|
+
const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
|
|
220
|
+
if (!validationTenantId || !validationWorkspaceId) {
|
|
221
|
+
throw new Error('[ServiceReadinessValidator] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
|
|
222
|
+
}
|
|
222
223
|
const headers = {
|
|
223
224
|
'x-validation-request': 'true',
|
|
224
|
-
'
|
|
225
|
+
'x-tenant-id': validationTenantId,
|
|
226
|
+
'x-workspace-id': validationWorkspaceId,
|
|
225
227
|
...resolveHeaders(operation.headers)
|
|
226
228
|
};
|
|
227
229
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
|
|
5
|
+
const { ValidationProofCodec, ValidationProofVerifier } = require('@onlineapps/service-validator-core');
|
|
6
6
|
const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
|
|
7
7
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
8
8
|
const ServiceReadinessValidator = require('./ServiceReadinessValidator');
|
|
@@ -97,10 +97,10 @@ class ValidationOrchestrator {
|
|
|
97
97
|
return false;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
// Verify proof integrity + age using the shared core verifier defaults.
|
|
101
|
+
// This MUST match what Tier-2 validator enforces to avoid PROOF_EXPIRED drift.
|
|
102
|
+
const verifier = new ValidationProofVerifier();
|
|
103
|
+
const verificationResult = verifier.verifyProof(proof);
|
|
104
104
|
|
|
105
105
|
if (!verificationResult.valid) {
|
|
106
106
|
console.log(`[ValidationOrchestrator] Proof validation failed: ${verificationResult.reason}`);
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Validates that a business service has correct directory structure,
|
|
5
5
|
* configuration files, and follows OA Drive standards.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Each standard level is cumulative — v1.2 requires all checks from v1.0 + v1.1 + v1.2.
|
|
8
|
+
* Older validator versions naturally support fewer levels (they don't know about newer ones).
|
|
8
9
|
*
|
|
9
10
|
* @module validators/ServiceStructureValidator
|
|
10
11
|
*/
|
|
@@ -14,6 +15,102 @@
|
|
|
14
15
|
const fs = require('fs');
|
|
15
16
|
const path = require('path');
|
|
16
17
|
|
|
18
|
+
const STANDARD_LEVELS = [
|
|
19
|
+
{
|
|
20
|
+
level: 'v1.0',
|
|
21
|
+
name: 'Base Service Standard',
|
|
22
|
+
since: '2025-06-01',
|
|
23
|
+
checks: (root) => {
|
|
24
|
+
const results = [];
|
|
25
|
+
const has = (p) => fs.existsSync(path.join(root, p));
|
|
26
|
+
const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
|
|
27
|
+
|
|
28
|
+
results.push({ passed: has('conn-config'), id: 'dir_conn_config', message: 'conn-config/ directory' });
|
|
29
|
+
results.push({ passed: has('src'), id: 'dir_src', message: 'src/ directory' });
|
|
30
|
+
results.push({ passed: has('tests'), id: 'dir_tests', message: 'tests/ directory' });
|
|
31
|
+
results.push({ passed: has('src/app.js'), id: 'file_app', message: 'src/app.js' });
|
|
32
|
+
results.push({ passed: has('index.js'), id: 'file_index', message: 'index.js entry point' });
|
|
33
|
+
|
|
34
|
+
const configRaw = read('conn-config/config.json');
|
|
35
|
+
let configValid = false;
|
|
36
|
+
if (configRaw) {
|
|
37
|
+
try {
|
|
38
|
+
const cfg = JSON.parse(configRaw);
|
|
39
|
+
configValid = !!(cfg.service?.name && cfg.service?.port);
|
|
40
|
+
} catch { /* invalid JSON */ }
|
|
41
|
+
}
|
|
42
|
+
results.push({ passed: configValid, id: 'config_valid', message: 'Valid config.json with service.name + service.port' });
|
|
43
|
+
|
|
44
|
+
const opsRaw = read('conn-config/operations.json');
|
|
45
|
+
let opsValid = false;
|
|
46
|
+
if (opsRaw) {
|
|
47
|
+
try {
|
|
48
|
+
const ops = JSON.parse(opsRaw);
|
|
49
|
+
opsValid = !!(ops.operations && typeof ops.operations === 'object');
|
|
50
|
+
} catch { /* invalid JSON */ }
|
|
51
|
+
}
|
|
52
|
+
results.push({ passed: opsValid, id: 'operations_valid', message: 'Valid operations.json' });
|
|
53
|
+
|
|
54
|
+
const pkgRaw = read('package.json');
|
|
55
|
+
let hasWrapper = false;
|
|
56
|
+
if (pkgRaw) {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(pkgRaw);
|
|
59
|
+
hasWrapper = !!pkg.dependencies?.['@onlineapps/service-wrapper'];
|
|
60
|
+
} catch { /* invalid JSON */ }
|
|
61
|
+
}
|
|
62
|
+
results.push({ passed: hasWrapper, id: 'dep_service_wrapper', message: '@onlineapps/service-wrapper dependency' });
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
level: 'v1.1',
|
|
69
|
+
name: 'Multitenancy Standard',
|
|
70
|
+
since: '2026-03-01',
|
|
71
|
+
checks: (root) => {
|
|
72
|
+
const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
|
|
73
|
+
|
|
74
|
+
const configRaw = read('conn-config/config.json');
|
|
75
|
+
let hasTenantContext = false;
|
|
76
|
+
if (configRaw) {
|
|
77
|
+
try {
|
|
78
|
+
const cfg = JSON.parse(configRaw);
|
|
79
|
+
hasTenantContext = cfg.wrapper?.tenantContext !== undefined;
|
|
80
|
+
} catch { /* invalid JSON */ }
|
|
81
|
+
}
|
|
82
|
+
return [
|
|
83
|
+
{ passed: hasTenantContext, id: 'tenant_context', message: 'wrapper.tenantContext configured in config.json' }
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
level: 'v1.2',
|
|
89
|
+
name: 'Business Error Handling Standard',
|
|
90
|
+
since: '2026-03-24',
|
|
91
|
+
checks: (root) => {
|
|
92
|
+
const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
|
|
93
|
+
|
|
94
|
+
const pkgRaw = read('package.json');
|
|
95
|
+
let hasServiceCommon = false;
|
|
96
|
+
if (pkgRaw) {
|
|
97
|
+
try {
|
|
98
|
+
const pkg = JSON.parse(pkgRaw);
|
|
99
|
+
hasServiceCommon = !!pkg.dependencies?.['@onlineapps/service-common'];
|
|
100
|
+
} catch { /* invalid JSON */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const appContent = read('src/app.js') || '';
|
|
104
|
+
const hasErrorMiddleware = appContent.includes('businessErrorHandler');
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
{ passed: hasServiceCommon, id: 'dep_service_common', message: '@onlineapps/service-common dependency' },
|
|
108
|
+
{ passed: hasErrorMiddleware, id: 'error_middleware', message: 'businessErrorHandler middleware in src/app.js' }
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
|
|
17
114
|
class ServiceStructureValidator {
|
|
18
115
|
constructor(serviceRoot) {
|
|
19
116
|
this.serviceRoot = serviceRoot;
|
|
@@ -49,16 +146,79 @@ class ServiceStructureValidator {
|
|
|
49
146
|
// 5. Validate test structure
|
|
50
147
|
this.validateTestStructure();
|
|
51
148
|
|
|
149
|
+
// 6. Determine standard level
|
|
150
|
+
const standardResult = this.determineStandardLevel();
|
|
151
|
+
|
|
52
152
|
const valid = this.errors.length === 0;
|
|
53
153
|
|
|
54
154
|
return {
|
|
55
155
|
valid,
|
|
56
156
|
errors: this.errors,
|
|
57
157
|
warnings: this.warnings,
|
|
58
|
-
info: this.info
|
|
158
|
+
info: this.info,
|
|
159
|
+
standardLevel: standardResult.level,
|
|
160
|
+
standardDetails: standardResult.details
|
|
59
161
|
};
|
|
60
162
|
}
|
|
61
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Determine the highest satisfied implementation standard level.
|
|
166
|
+
* Levels are cumulative — each requires all previous levels to pass.
|
|
167
|
+
*
|
|
168
|
+
* @returns {{ level: string|null, details: Array<{ level: string, name: string, passed: boolean, checks: Array }> }}
|
|
169
|
+
*/
|
|
170
|
+
determineStandardLevel() {
|
|
171
|
+
const details = [];
|
|
172
|
+
let highestPassed = null;
|
|
173
|
+
|
|
174
|
+
for (const standard of STANDARD_LEVELS) {
|
|
175
|
+
const checkResults = standard.checks(this.serviceRoot);
|
|
176
|
+
const allPassed = checkResults.every(c => c.passed);
|
|
177
|
+
|
|
178
|
+
details.push({
|
|
179
|
+
level: standard.level,
|
|
180
|
+
name: standard.name,
|
|
181
|
+
since: standard.since,
|
|
182
|
+
passed: allPassed,
|
|
183
|
+
checks: checkResults
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (allPassed) {
|
|
187
|
+
highestPassed = standard.level;
|
|
188
|
+
} else {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (highestPassed) {
|
|
194
|
+
this.info.push(`✓ Implementation standard level: ${highestPassed}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const nextLevel = details.find(d => !d.passed);
|
|
198
|
+
if (nextLevel) {
|
|
199
|
+
const failing = nextLevel.checks.filter(c => !c.passed);
|
|
200
|
+
for (const check of failing) {
|
|
201
|
+
this.warnings.push({
|
|
202
|
+
type: 'STANDARD_LEVEL_GAP',
|
|
203
|
+
level: nextLevel.level,
|
|
204
|
+
check: check.id,
|
|
205
|
+
message: `Standard ${nextLevel.level} (${nextLevel.name}): missing ${check.message}`,
|
|
206
|
+
fix: `Implement ${check.message} to reach standard ${nextLevel.level}. See docs/standards/SERVICE_TEMPLATE.md`
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { level: highestPassed, details };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get all known standard levels (for /info endpoint and external tools).
|
|
216
|
+
* @returns {Array<{ level: string, name: string, since: string }>}
|
|
217
|
+
*/
|
|
218
|
+
static getStandardLevels() {
|
|
219
|
+
return STANDARD_LEVELS.map(s => ({ level: s.level, name: s.name, since: s.since }));
|
|
220
|
+
}
|
|
221
|
+
|
|
62
222
|
/**
|
|
63
223
|
* Validate directory structure exists
|
|
64
224
|
*/
|
|
@@ -350,7 +510,8 @@ class ServiceStructureValidator {
|
|
|
350
510
|
// Check for @onlineapps dependencies
|
|
351
511
|
const requiredDeps = [
|
|
352
512
|
'@onlineapps/service-wrapper',
|
|
353
|
-
'@onlineapps/conn-orch-validator'
|
|
513
|
+
'@onlineapps/conn-orch-validator',
|
|
514
|
+
'@onlineapps/service-common'
|
|
354
515
|
];
|
|
355
516
|
|
|
356
517
|
for (const dep of requiredDeps) {
|
|
@@ -391,6 +552,26 @@ class ServiceStructureValidator {
|
|
|
391
552
|
});
|
|
392
553
|
} else {
|
|
393
554
|
this.info.push('✓ Found src/app.js');
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const appContent = fs.readFileSync(appPath, 'utf-8');
|
|
558
|
+
if (appContent.includes('businessErrorHandler')) {
|
|
559
|
+
this.info.push('✓ businessErrorHandler middleware registered');
|
|
560
|
+
} else {
|
|
561
|
+
this.warnings.push({
|
|
562
|
+
type: 'MISSING_ERROR_MIDDLEWARE',
|
|
563
|
+
path: 'src/app.js',
|
|
564
|
+
message: 'businessErrorHandler middleware not found in src/app.js',
|
|
565
|
+
fix: 'Register businessErrorHandler from @onlineapps/service-common as Express error middleware. See: /docs/standards/ERROR_HANDLING.md'
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} catch (readError) {
|
|
569
|
+
this.warnings.push({
|
|
570
|
+
type: 'UNREADABLE_APP',
|
|
571
|
+
path: 'src/app.js',
|
|
572
|
+
message: `Could not read src/app.js: ${readError.message}`
|
|
573
|
+
});
|
|
574
|
+
}
|
|
394
575
|
}
|
|
395
576
|
|
|
396
577
|
const indexPath = path.join(this.serviceRoot, 'index.js');
|