@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:** 30 days OR until service fingerprint changes
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: 2025-10-22*
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.27",
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
- // Build request
161
- // Validation context headers:
162
- // 1. x-validation-request: signals this is a validation probe (deterministic responses)
163
- // 2. account-id: required by multitenancy middleware for /api/** endpoints
164
- const validationAccountId = process.env.OA_VALIDATION_ACCOUNT_ID || '99999';
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
- 'account-id': validationAccountId,
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
- // Validation context headers:
219
- // 1. x-validation-request: signals this is a validation probe
220
- // 2. account-id: required by multitenancy middleware for /api/** endpoints
221
- const validationAccountId = process.env.OA_VALIDATION_ACCOUNT_ID || '99999';
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
- 'account-id': validationAccountId,
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
- // Decode and verify proof using ValidationProofCodec
101
- const verificationResult = ValidationProofCodec.decode(proof, {
102
- maxProofAge: 30 * 24 * 60 * 60 * 1000 // 30 days
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
- * Used by test helpers to provide clear error messages when something is wrong.
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');