@totemsdk/manifest 0.1.0

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/dist/verify.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * verifyManifest — verifies a SignedManifest without external state.
3
+ *
4
+ * Steps:
5
+ * 1. Recompute the canonical manifest digest.
6
+ * 2. Verify the WOTS signature against the stored 32-byte PKdigest
7
+ * (signerPublicKey field) using wotsVerifyDigest.
8
+ * 3. Confirm authorAddress matches the manifest's address field.
9
+ *
10
+ * wotsVerifyDigest is used (rather than wotsVerify) because signerPublicKey
11
+ * stores the 32-byte WOTS PKdigest as returned by wotsKeypairFromSeed.kp.pk.
12
+ * The full 1088-byte key is not stored in SignedManifest to keep it compact.
13
+ */
14
+ import { wotsVerifyDigest, hexToBytes } from '@totemsdk/core';
15
+ import { manifestDigest } from './sign.js';
16
+ function manifestAddressField(manifest) {
17
+ switch (manifest.type) {
18
+ case 'app': return manifest.authorAddress;
19
+ case 'capability': return manifest.agentAddress;
20
+ case 'dapp': return manifest.authorAddress;
21
+ case 'edge-service': return manifest.operatorAddress;
22
+ default: {
23
+ const _exhaustive = manifest;
24
+ throw new Error(`verifyManifest: unknown manifest type: ${JSON.stringify(_exhaustive)}`);
25
+ }
26
+ }
27
+ }
28
+ export function verifyManifest(signed) {
29
+ const { manifest, signature, signerPublicKey, authorAddress } = signed;
30
+ let sigBytes;
31
+ let pkDigest;
32
+ try {
33
+ sigBytes = hexToBytes(signature);
34
+ pkDigest = hexToBytes(signerPublicKey);
35
+ }
36
+ catch (e) {
37
+ return { valid: false, reason: `hex decode failed: ${String(e)}`, signerAddress: authorAddress };
38
+ }
39
+ const digest = manifestDigest(manifest);
40
+ let sigValid;
41
+ try {
42
+ sigValid = wotsVerifyDigest(sigBytes, digest, pkDigest);
43
+ }
44
+ catch (e) {
45
+ return { valid: false, reason: `WOTS verify threw: ${String(e)}`, signerAddress: authorAddress };
46
+ }
47
+ if (!sigValid) {
48
+ return { valid: false, reason: 'WOTS signature invalid', signerAddress: authorAddress };
49
+ }
50
+ const expectedAddress = manifestAddressField(manifest);
51
+ if (authorAddress !== expectedAddress) {
52
+ return {
53
+ valid: false,
54
+ reason: `authorAddress mismatch: signed by '${authorAddress}' but manifest declares '${expectedAddress}'`,
55
+ signerAddress: authorAddress,
56
+ };
57
+ }
58
+ return { valid: true, signerAddress: authorAddress };
59
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@totemsdk/manifest",
3
+ "version": "0.1.0",
4
+ "description": "Canonical signed declaration format for Totem Edge entities — apps, agent capabilities, dApps, and edge services",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "dependencies": {
19
+ "@totemsdk/core": "1.0.9"
20
+ },
21
+ "peerDependencies": {
22
+ "@noble/hashes": ">=1.3.0 <2.0.0"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "@noble/hashes": {
26
+ "optional": false
27
+ }
28
+ },
29
+ "devDependencies": {
30
+ "@noble/hashes": "^1.3.0",
31
+ "@types/jest": "^29.0.0",
32
+ "@types/node": "^20.0.0",
33
+ "jest": "^29.0.0",
34
+ "ts-jest": "^29.0.0",
35
+ "typescript": "^5.0.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "license": "MIT",
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "clean": "rm -rf dist",
47
+ "test": "jest --passWithNoTests"
48
+ }
49
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * encoding.test.ts
3
+ * encode → decode round-trips for all four manifest types.
4
+ */
5
+
6
+ import { signManifest } from '../sign.js';
7
+ import { encodeManifest, decodeManifest } from '../encoding.js';
8
+ import { MANIFEST_VERSION, MANIFEST_TYPE_BYTE } from '../constants.js';
9
+ import type {
10
+ AppManifest,
11
+ CapabilityManifest,
12
+ DAppManifest,
13
+ EdgeServiceManifest,
14
+ } from '../types.js';
15
+
16
+ jest.setTimeout(60_000);
17
+
18
+ const TEST_SEED = new Uint8Array(32).fill(0x11);
19
+ const KEY_INDEX = 0;
20
+
21
+ let resolvedAddress: string;
22
+
23
+ beforeAll(async () => {
24
+ const tmp = await signManifest(
25
+ {
26
+ type: 'app',
27
+ appId: '',
28
+ name: 'init',
29
+ version: '1.0.0',
30
+ authorAddress: 'MxINIT',
31
+ pearTopicKey: 'a'.repeat(64),
32
+ price: '0',
33
+ category: [],
34
+ permissions: [],
35
+ description: '',
36
+ minTotemVersion: '0.1.0',
37
+ } satisfies AppManifest,
38
+ TEST_SEED,
39
+ KEY_INDEX,
40
+ );
41
+ resolvedAddress = tmp.authorAddress;
42
+ });
43
+
44
+ describe('encodeManifest / decodeManifest', () => {
45
+ it('round-trips AppManifest and preserves manifest type', async () => {
46
+ const manifest: AppManifest = {
47
+ type: 'app',
48
+ appId: 'abc123',
49
+ name: 'My App',
50
+ version: '1.0.0',
51
+ authorAddress: resolvedAddress,
52
+ pearTopicKey: 'f'.repeat(64),
53
+ price: '0',
54
+ category: ['finance'],
55
+ permissions: ['wallet:read-balance'],
56
+ description: 'Test',
57
+ minTotemVersion: '0.1.0',
58
+ };
59
+ const signed = await signManifest(manifest, TEST_SEED, KEY_INDEX);
60
+ const encoded = encodeManifest(signed);
61
+ const decoded = decodeManifest(encoded);
62
+
63
+ expect(decoded.manifest.type).toBe('app');
64
+ expect(decoded.manifest.type).toBe(signed.manifest.type);
65
+ expect((decoded.manifest as AppManifest).name).toBe('My App');
66
+ expect(decoded.signature).toBe(signed.signature);
67
+ });
68
+
69
+ it('round-trips CapabilityManifest', async () => {
70
+ const manifest: CapabilityManifest = {
71
+ type: 'capability',
72
+ capabilityId: 'cap1',
73
+ capabilityName: 'price-oracle',
74
+ agentAddress: resolvedAddress,
75
+ agentIdentityKey: 'e'.repeat(64),
76
+ description: 'Price oracle',
77
+ inputSchema: { type: 'object' },
78
+ outputSchema: { type: 'object' },
79
+ pricePerCall: '2',
80
+ expiresAt: Date.now() + 3600_000,
81
+ tags: ['oracle'],
82
+ };
83
+ const signed = await signManifest(manifest, TEST_SEED, KEY_INDEX);
84
+ const encoded = encodeManifest(signed);
85
+ const decoded = decodeManifest(encoded);
86
+
87
+ expect(decoded.manifest.type).toBe('capability');
88
+ expect((decoded.manifest as CapabilityManifest).capabilityName).toBe('price-oracle');
89
+ });
90
+
91
+ it('round-trips DAppManifest', async () => {
92
+ const manifest: DAppManifest = {
93
+ type: 'dapp',
94
+ dappId: 'dapp1',
95
+ name: 'Swap dApp',
96
+ version: '2.0.0',
97
+ authorAddress: resolvedAddress,
98
+ contractHash: 'd'.repeat(64),
99
+ abi: [],
100
+ price: '5',
101
+ category: ['defi'],
102
+ description: 'A swap contract',
103
+ };
104
+ const signed = await signManifest(manifest, TEST_SEED, KEY_INDEX);
105
+ const encoded = encodeManifest(signed);
106
+ const decoded = decodeManifest(encoded);
107
+
108
+ expect(decoded.manifest.type).toBe('dapp');
109
+ expect((decoded.manifest as DAppManifest).name).toBe('Swap dApp');
110
+ });
111
+
112
+ it('round-trips EdgeServiceManifest', async () => {
113
+ const manifest: EdgeServiceManifest = {
114
+ type: 'edge-service',
115
+ serviceId: 'edge1',
116
+ name: 'DHT Relay',
117
+ version: '1.0.0',
118
+ operatorAddress: resolvedAddress,
119
+ serviceType: 'omnia-router',
120
+ description: 'Omnia routing node',
121
+ capabilities: ['relay'],
122
+ tags: ['infrastructure'],
123
+ };
124
+ const signed = await signManifest(manifest, TEST_SEED, KEY_INDEX);
125
+ const encoded = encodeManifest(signed);
126
+ const decoded = decodeManifest(encoded);
127
+
128
+ expect(decoded.manifest.type).toBe('edge-service');
129
+ expect((decoded.manifest as EdgeServiceManifest).serviceType).toBe('omnia-router');
130
+ });
131
+
132
+ it('first byte is MANIFEST_VERSION', async () => {
133
+ const manifest: AppManifest = {
134
+ type: 'app',
135
+ appId: '',
136
+ name: 'v-test',
137
+ version: '1.0.0',
138
+ authorAddress: resolvedAddress,
139
+ pearTopicKey: 'a'.repeat(64),
140
+ price: '0',
141
+ category: [],
142
+ permissions: [],
143
+ description: '',
144
+ minTotemVersion: '0.1.0',
145
+ };
146
+ const signed = await signManifest(manifest, TEST_SEED, KEY_INDEX);
147
+ const encoded = encodeManifest(signed);
148
+ expect(encoded[0]).toBe(MANIFEST_VERSION);
149
+ });
150
+
151
+ it('second byte is correct type discriminant', async () => {
152
+ const appSigned = await signManifest(
153
+ { type: 'app', appId: '', name: 'x', version: '1.0.0', authorAddress: resolvedAddress, pearTopicKey: 'a'.repeat(64), price: '0', category: [], permissions: [], description: '', minTotemVersion: '0.1.0' } satisfies AppManifest,
154
+ TEST_SEED, KEY_INDEX,
155
+ );
156
+ expect(encodeManifest(appSigned)[1]).toBe(MANIFEST_TYPE_BYTE.app);
157
+
158
+ const capSigned = await signManifest(
159
+ { type: 'capability', capabilityId: '', capabilityName: 'x', agentAddress: resolvedAddress, agentIdentityKey: 'e'.repeat(64), description: '', inputSchema: {}, outputSchema: {}, pricePerCall: '1', expiresAt: 0, tags: [] } satisfies CapabilityManifest,
160
+ TEST_SEED, KEY_INDEX,
161
+ );
162
+ expect(encodeManifest(capSigned)[1]).toBe(MANIFEST_TYPE_BYTE.capability);
163
+
164
+ const edgeSigned = await signManifest(
165
+ { type: 'edge-service', serviceId: '', name: 'x', version: '1.0.0', operatorAddress: resolvedAddress, serviceType: 'sensor', description: '', capabilities: [], tags: [] } satisfies EdgeServiceManifest,
166
+ TEST_SEED, KEY_INDEX,
167
+ );
168
+ expect(encodeManifest(edgeSigned)[1]).toBe(MANIFEST_TYPE_BYTE['edge-service']);
169
+ });
170
+
171
+ it('throws on unknown type discriminant byte', () => {
172
+ const bad = new Uint8Array([1, 0xff, 0, 0, 0, 5, 104, 101, 108, 108, 111]);
173
+ expect(() => decodeManifest(bad)).toThrow(/unknown type discriminant/);
174
+ });
175
+
176
+ it('throws on buffer too short', () => {
177
+ expect(() => decodeManifest(new Uint8Array([1, 1, 0, 0]))).toThrow(/too short/);
178
+ });
179
+
180
+ it('throws on MANIFEST_VERSION mismatch', () => {
181
+ const bad = new Uint8Array(10).fill(0);
182
+ bad[0] = 255;
183
+ expect(() => decodeManifest(bad)).toThrow(/unsupported MANIFEST_VERSION/);
184
+ });
185
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * guards.test.ts
3
+ * Type guard tests for all four manifest kinds.
4
+ * Guards must work on both raw manifests and SignedManifest wrappers.
5
+ */
6
+
7
+ import {
8
+ isAppManifest,
9
+ isCapabilityManifest,
10
+ isDAppManifest,
11
+ isEdgeServiceManifest,
12
+ } from '../guards.js';
13
+ import type {
14
+ AppManifest,
15
+ CapabilityManifest,
16
+ DAppManifest,
17
+ EdgeServiceManifest,
18
+ SignedManifest,
19
+ } from '../types.js';
20
+
21
+ const APP: AppManifest = {
22
+ type: 'app',
23
+ appId: '',
24
+ name: 'Test',
25
+ version: '1.0.0',
26
+ authorAddress: 'MxAPP',
27
+ pearTopicKey: 'a'.repeat(64),
28
+ price: '0',
29
+ category: [],
30
+ permissions: [],
31
+ description: '',
32
+ minTotemVersion: '0.1.0',
33
+ };
34
+
35
+ const CAP: CapabilityManifest = {
36
+ type: 'capability',
37
+ capabilityId: '',
38
+ capabilityName: 'x',
39
+ agentAddress: 'MxCAP',
40
+ agentIdentityKey: 'b'.repeat(64),
41
+ description: '',
42
+ inputSchema: {},
43
+ outputSchema: {},
44
+ pricePerCall: '1',
45
+ expiresAt: 0,
46
+ tags: [],
47
+ };
48
+
49
+ const DAPP: DAppManifest = {
50
+ type: 'dapp',
51
+ dappId: '',
52
+ name: 'x',
53
+ version: '1.0.0',
54
+ authorAddress: 'MxDAPP',
55
+ contractHash: 'c'.repeat(64),
56
+ abi: [],
57
+ price: '0',
58
+ category: [],
59
+ description: '',
60
+ };
61
+
62
+ const EDGE: EdgeServiceManifest = {
63
+ type: 'edge-service',
64
+ serviceId: '',
65
+ name: 'x',
66
+ version: '1.0.0',
67
+ operatorAddress: 'MxEDGE',
68
+ serviceType: 'lookup-provider',
69
+ description: '',
70
+ capabilities: [],
71
+ tags: [],
72
+ };
73
+
74
+ function mockSigned<T extends { type: string }>(manifest: T): SignedManifest<T extends AppManifest ? AppManifest : T extends CapabilityManifest ? CapabilityManifest : T extends DAppManifest ? DAppManifest : EdgeServiceManifest> {
75
+ return {
76
+ manifest: manifest as any,
77
+ authorAddress: 'MxSIGNER',
78
+ signerPublicKey: 'aa'.repeat(544),
79
+ signedAt: Date.now(),
80
+ signature: 'bb'.repeat(100),
81
+ };
82
+ }
83
+
84
+ describe('isAppManifest', () => {
85
+ it('returns true for raw AppManifest', () => expect(isAppManifest(APP)).toBe(true));
86
+ it('returns true for SignedManifest<AppManifest>', () => expect(isAppManifest(mockSigned(APP))).toBe(true));
87
+ it('returns false for CapabilityManifest', () => expect(isAppManifest(CAP)).toBe(false));
88
+ it('returns false for DAppManifest', () => expect(isAppManifest(DAPP)).toBe(false));
89
+ it('returns false for EdgeServiceManifest', () => expect(isAppManifest(EDGE)).toBe(false));
90
+ it('returns false for null', () => expect(isAppManifest(null)).toBe(false));
91
+ it('returns false for undefined', () => expect(isAppManifest(undefined)).toBe(false));
92
+ it('returns false for SignedManifest<CapabilityManifest>', () => expect(isAppManifest(mockSigned(CAP))).toBe(false));
93
+ });
94
+
95
+ describe('isCapabilityManifest', () => {
96
+ it('returns true for raw CapabilityManifest', () => expect(isCapabilityManifest(CAP)).toBe(true));
97
+ it('returns true for SignedManifest<CapabilityManifest>', () => expect(isCapabilityManifest(mockSigned(CAP))).toBe(true));
98
+ it('returns false for AppManifest', () => expect(isCapabilityManifest(APP)).toBe(false));
99
+ it('returns false for DAppManifest', () => expect(isCapabilityManifest(DAPP)).toBe(false));
100
+ it('returns false for EdgeServiceManifest', () => expect(isCapabilityManifest(EDGE)).toBe(false));
101
+ it('returns false for null', () => expect(isCapabilityManifest(null)).toBe(false));
102
+ });
103
+
104
+ describe('isDAppManifest', () => {
105
+ it('returns true for raw DAppManifest', () => expect(isDAppManifest(DAPP)).toBe(true));
106
+ it('returns true for SignedManifest<DAppManifest>', () => expect(isDAppManifest(mockSigned(DAPP))).toBe(true));
107
+ it('returns false for AppManifest', () => expect(isDAppManifest(APP)).toBe(false));
108
+ it('returns false for CapabilityManifest', () => expect(isDAppManifest(CAP)).toBe(false));
109
+ it('returns false for EdgeServiceManifest', () => expect(isDAppManifest(EDGE)).toBe(false));
110
+ it('returns false for null', () => expect(isDAppManifest(null)).toBe(false));
111
+ });
112
+
113
+ describe('isEdgeServiceManifest', () => {
114
+ it('returns true for raw EdgeServiceManifest', () => expect(isEdgeServiceManifest(EDGE)).toBe(true));
115
+ it('returns true for SignedManifest<EdgeServiceManifest>', () => expect(isEdgeServiceManifest(mockSigned(EDGE))).toBe(true));
116
+ it('returns false for AppManifest', () => expect(isEdgeServiceManifest(APP)).toBe(false));
117
+ it('returns false for CapabilityManifest', () => expect(isEdgeServiceManifest(CAP)).toBe(false));
118
+ it('returns false for DAppManifest', () => expect(isEdgeServiceManifest(DAPP)).toBe(false));
119
+ it('returns false for null', () => expect(isEdgeServiceManifest(null)).toBe(false));
120
+ });
121
+
122
+ describe('cross-type guard correctness', () => {
123
+ const allManifests = [APP, CAP, DAPP, EDGE];
124
+ const guards = [isAppManifest, isCapabilityManifest, isDAppManifest, isEdgeServiceManifest];
125
+
126
+ it('each manifest is identified by exactly one guard', () => {
127
+ for (const m of allManifests) {
128
+ const trueCount = guards.filter(g => g(m)).length;
129
+ expect(trueCount).toBe(1);
130
+ }
131
+ });
132
+
133
+ it('each signed manifest is identified by exactly one guard', () => {
134
+ for (const m of allManifests) {
135
+ const signed = mockSigned(m);
136
+ const trueCount = guards.filter(g => g(signed)).length;
137
+ expect(trueCount).toBe(1);
138
+ }
139
+ });
140
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * id.test.ts
3
+ * Stable IDs: same inputs → same ID; version change → same ID; key field change → different ID.
4
+ */
5
+
6
+ import { computeManifestId } from '../id.js';
7
+ import type {
8
+ AppManifest,
9
+ CapabilityManifest,
10
+ DAppManifest,
11
+ EdgeServiceManifest,
12
+ } from '../types.js';
13
+
14
+ const APP: AppManifest = {
15
+ type: 'app',
16
+ appId: '',
17
+ name: 'Test App',
18
+ version: '1.0.0',
19
+ authorAddress: 'MxAUTHOR1111',
20
+ pearTopicKey: 'aaaa1111',
21
+ price: '0',
22
+ category: ['finance'],
23
+ permissions: ['wallet:read-balance'],
24
+ description: 'Test',
25
+ minTotemVersion: '0.1.0',
26
+ };
27
+
28
+ const CAP: CapabilityManifest = {
29
+ type: 'capability',
30
+ capabilityId: '',
31
+ capabilityName: 'invoice-translate',
32
+ agentAddress: 'MxAGENT2222',
33
+ agentIdentityKey: 'bbbb2222',
34
+ description: 'Translates',
35
+ inputSchema: {},
36
+ outputSchema: {},
37
+ pricePerCall: '1',
38
+ expiresAt: 9999,
39
+ tags: [],
40
+ };
41
+
42
+ const DAPP: DAppManifest = {
43
+ type: 'dapp',
44
+ dappId: '',
45
+ name: 'Swap',
46
+ version: '1.0.0',
47
+ authorAddress: 'MxAUTHOR3333',
48
+ contractHash: 'cccc3333',
49
+ abi: [],
50
+ price: '0',
51
+ category: [],
52
+ description: '',
53
+ };
54
+
55
+ const EDGE: EdgeServiceManifest = {
56
+ type: 'edge-service',
57
+ serviceId: '',
58
+ name: 'My Sensor',
59
+ version: '1.0.0',
60
+ operatorAddress: 'MxOPERATOR4444',
61
+ serviceType: 'sensor',
62
+ description: '',
63
+ capabilities: [],
64
+ tags: [],
65
+ };
66
+
67
+ describe('computeManifestId — stable IDs', () => {
68
+ it('returns the same ID for identical AppManifest inputs', () => {
69
+ expect(computeManifestId(APP)).toBe(computeManifestId({ ...APP }));
70
+ });
71
+
72
+ it('AppManifest: ID is stable when version changes', () => {
73
+ expect(computeManifestId(APP)).toBe(computeManifestId({ ...APP, version: '2.0.0' }));
74
+ });
75
+
76
+ it('AppManifest: ID changes when authorAddress changes', () => {
77
+ expect(computeManifestId(APP)).not.toBe(computeManifestId({ ...APP, authorAddress: 'MxOTHER' }));
78
+ });
79
+
80
+ it('AppManifest: ID changes when pearTopicKey changes', () => {
81
+ expect(computeManifestId(APP)).not.toBe(computeManifestId({ ...APP, pearTopicKey: 'bbbb' }));
82
+ });
83
+
84
+ it('returns the same ID for identical CapabilityManifest inputs', () => {
85
+ expect(computeManifestId(CAP)).toBe(computeManifestId({ ...CAP }));
86
+ });
87
+
88
+ it('CapabilityManifest: ID is stable when expiresAt changes', () => {
89
+ expect(computeManifestId(CAP)).toBe(computeManifestId({ ...CAP, expiresAt: 0 }));
90
+ });
91
+
92
+ it('CapabilityManifest: ID changes when agentAddress changes', () => {
93
+ expect(computeManifestId(CAP)).not.toBe(computeManifestId({ ...CAP, agentAddress: 'MxOTHER' }));
94
+ });
95
+
96
+ it('CapabilityManifest: ID changes when capabilityName changes', () => {
97
+ expect(computeManifestId(CAP)).not.toBe(computeManifestId({ ...CAP, capabilityName: 'other-capability' }));
98
+ });
99
+
100
+ it('returns the same ID for identical DAppManifest inputs', () => {
101
+ expect(computeManifestId(DAPP)).toBe(computeManifestId({ ...DAPP }));
102
+ });
103
+
104
+ it('DAppManifest: ID is stable when version changes', () => {
105
+ expect(computeManifestId(DAPP)).toBe(computeManifestId({ ...DAPP, version: '9.9.9' }));
106
+ });
107
+
108
+ it('DAppManifest: ID changes when contractHash changes', () => {
109
+ expect(computeManifestId(DAPP)).not.toBe(computeManifestId({ ...DAPP, contractHash: 'dddd' }));
110
+ });
111
+
112
+ it('returns the same ID for identical EdgeServiceManifest inputs', () => {
113
+ expect(computeManifestId(EDGE)).toBe(computeManifestId({ ...EDGE }));
114
+ });
115
+
116
+ it('EdgeServiceManifest: ID is stable when version changes', () => {
117
+ expect(computeManifestId(EDGE)).toBe(computeManifestId({ ...EDGE, version: '9.0.0' }));
118
+ });
119
+
120
+ it('EdgeServiceManifest: ID changes when serviceType changes', () => {
121
+ expect(computeManifestId(EDGE)).not.toBe(computeManifestId({ ...EDGE, serviceType: 'robot' }));
122
+ });
123
+
124
+ it('EdgeServiceManifest: ID changes when name changes', () => {
125
+ expect(computeManifestId(EDGE)).not.toBe(computeManifestId({ ...EDGE, name: 'Other Sensor' }));
126
+ });
127
+
128
+ it('IDs are 64-character hex strings (SHA3-256)', () => {
129
+ const id = computeManifestId(APP);
130
+ expect(id).toMatch(/^[0-9a-f]{64}$/);
131
+ });
132
+
133
+ it('IDs differ across manifest types', () => {
134
+ const ids = [
135
+ computeManifestId(APP),
136
+ computeManifestId(CAP),
137
+ computeManifestId(DAPP),
138
+ computeManifestId(EDGE),
139
+ ];
140
+ const unique = new Set(ids);
141
+ expect(unique.size).toBe(4);
142
+ });
143
+ });