@unrdf/kgc-probe 26.4.2
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 +414 -0
- package/package.json +81 -0
- package/src/agents/index.mjs +1402 -0
- package/src/artifact.mjs +405 -0
- package/src/cli.mjs +932 -0
- package/src/config.mjs +115 -0
- package/src/guards.mjs +1213 -0
- package/src/index.mjs +347 -0
- package/src/merge.mjs +196 -0
- package/src/observation.mjs +193 -0
- package/src/orchestrator.mjs +315 -0
- package/src/probe.mjs +58 -0
- package/src/probes/CONCURRENCY-PROBE.md +256 -0
- package/src/probes/README.md +275 -0
- package/src/probes/concurrency.mjs +1175 -0
- package/src/probes/filesystem.mjs +731 -0
- package/src/probes/filesystem.test.mjs +244 -0
- package/src/probes/network.mjs +503 -0
- package/src/probes/performance.mjs +816 -0
- package/src/probes/persistence.mjs +785 -0
- package/src/probes/runtime.mjs +589 -0
- package/src/probes/tooling.mjs +454 -0
- package/src/probes/tooling.test.mjs +372 -0
- package/src/probes/verify-execution.mjs +131 -0
- package/src/probes/verify-guards.mjs +73 -0
- package/src/probes/wasm.mjs +715 -0
- package/src/receipt.mjs +197 -0
- package/src/receipts/index.mjs +813 -0
- package/src/reporter.example.mjs +223 -0
- package/src/reporter.mjs +555 -0
- package/src/reporters/markdown.mjs +355 -0
- package/src/reporters/rdf.mjs +383 -0
- package/src/storage/index.mjs +827 -0
- package/src/types.mjs +1028 -0
- package/src/utils/errors.mjs +397 -0
- package/src/utils/index.mjs +32 -0
- package/src/utils/logger.mjs +236 -0
- package/src/vocabulary.ttl +169 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview KGC Probe - Receipt System Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographic receipts for probe observations and merge operations:
|
|
5
|
+
* - ObservationReceipt: Per-agent observation hashing with chain
|
|
6
|
+
* - MergeReceipt: Merkle tree of shard hashes
|
|
7
|
+
* - VerificationReceipt: Confidence scoring and verification status
|
|
8
|
+
*
|
|
9
|
+
* Integrates with @unrdf/v6-core BaseReceipt system.
|
|
10
|
+
*
|
|
11
|
+
* @module @unrdf/kgc-probe/receipts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { randomUUID, createHash } from 'crypto';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CONSTANTS
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/** Hash algorithm used for receipts */
|
|
21
|
+
export const HASH_ALGORITHM = 'sha256';
|
|
22
|
+
|
|
23
|
+
/** Receipt version */
|
|
24
|
+
export const RECEIPT_VERSION = '1.0.0';
|
|
25
|
+
|
|
26
|
+
/** Receipt types */
|
|
27
|
+
export const RECEIPT_TYPES = Object.freeze({
|
|
28
|
+
OBSERVATION: 'probe:observation',
|
|
29
|
+
MERGE: 'probe:merge',
|
|
30
|
+
VERIFICATION: 'probe:verification'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// HASH UTILITIES
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic serialization of object
|
|
39
|
+
* @param {any} obj - Object to serialize
|
|
40
|
+
* @returns {string} Deterministic JSON string
|
|
41
|
+
*/
|
|
42
|
+
export function deterministicSerialize(obj) {
|
|
43
|
+
if (obj === null || obj === undefined) {
|
|
44
|
+
return 'null';
|
|
45
|
+
}
|
|
46
|
+
if (typeof obj === 'bigint') {
|
|
47
|
+
return obj.toString();
|
|
48
|
+
}
|
|
49
|
+
if (typeof obj !== 'object') {
|
|
50
|
+
return JSON.stringify(obj);
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(obj)) {
|
|
53
|
+
return '[' + obj.map(deterministicSerialize).join(',') + ']';
|
|
54
|
+
}
|
|
55
|
+
const keys = Object.keys(obj).sort();
|
|
56
|
+
const pairs = keys.map(k => `${JSON.stringify(k)}:${deterministicSerialize(obj[k])}`);
|
|
57
|
+
return '{' + pairs.join(',') + '}';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compute deterministic hash of data
|
|
62
|
+
* @param {any} data - Data to hash
|
|
63
|
+
* @returns {string} Hex-encoded hash
|
|
64
|
+
*/
|
|
65
|
+
export function computeHash(data) {
|
|
66
|
+
const serialized = typeof data === 'string'
|
|
67
|
+
? data
|
|
68
|
+
: deterministicSerialize(data);
|
|
69
|
+
return createHash(HASH_ALGORITHM).update(serialized).digest('hex');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compute chained hash (previousHash + currentHash)
|
|
74
|
+
* @param {string|null} previousHash - Previous hash in chain (null for genesis)
|
|
75
|
+
* @param {string} currentHash - Current payload hash
|
|
76
|
+
* @returns {string} Chained hash
|
|
77
|
+
*/
|
|
78
|
+
export function computeChainHash(previousHash, currentHash) {
|
|
79
|
+
const input = `${previousHash || 'GENESIS'}:${currentHash}`;
|
|
80
|
+
return createHash(HASH_ALGORITHM).update(input).digest('hex');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// OBSERVATION RECEIPT
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {Object} ObservationReceipt
|
|
89
|
+
* @property {string} id - Receipt UUID
|
|
90
|
+
* @property {string} receiptType - Receipt type discriminator
|
|
91
|
+
* @property {string} agentId - Agent that produced observation
|
|
92
|
+
* @property {number} observationIndex - Sequence number in agent chain
|
|
93
|
+
* @property {string} domain - Observation domain
|
|
94
|
+
* @property {string} obsHash - Hash of observation payload
|
|
95
|
+
* @property {string|null} prevHash - Previous observation hash (chain)
|
|
96
|
+
* @property {bigint} timestamp_ns - Nanosecond timestamp
|
|
97
|
+
* @property {string} timestamp_iso - ISO timestamp
|
|
98
|
+
* @property {string} payloadHash - Hash of receipt payload
|
|
99
|
+
* @property {string|null} previousHash - Previous receipt hash
|
|
100
|
+
* @property {string} receiptHash - Chained receipt hash
|
|
101
|
+
* @property {Object} observation - Original observation
|
|
102
|
+
* @property {Object[]} checks - Determinism checks performed
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create an observation receipt
|
|
107
|
+
* @param {Object} params - Receipt parameters
|
|
108
|
+
* @param {string} params.agentId - Agent identifier
|
|
109
|
+
* @param {number} params.observationIndex - Sequence number
|
|
110
|
+
* @param {Object} params.payload - Observation payload
|
|
111
|
+
* @param {string} params.domain - Observation domain
|
|
112
|
+
* @param {ObservationReceipt|null} [params.previousReceipt] - Previous receipt for chaining
|
|
113
|
+
* @returns {ObservationReceipt} Created receipt
|
|
114
|
+
*/
|
|
115
|
+
export function createObservationReceipt({
|
|
116
|
+
agentId,
|
|
117
|
+
observationIndex,
|
|
118
|
+
payload,
|
|
119
|
+
domain,
|
|
120
|
+
previousReceipt = null
|
|
121
|
+
}) {
|
|
122
|
+
if (!agentId) throw new Error('agentId is required');
|
|
123
|
+
if (observationIndex < 1) throw new Error('observationIndex must be >= 1');
|
|
124
|
+
if (!payload) throw new Error('payload is required');
|
|
125
|
+
if (!domain) throw new Error('domain is required');
|
|
126
|
+
|
|
127
|
+
const id = randomUUID();
|
|
128
|
+
const now = process.hrtime.bigint();
|
|
129
|
+
const timestamp_iso = new Date().toISOString();
|
|
130
|
+
|
|
131
|
+
// Compute observation hash
|
|
132
|
+
const obsHash = computeHash(payload);
|
|
133
|
+
|
|
134
|
+
// Previous hash in agent's chain
|
|
135
|
+
const prevHash = previousReceipt ? previousReceipt.obsHash : null;
|
|
136
|
+
|
|
137
|
+
// Determinism checks
|
|
138
|
+
const checks = [
|
|
139
|
+
{
|
|
140
|
+
checkType: 'hash_recompute',
|
|
141
|
+
passed: computeHash(payload) === obsHash,
|
|
142
|
+
details: 'Hash recomputation matches'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
checkType: 'serialization_stable',
|
|
146
|
+
passed: deterministicSerialize(payload) === deterministicSerialize(payload),
|
|
147
|
+
details: 'Serialization is stable'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
checkType: 'payload_integrity',
|
|
151
|
+
passed: typeof payload === 'object' && payload !== null,
|
|
152
|
+
details: 'Payload is valid object'
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
// Ensure all checks pass
|
|
157
|
+
const allPassed = checks.every(c => c.passed);
|
|
158
|
+
if (!allPassed) {
|
|
159
|
+
const failed = checks.filter(c => !c.passed).map(c => c.checkType);
|
|
160
|
+
throw new Error(`Determinism checks failed: ${failed.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Build receipt payload
|
|
164
|
+
const receiptPayload = {
|
|
165
|
+
id,
|
|
166
|
+
receiptType: RECEIPT_TYPES.OBSERVATION,
|
|
167
|
+
agentId,
|
|
168
|
+
observationIndex,
|
|
169
|
+
domain,
|
|
170
|
+
obsHash,
|
|
171
|
+
prevHash,
|
|
172
|
+
timestamp_ns: now,
|
|
173
|
+
timestamp_iso,
|
|
174
|
+
observation: {
|
|
175
|
+
payload,
|
|
176
|
+
timestamp: now,
|
|
177
|
+
hash: obsHash,
|
|
178
|
+
metadata: {
|
|
179
|
+
serializationVersion: RECEIPT_VERSION,
|
|
180
|
+
encoding: 'utf-8',
|
|
181
|
+
deterministic: true
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
checks
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Compute hashes
|
|
188
|
+
const payloadHash = computeHash(receiptPayload);
|
|
189
|
+
const previousReceiptHash = previousReceipt ? previousReceipt.receiptHash : null;
|
|
190
|
+
const receiptHash = computeChainHash(previousReceiptHash, payloadHash);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...receiptPayload,
|
|
194
|
+
payloadHash,
|
|
195
|
+
previousHash: previousReceiptHash,
|
|
196
|
+
receiptHash
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// MERGE RECEIPT
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @typedef {Object} ShardInfo
|
|
206
|
+
* @property {string} agentId - Agent ID
|
|
207
|
+
* @property {string} chainFinalHash - Final hash in agent's chain
|
|
208
|
+
* @property {number} obsCount - Number of observations
|
|
209
|
+
* @property {string} domain - Agent domain
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @typedef {Object} MergeReceipt
|
|
214
|
+
* @property {string} id - Receipt UUID
|
|
215
|
+
* @property {string} receiptType - Receipt type
|
|
216
|
+
* @property {string} mergeId - Merge operation ID
|
|
217
|
+
* @property {ShardInfo[]} shards - Merged shard information
|
|
218
|
+
* @property {string} merkleRoot - Merkle tree root hash
|
|
219
|
+
* @property {string[]} proofPath - Merkle proof path
|
|
220
|
+
* @property {Object} mergeAlgorithm - Algorithm metadata
|
|
221
|
+
* @property {Object[]|null} conflicts - Detected conflicts
|
|
222
|
+
* @property {bigint} timestamp_ns - Nanosecond timestamp
|
|
223
|
+
* @property {string} timestamp_iso - ISO timestamp
|
|
224
|
+
* @property {string} payloadHash - Payload hash
|
|
225
|
+
* @property {string|null} previousHash - Always null for merge
|
|
226
|
+
* @property {string} receiptHash - Receipt hash
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build merkle tree from shard hashes
|
|
231
|
+
* @param {ShardInfo[]} shards - Shard information
|
|
232
|
+
* @returns {{root: string, proofPath: string[]}} Merkle root and proof
|
|
233
|
+
*/
|
|
234
|
+
export function buildMerkleTree(shards) {
|
|
235
|
+
if (shards.length === 0) {
|
|
236
|
+
return { root: computeHash('EMPTY'), proofPath: [] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort shards by agentId for determinism
|
|
240
|
+
const sorted = [...shards].sort((a, b) => a.agentId.localeCompare(b.agentId));
|
|
241
|
+
|
|
242
|
+
// Build leaf hashes
|
|
243
|
+
const leaves = sorted.map(s => computeHash(`${s.agentId}:${s.chainFinalHash}`));
|
|
244
|
+
const proofPath = [...leaves];
|
|
245
|
+
|
|
246
|
+
// Build tree bottom-up
|
|
247
|
+
let level = leaves;
|
|
248
|
+
while (level.length > 1) {
|
|
249
|
+
const nextLevel = [];
|
|
250
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
251
|
+
const left = level[i];
|
|
252
|
+
const right = level[i + 1] || left; // Duplicate if odd
|
|
253
|
+
nextLevel.push(computeHash(left + right));
|
|
254
|
+
}
|
|
255
|
+
level = nextLevel;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { root: level[0], proofPath };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Verify merkle root
|
|
263
|
+
* @param {ShardInfo[]} shards - Shard information
|
|
264
|
+
* @param {string} expectedRoot - Expected merkle root
|
|
265
|
+
* @returns {boolean} True if root matches
|
|
266
|
+
*/
|
|
267
|
+
export function verifyMerkleRoot(shards, expectedRoot) {
|
|
268
|
+
const { root } = buildMerkleTree(shards);
|
|
269
|
+
return root === expectedRoot;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create a merge receipt
|
|
274
|
+
* @param {Object} params - Receipt parameters
|
|
275
|
+
* @param {string} params.mergeId - Unique merge operation ID
|
|
276
|
+
* @param {ShardInfo[]} params.shards - Shard information
|
|
277
|
+
* @param {Object[]|null} [params.conflicts] - Detected conflicts
|
|
278
|
+
* @param {string} [params.orchestratorId] - Orchestrator ID
|
|
279
|
+
* @returns {MergeReceipt} Created receipt
|
|
280
|
+
*/
|
|
281
|
+
export function createMergeReceipt({
|
|
282
|
+
mergeId,
|
|
283
|
+
shards,
|
|
284
|
+
conflicts = null,
|
|
285
|
+
orchestratorId = 'default'
|
|
286
|
+
}) {
|
|
287
|
+
if (!mergeId) throw new Error('mergeId is required');
|
|
288
|
+
if (!shards || shards.length === 0) throw new Error('shards is required (non-empty)');
|
|
289
|
+
|
|
290
|
+
const id = randomUUID();
|
|
291
|
+
const now = process.hrtime.bigint();
|
|
292
|
+
const timestamp_iso = new Date().toISOString();
|
|
293
|
+
|
|
294
|
+
// Build merkle tree
|
|
295
|
+
const { root: merkleRoot, proofPath } = buildMerkleTree(shards);
|
|
296
|
+
|
|
297
|
+
// Build receipt payload
|
|
298
|
+
const receiptPayload = {
|
|
299
|
+
id,
|
|
300
|
+
receiptType: RECEIPT_TYPES.MERGE,
|
|
301
|
+
mergeId,
|
|
302
|
+
shards,
|
|
303
|
+
merkleRoot,
|
|
304
|
+
proofPath,
|
|
305
|
+
mergeAlgorithm: {
|
|
306
|
+
algorithm: 'lww-deterministic',
|
|
307
|
+
version: '1.0.0',
|
|
308
|
+
parameters: {
|
|
309
|
+
leafOrder: 'agentId-ascending',
|
|
310
|
+
hashFunction: HASH_ALGORITHM
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
conflicts,
|
|
314
|
+
timestamp_ns: now,
|
|
315
|
+
timestamp_iso,
|
|
316
|
+
orchestratorId,
|
|
317
|
+
mergedAt: timestamp_iso
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Compute hashes
|
|
321
|
+
const payloadHash = computeHash(receiptPayload);
|
|
322
|
+
const receiptHash = computeChainHash(null, payloadHash);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
...receiptPayload,
|
|
326
|
+
payloadHash,
|
|
327
|
+
previousHash: null,
|
|
328
|
+
receiptHash
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// VERIFICATION RECEIPT
|
|
334
|
+
// ============================================================================
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @typedef {Object} VerificationCheck
|
|
338
|
+
* @property {string} checkType - Type of verification check
|
|
339
|
+
* @property {boolean} passed - Whether check passed
|
|
340
|
+
* @property {string} details - Check details
|
|
341
|
+
* @property {number} weight - Weight in confidence calculation
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @typedef {Object} CertificateStep
|
|
346
|
+
* @property {string} step - Step name
|
|
347
|
+
* @property {string} status - Step status
|
|
348
|
+
* @property {string} hash - Step hash
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @typedef {Object} VerificationReceipt
|
|
353
|
+
* @property {string} id - Receipt UUID
|
|
354
|
+
* @property {string} receiptType - Receipt type
|
|
355
|
+
* @property {string} verificationId - Verification operation ID
|
|
356
|
+
* @property {string} mergeReceiptHash - Hash of merge receipt verified
|
|
357
|
+
* @property {VerificationCheck[]} verifications - Checks performed
|
|
358
|
+
* @property {boolean} deterministic - Overall determinism result
|
|
359
|
+
* @property {boolean} conflictFree - Whether conflicts were found
|
|
360
|
+
* @property {CertificateStep[]} certificateChain - Certificate chain
|
|
361
|
+
* @property {number} obsCount - Observations verified
|
|
362
|
+
* @property {number} agentCount - Agents verified
|
|
363
|
+
* @property {number} confidenceScore - Confidence score [0, 1]
|
|
364
|
+
* @property {bigint} timestamp_ns - Nanosecond timestamp
|
|
365
|
+
* @property {string} timestamp_iso - ISO timestamp
|
|
366
|
+
* @property {string} payloadHash - Payload hash
|
|
367
|
+
* @property {string|null} previousHash - Always null for verification
|
|
368
|
+
* @property {string} receiptHash - Receipt hash
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Calculate confidence score from verification checks
|
|
373
|
+
* @param {VerificationCheck[]} checks - Verification checks
|
|
374
|
+
* @returns {number} Confidence score [0, 1]
|
|
375
|
+
*/
|
|
376
|
+
export function calculateConfidenceScore(checks) {
|
|
377
|
+
if (checks.length === 0) return 0;
|
|
378
|
+
|
|
379
|
+
let totalWeight = 0;
|
|
380
|
+
let passedWeight = 0;
|
|
381
|
+
|
|
382
|
+
for (const check of checks) {
|
|
383
|
+
const weight = check.weight ?? 1;
|
|
384
|
+
totalWeight += weight;
|
|
385
|
+
if (check.passed) {
|
|
386
|
+
passedWeight += weight;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return totalWeight > 0 ? passedWeight / totalWeight : 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get failed checks from verification
|
|
395
|
+
* @param {VerificationCheck[]} checks - Verification checks
|
|
396
|
+
* @returns {VerificationCheck[]} Failed checks
|
|
397
|
+
*/
|
|
398
|
+
export function getFailedChecks(checks) {
|
|
399
|
+
return checks.filter(c => !c.passed);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Summarize verification result
|
|
404
|
+
* @param {VerificationReceipt} receipt - Verification receipt
|
|
405
|
+
* @returns {Object} Summary
|
|
406
|
+
*/
|
|
407
|
+
export function summarizeVerification(receipt) {
|
|
408
|
+
const failed = getFailedChecks(receipt.verifications);
|
|
409
|
+
return {
|
|
410
|
+
verificationId: receipt.verificationId,
|
|
411
|
+
passed: failed.length === 0,
|
|
412
|
+
deterministic: receipt.deterministic,
|
|
413
|
+
conflictFree: receipt.conflictFree,
|
|
414
|
+
confidenceScore: receipt.confidenceScore,
|
|
415
|
+
totalChecks: receipt.verifications.length,
|
|
416
|
+
failedChecks: failed.length,
|
|
417
|
+
obsCount: receipt.obsCount,
|
|
418
|
+
agentCount: receipt.agentCount
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Create a verification receipt
|
|
424
|
+
* @param {Object} params - Receipt parameters
|
|
425
|
+
* @param {string} params.verificationId - Verification ID
|
|
426
|
+
* @param {string} params.mergeReceiptHash - Hash of merge receipt
|
|
427
|
+
* @param {VerificationCheck[]} params.verifications - Checks performed
|
|
428
|
+
* @param {boolean} params.deterministic - Determinism result
|
|
429
|
+
* @param {boolean} params.conflictFree - Conflict-free result
|
|
430
|
+
* @param {CertificateStep[]} params.certificateChain - Certificate chain
|
|
431
|
+
* @param {number} params.obsCount - Observations verified
|
|
432
|
+
* @param {number} params.agentCount - Agents verified
|
|
433
|
+
* @param {string} [params.verifierId] - Verifier ID
|
|
434
|
+
* @returns {VerificationReceipt} Created receipt
|
|
435
|
+
*/
|
|
436
|
+
export function createVerificationReceipt({
|
|
437
|
+
verificationId,
|
|
438
|
+
mergeReceiptHash,
|
|
439
|
+
verifications,
|
|
440
|
+
deterministic,
|
|
441
|
+
conflictFree,
|
|
442
|
+
certificateChain,
|
|
443
|
+
obsCount,
|
|
444
|
+
agentCount,
|
|
445
|
+
verifierId = 'default'
|
|
446
|
+
}) {
|
|
447
|
+
if (!verificationId) throw new Error('verificationId is required');
|
|
448
|
+
if (!mergeReceiptHash) throw new Error('mergeReceiptHash is required');
|
|
449
|
+
if (!Array.isArray(verifications)) throw new Error('verifications array is required');
|
|
450
|
+
if (!Array.isArray(certificateChain)) throw new Error('certificateChain array is required');
|
|
451
|
+
|
|
452
|
+
const id = randomUUID();
|
|
453
|
+
const now = process.hrtime.bigint();
|
|
454
|
+
const timestamp_iso = new Date().toISOString();
|
|
455
|
+
|
|
456
|
+
// Calculate confidence score
|
|
457
|
+
const confidenceScore = calculateConfidenceScore(verifications);
|
|
458
|
+
|
|
459
|
+
// Build receipt payload
|
|
460
|
+
const receiptPayload = {
|
|
461
|
+
id,
|
|
462
|
+
receiptType: RECEIPT_TYPES.VERIFICATION,
|
|
463
|
+
verificationId,
|
|
464
|
+
mergeReceiptHash,
|
|
465
|
+
verifications,
|
|
466
|
+
deterministic,
|
|
467
|
+
conflictFree,
|
|
468
|
+
certificateChain,
|
|
469
|
+
obsCount,
|
|
470
|
+
agentCount,
|
|
471
|
+
confidenceScore,
|
|
472
|
+
timestamp_ns: now,
|
|
473
|
+
timestamp_iso,
|
|
474
|
+
verifierId,
|
|
475
|
+
verifiedAt: timestamp_iso
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// Compute hashes
|
|
479
|
+
const payloadHash = computeHash(receiptPayload);
|
|
480
|
+
const receiptHash = computeChainHash(null, payloadHash);
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
...receiptPayload,
|
|
484
|
+
payloadHash,
|
|
485
|
+
previousHash: null,
|
|
486
|
+
receiptHash
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ============================================================================
|
|
491
|
+
// VERIFICATION FUNCTIONS
|
|
492
|
+
// ============================================================================
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Verify an observation receipt
|
|
496
|
+
* @param {ObservationReceipt} receipt - Receipt to verify
|
|
497
|
+
* @param {ObservationReceipt|null} [previousReceipt] - Previous receipt for chain verification
|
|
498
|
+
* @returns {{valid: boolean, errors: string[], checks: Object}}
|
|
499
|
+
*/
|
|
500
|
+
export function verifyObservationReceipt(receipt, previousReceipt = null) {
|
|
501
|
+
const errors = [];
|
|
502
|
+
const checks = {
|
|
503
|
+
hasRequiredFields: true,
|
|
504
|
+
obsHashValid: true,
|
|
505
|
+
chainValid: true,
|
|
506
|
+
receiptHashValid: true
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Check required fields
|
|
510
|
+
const requiredFields = ['id', 'agentId', 'obsHash', 'payloadHash', 'receiptHash'];
|
|
511
|
+
for (const field of requiredFields) {
|
|
512
|
+
if (!receipt[field]) {
|
|
513
|
+
checks.hasRequiredFields = false;
|
|
514
|
+
errors.push(`Missing required field: ${field}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Verify observation hash
|
|
519
|
+
if (receipt.observation?.payload) {
|
|
520
|
+
const expectedObsHash = computeHash(receipt.observation.payload);
|
|
521
|
+
if (expectedObsHash !== receipt.obsHash) {
|
|
522
|
+
checks.obsHashValid = false;
|
|
523
|
+
errors.push('Observation hash mismatch');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Verify chain
|
|
528
|
+
if (previousReceipt) {
|
|
529
|
+
if (receipt.prevHash !== previousReceipt.obsHash) {
|
|
530
|
+
checks.chainValid = false;
|
|
531
|
+
errors.push('Previous observation hash mismatch');
|
|
532
|
+
}
|
|
533
|
+
if (receipt.previousHash !== previousReceipt.receiptHash) {
|
|
534
|
+
checks.chainValid = false;
|
|
535
|
+
errors.push('Previous receipt hash mismatch');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Verify receipt hash
|
|
540
|
+
const expectedChainHash = computeChainHash(receipt.previousHash, receipt.payloadHash);
|
|
541
|
+
if (expectedChainHash !== receipt.receiptHash) {
|
|
542
|
+
checks.receiptHashValid = false;
|
|
543
|
+
errors.push('Receipt hash mismatch');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
valid: errors.length === 0,
|
|
548
|
+
errors,
|
|
549
|
+
checks
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Verify a merge receipt
|
|
555
|
+
* @param {MergeReceipt} receipt - Receipt to verify
|
|
556
|
+
* @returns {{valid: boolean, errors: string[], checks: Object}}
|
|
557
|
+
*/
|
|
558
|
+
export function verifyMergeReceipt(receipt) {
|
|
559
|
+
const errors = [];
|
|
560
|
+
const checks = {
|
|
561
|
+
hasRequiredFields: true,
|
|
562
|
+
merkleRootValid: true,
|
|
563
|
+
receiptHashValid: true
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// Check required fields
|
|
567
|
+
const requiredFields = ['id', 'mergeId', 'shards', 'merkleRoot', 'payloadHash', 'receiptHash'];
|
|
568
|
+
for (const field of requiredFields) {
|
|
569
|
+
if (!receipt[field]) {
|
|
570
|
+
checks.hasRequiredFields = false;
|
|
571
|
+
errors.push(`Missing required field: ${field}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Verify merkle root
|
|
576
|
+
if (receipt.shards) {
|
|
577
|
+
const expectedRoot = buildMerkleTree(receipt.shards).root;
|
|
578
|
+
if (expectedRoot !== receipt.merkleRoot) {
|
|
579
|
+
checks.merkleRootValid = false;
|
|
580
|
+
errors.push('Merkle root mismatch');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Verify receipt hash
|
|
585
|
+
const expectedChainHash = computeChainHash(null, receipt.payloadHash);
|
|
586
|
+
if (expectedChainHash !== receipt.receiptHash) {
|
|
587
|
+
checks.receiptHashValid = false;
|
|
588
|
+
errors.push('Receipt hash mismatch');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
valid: errors.length === 0,
|
|
593
|
+
errors,
|
|
594
|
+
checks
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Verify a verification receipt
|
|
600
|
+
* @param {VerificationReceipt} receipt - Receipt to verify
|
|
601
|
+
* @returns {{valid: boolean, errors: string[], checks: Object}}
|
|
602
|
+
*/
|
|
603
|
+
export function verifyVerificationReceipt(receipt) {
|
|
604
|
+
const errors = [];
|
|
605
|
+
const checks = {
|
|
606
|
+
hasRequiredFields: true,
|
|
607
|
+
confidenceValid: true,
|
|
608
|
+
receiptHashValid: true
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// Check required fields
|
|
612
|
+
const requiredFields = ['id', 'verificationId', 'mergeReceiptHash', 'payloadHash', 'receiptHash'];
|
|
613
|
+
for (const field of requiredFields) {
|
|
614
|
+
if (!receipt[field]) {
|
|
615
|
+
checks.hasRequiredFields = false;
|
|
616
|
+
errors.push(`Missing required field: ${field}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Verify confidence calculation
|
|
621
|
+
if (receipt.verifications) {
|
|
622
|
+
const expectedConfidence = calculateConfidenceScore(receipt.verifications);
|
|
623
|
+
if (Math.abs(expectedConfidence - receipt.confidenceScore) > 0.001) {
|
|
624
|
+
checks.confidenceValid = false;
|
|
625
|
+
errors.push('Confidence score mismatch');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Verify receipt hash
|
|
630
|
+
const expectedChainHash = computeChainHash(null, receipt.payloadHash);
|
|
631
|
+
if (expectedChainHash !== receipt.receiptHash) {
|
|
632
|
+
checks.receiptHashValid = false;
|
|
633
|
+
errors.push('Receipt hash mismatch');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
valid: errors.length === 0,
|
|
638
|
+
errors,
|
|
639
|
+
checks
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ============================================================================
|
|
644
|
+
// RECEIPT CHAIN BUILDER
|
|
645
|
+
// ============================================================================
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* ReceiptChainBuilder - Build chains of observation receipts
|
|
649
|
+
*/
|
|
650
|
+
export class ReceiptChainBuilder {
|
|
651
|
+
/**
|
|
652
|
+
*
|
|
653
|
+
*/
|
|
654
|
+
constructor() {
|
|
655
|
+
/** @type {Map<string, ObservationReceipt[]>} */
|
|
656
|
+
this.chains = new Map();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Add observation to agent's chain
|
|
661
|
+
* @param {string} agentId - Agent ID
|
|
662
|
+
* @param {Object} payload - Observation payload
|
|
663
|
+
* @param {string} domain - Observation domain
|
|
664
|
+
* @returns {ObservationReceipt} Created receipt
|
|
665
|
+
*/
|
|
666
|
+
addObservation(agentId, payload, domain) {
|
|
667
|
+
if (!this.chains.has(agentId)) {
|
|
668
|
+
this.chains.set(agentId, []);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const chain = this.chains.get(agentId);
|
|
672
|
+
const previousReceipt = chain.length > 0 ? chain[chain.length - 1] : null;
|
|
673
|
+
const observationIndex = chain.length + 1;
|
|
674
|
+
|
|
675
|
+
const receipt = createObservationReceipt({
|
|
676
|
+
agentId,
|
|
677
|
+
observationIndex,
|
|
678
|
+
payload,
|
|
679
|
+
domain,
|
|
680
|
+
previousReceipt
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
chain.push(receipt);
|
|
684
|
+
return receipt;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Get chain for agent
|
|
689
|
+
* @param {string} agentId - Agent ID
|
|
690
|
+
* @returns {ObservationReceipt[]} Agent's receipt chain
|
|
691
|
+
*/
|
|
692
|
+
getChain(agentId) {
|
|
693
|
+
return this.chains.get(agentId) || [];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get final hash for agent's chain
|
|
698
|
+
* @param {string} agentId - Agent ID
|
|
699
|
+
* @returns {string|null} Final chain hash or null
|
|
700
|
+
*/
|
|
701
|
+
getFinalHash(agentId) {
|
|
702
|
+
const chain = this.chains.get(agentId);
|
|
703
|
+
if (!chain || chain.length === 0) return null;
|
|
704
|
+
return chain[chain.length - 1].obsHash;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Get all agent IDs
|
|
709
|
+
* @returns {string[]}
|
|
710
|
+
*/
|
|
711
|
+
getAgentIds() {
|
|
712
|
+
return Array.from(this.chains.keys());
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Get shard info for all agents
|
|
717
|
+
* @returns {ShardInfo[]}
|
|
718
|
+
*/
|
|
719
|
+
getShardInfo() {
|
|
720
|
+
const shards = [];
|
|
721
|
+
for (const [agentId, chain] of this.chains) {
|
|
722
|
+
if (chain.length > 0) {
|
|
723
|
+
shards.push({
|
|
724
|
+
agentId,
|
|
725
|
+
chainFinalHash: chain[chain.length - 1].obsHash,
|
|
726
|
+
obsCount: chain.length,
|
|
727
|
+
domain: chain[0].domain
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return shards;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Verify entire chain for agent
|
|
736
|
+
* @param {string} agentId - Agent ID
|
|
737
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
738
|
+
*/
|
|
739
|
+
verifyChain(agentId) {
|
|
740
|
+
const chain = this.chains.get(agentId);
|
|
741
|
+
if (!chain) {
|
|
742
|
+
return { valid: false, errors: ['No chain found for agent'] };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const allErrors = [];
|
|
746
|
+
for (let i = 0; i < chain.length; i++) {
|
|
747
|
+
const previous = i > 0 ? chain[i - 1] : null;
|
|
748
|
+
const result = verifyObservationReceipt(chain[i], previous);
|
|
749
|
+
if (!result.valid) {
|
|
750
|
+
allErrors.push(...result.errors.map(e => `[${i}] ${e}`));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
valid: allErrors.length === 0,
|
|
756
|
+
errors: allErrors
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Clear all chains
|
|
762
|
+
*/
|
|
763
|
+
clear() {
|
|
764
|
+
this.chains.clear();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Create receipt chain builder
|
|
770
|
+
* @returns {ReceiptChainBuilder}
|
|
771
|
+
*/
|
|
772
|
+
export function createReceiptChainBuilder() {
|
|
773
|
+
return new ReceiptChainBuilder();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ============================================================================
|
|
777
|
+
// EXPORTS
|
|
778
|
+
// ============================================================================
|
|
779
|
+
|
|
780
|
+
export default {
|
|
781
|
+
// Constants
|
|
782
|
+
HASH_ALGORITHM,
|
|
783
|
+
RECEIPT_VERSION,
|
|
784
|
+
RECEIPT_TYPES,
|
|
785
|
+
|
|
786
|
+
// Hash utilities
|
|
787
|
+
computeHash,
|
|
788
|
+
computeChainHash,
|
|
789
|
+
deterministicSerialize,
|
|
790
|
+
|
|
791
|
+
// Merkle
|
|
792
|
+
buildMerkleTree,
|
|
793
|
+
verifyMerkleRoot,
|
|
794
|
+
|
|
795
|
+
// Receipt creation
|
|
796
|
+
createObservationReceipt,
|
|
797
|
+
createMergeReceipt,
|
|
798
|
+
createVerificationReceipt,
|
|
799
|
+
|
|
800
|
+
// Verification
|
|
801
|
+
verifyObservationReceipt,
|
|
802
|
+
verifyMergeReceipt,
|
|
803
|
+
verifyVerificationReceipt,
|
|
804
|
+
|
|
805
|
+
// Confidence
|
|
806
|
+
calculateConfidenceScore,
|
|
807
|
+
getFailedChecks,
|
|
808
|
+
summarizeVerification,
|
|
809
|
+
|
|
810
|
+
// Chain builder
|
|
811
|
+
ReceiptChainBuilder,
|
|
812
|
+
createReceiptChainBuilder
|
|
813
|
+
};
|