@onlineapps/service-validator-core 1.0.11 → 1.0.13

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.11",
3
+ "version": "1.0.13",
4
4
  "description": "Core validation logic for microservices",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/config.js CHANGED
@@ -28,3 +28,6 @@ const runtimeCfg = createRuntimeConfig({
28
28
  module.exports = runtimeCfg;
29
29
 
30
30
 
31
+
32
+
33
+
package/src/defaults.js CHANGED
@@ -15,3 +15,6 @@ module.exports = {
15
15
  };
16
16
 
17
17
 
18
+
19
+
20
+
@@ -52,12 +52,20 @@ class FingerprintUtils {
52
52
  }
53
53
 
54
54
  /**
55
- * Generate fingerprint for service configuration
56
- * Used for cache validation - skip re-validation if config unchanged
55
+ * Generate fingerprint for service configuration.
56
+ * Used for cache validation - skip re-validation if config unchanged.
57
+ *
58
+ * The fingerprint input includes `operations` (per the Operations Registry
59
+ * contract) so that changes to `mutates` / `resource_type` / schemas
60
+ * invalidate the cached validation proof as expected.
61
+ *
62
+ * @see api/docs/standards/operations-registry-contract.md §6 Fingerprint
63
+ * @see api/docs/standards/OPERATIONS.md
57
64
  *
58
65
  * @param {Object} config - Service configuration
59
66
  * @param {string} config.version - Service version (REQUIRED)
60
- * @param {Array} config.endpoints - API endpoints (REQUIRED)
67
+ * @param {Array} config.endpoints - API endpoints (REQUIRED, may be empty)
68
+ * @param {Object} config.operations - Operations map from operations.json (REQUIRED, may be empty)
61
69
  * @param {Object} config.metadata - Service metadata (REQUIRED)
62
70
  * @param {string} config.health - Health check endpoint (REQUIRED)
63
71
  * @returns {string} Configuration fingerprint
@@ -73,6 +81,9 @@ class FingerprintUtils {
73
81
  if (!Array.isArray(config.endpoints)) {
74
82
  throw new Error('config.endpoints must be an array');
75
83
  }
84
+ if (typeof config.operations !== 'object' || config.operations === null || Array.isArray(config.operations)) {
85
+ throw new Error('config.operations must be an object (see operations-registry-contract.md §3)');
86
+ }
76
87
  if (typeof config.metadata !== 'object' || config.metadata === null) {
77
88
  throw new Error('config.metadata must be an object');
78
89
  }
@@ -80,12 +91,17 @@ class FingerprintUtils {
80
91
  throw new Error('config.health is required');
81
92
  }
82
93
 
83
- return this.generate({
94
+ // Stable, deep-sorted JSON so nested operation schemas actually contribute
95
+ // to the hash. The generic generate() uses JSON.stringify(data, keys) which
96
+ // filters keys recursively and would drop nested operation properties.
97
+ const stable = _stableStringify({
84
98
  version: config.version,
85
99
  endpoints: config.endpoints,
100
+ operations: config.operations,
86
101
  metadata: config.metadata,
87
102
  health: config.health
88
103
  });
104
+ return crypto.createHash('sha256').update(stable).digest('hex');
89
105
  }
90
106
 
91
107
  /**
@@ -123,4 +139,24 @@ class FingerprintUtils {
123
139
  }
124
140
  }
125
141
 
142
+ /**
143
+ * Deep stable JSON serializer — sorts keys at every depth so that
144
+ * logically-equal objects always produce the same string (and therefore the
145
+ * same hash), regardless of key insertion order.
146
+ *
147
+ * Private helper: only used by generateConfigFingerprint to avoid the
148
+ * recursive-filter bug in JSON.stringify(data, keysArray).
149
+ */
150
+ function _stableStringify(value) {
151
+ if (value === null || typeof value !== 'object') {
152
+ return JSON.stringify(value);
153
+ }
154
+ if (Array.isArray(value)) {
155
+ return '[' + value.map(_stableStringify).join(',') + ']';
156
+ }
157
+ const keys = Object.keys(value).sort();
158
+ const parts = keys.map(k => JSON.stringify(k) + ':' + _stableStringify(value[k]));
159
+ return '{' + parts.join(',') + '}';
160
+ }
161
+
126
162
  module.exports = FingerprintUtils;
@@ -0,0 +1,80 @@
1
+ const FingerprintUtils = require('../../src/utils/FingerprintUtils');
2
+
3
+ /**
4
+ * Unit tests for FingerprintUtils.generateConfigFingerprint — verifies that
5
+ * `operations` is a required input so changes to mutates / resource_type
6
+ * invalidate the cached validation proof.
7
+ *
8
+ * @see api/docs/standards/operations-registry-contract.md §6 Fingerprint
9
+ */
10
+ describe('FingerprintUtils.generateConfigFingerprint @unit', () => {
11
+ const baseConfig = () => ({
12
+ version: '1.0.0',
13
+ endpoints: [],
14
+ operations: {},
15
+ metadata: {},
16
+ health: '/health'
17
+ });
18
+
19
+ it('returns a 64-char SHA256 hex for a valid config', () => {
20
+ const hash = FingerprintUtils.generateConfigFingerprint(baseConfig());
21
+ expect(typeof hash).toBe('string');
22
+ expect(hash).toMatch(/^[0-9a-f]{64}$/);
23
+ });
24
+
25
+ it('produces a different fingerprint when operations change', () => {
26
+ const a = FingerprintUtils.generateConfigFingerprint({
27
+ ...baseConfig(),
28
+ operations: { 'get-x': { mutates: false, resource_type: null } }
29
+ });
30
+ const b = FingerprintUtils.generateConfigFingerprint({
31
+ ...baseConfig(),
32
+ operations: { 'get-x': { mutates: true, resource_type: 'doc' } }
33
+ });
34
+ expect(a).not.toEqual(b);
35
+ });
36
+
37
+ it('throws when operations is missing', () => {
38
+ const cfg = baseConfig();
39
+ delete cfg.operations;
40
+ expect(() => FingerprintUtils.generateConfigFingerprint(cfg))
41
+ .toThrow(/operations must be an object/);
42
+ });
43
+
44
+ it('throws when operations is an array (must be a keyed object)', () => {
45
+ expect(() => FingerprintUtils.generateConfigFingerprint({
46
+ ...baseConfig(),
47
+ operations: []
48
+ })).toThrow(/operations must be an object/);
49
+ });
50
+
51
+ it('throws when operations is null', () => {
52
+ expect(() => FingerprintUtils.generateConfigFingerprint({
53
+ ...baseConfig(),
54
+ operations: null
55
+ })).toThrow(/operations must be an object/);
56
+ });
57
+
58
+ it('accepts an empty operations object (service with no operations yet)', () => {
59
+ const hash = FingerprintUtils.generateConfigFingerprint({
60
+ ...baseConfig(),
61
+ operations: {}
62
+ });
63
+ expect(hash).toMatch(/^[0-9a-f]{64}$/);
64
+ });
65
+
66
+ it('still enforces other required fields', () => {
67
+ expect(() => FingerprintUtils.generateConfigFingerprint({
68
+ endpoints: [], operations: {}, metadata: {}, health: '/h'
69
+ })).toThrow(/version is required/);
70
+ expect(() => FingerprintUtils.generateConfigFingerprint({
71
+ version: '1', operations: {}, metadata: {}, health: '/h'
72
+ })).toThrow(/endpoints must be an array/);
73
+ expect(() => FingerprintUtils.generateConfigFingerprint({
74
+ version: '1', endpoints: [], operations: {}, health: '/h'
75
+ })).toThrow(/metadata must be an object/);
76
+ expect(() => FingerprintUtils.generateConfigFingerprint({
77
+ version: '1', endpoints: [], operations: {}, metadata: {}
78
+ })).toThrow(/health is required/);
79
+ });
80
+ });