@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.
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview Receipt generation with hash chains for KGC Probe
4
+ *
5
+ * Receipt = cryptographic proof of observation integrity.
6
+ * Uses BLAKE3 for fast, deterministic hashing.
7
+ *
8
+ * Hash chain: Each receipt includes hash of previous receipt → tamper-evident sequence.
9
+ *
10
+ * Design principles:
11
+ * - Deterministic: Same input → same hash
12
+ * - Tamper-evident: Any modification breaks chain
13
+ * - Fast: BLAKE3 is optimized for speed
14
+ * - Verifiable: Can reconstruct and verify entire chain
15
+ */
16
+
17
+ import { blake3 } from '@noble/hashes/blake3';
18
+ import { bytesToHex } from '@noble/hashes/utils';
19
+
20
+ /**
21
+ * Hash data deterministically using BLAKE3
22
+ *
23
+ * @param {any} data - Data to hash (will be JSON-stringified with stable key order)
24
+ * @returns {string} - Hex-encoded hash
25
+ */
26
+ export function hashData(data) {
27
+ // Stable JSON serialization (sorted keys)
28
+ const json = JSON.stringify(data, Object.keys(data).sort());
29
+ const bytes = new TextEncoder().encode(json);
30
+ const hash = blake3(bytes);
31
+ return bytesToHex(hash);
32
+ }
33
+
34
+ /**
35
+ * Create receipt for an observation
36
+ *
37
+ * @param {import('./observation.mjs').Observation} observation - Observation to create receipt for
38
+ * @param {string} [previousHash] - Hash of previous receipt (for chain)
39
+ * @returns {Receipt}
40
+ *
41
+ * @typedef {Object} Receipt
42
+ * @property {string} observationId - ID of observation this receipt covers
43
+ * @property {string} hash - BLAKE3 hash of observation content
44
+ * @property {string} [previousHash] - Hash of previous receipt (chain)
45
+ * @property {number} index - Position in chain (0-indexed)
46
+ * @property {string} timestamp - ISO 8601 timestamp
47
+ */
48
+ export function createReceipt(observation, previousHash = null) {
49
+ // Create deterministic content for hashing
50
+ const content = {
51
+ id: observation.id,
52
+ category: observation.category,
53
+ severity: observation.severity,
54
+ message: observation.message,
55
+ location: observation.location,
56
+ data: observation.data,
57
+ metadata: observation.metadata,
58
+ tags: observation.tags.slice().sort() // Stable tag order
59
+ };
60
+
61
+ const hash = hashData(content);
62
+
63
+ const receipt = {
64
+ observationId: observation.id,
65
+ hash,
66
+ previousHash,
67
+ index: previousHash ? -1 : 0, // Will be set by chain builder
68
+ timestamp: new Date().toISOString()
69
+ };
70
+
71
+ return receipt;
72
+ }
73
+
74
+ /**
75
+ * Build receipt chain from observations
76
+ *
77
+ * @param {import('./observation.mjs').Observation[]} observations - Observations to chain
78
+ * @returns {Receipt[]} - Receipt chain
79
+ */
80
+ export function buildReceiptChain(observations) {
81
+ const receipts = [];
82
+ let previousHash = null;
83
+
84
+ for (let i = 0; i < observations.length; i++) {
85
+ const receipt = createReceipt(observations[i], previousHash);
86
+ receipt.index = i;
87
+ receipts.push(receipt);
88
+ previousHash = receipt.hash;
89
+ }
90
+
91
+ return receipts;
92
+ }
93
+
94
+ /**
95
+ * Verify receipt chain integrity
96
+ *
97
+ * @param {Receipt[]} receipts - Receipt chain to verify
98
+ * @returns {boolean} - True if chain is valid
99
+ */
100
+ export function verifyReceiptChain(receipts) {
101
+ if (receipts.length === 0) {
102
+ return true;
103
+ }
104
+
105
+ // First receipt should have no previous hash
106
+ if (receipts[0].previousHash !== null) {
107
+ return false;
108
+ }
109
+
110
+ // Verify chain links
111
+ for (let i = 1; i < receipts.length; i++) {
112
+ if (receipts[i].previousHash !== receipts[i - 1].hash) {
113
+ return false;
114
+ }
115
+ if (receipts[i].index !== i) {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * Verify observation against receipt
125
+ *
126
+ * @param {import('./observation.mjs').Observation} observation - Observation to verify
127
+ * @param {Receipt} receipt - Receipt to verify against
128
+ * @returns {boolean} - True if observation matches receipt
129
+ */
130
+ export function verifyObservation(observation, receipt) {
131
+ const recomputedReceipt = createReceipt(observation, receipt.previousHash);
132
+ return recomputedReceipt.hash === receipt.hash;
133
+ }
134
+
135
+ /**
136
+ * Create receipt manifest (summary of entire chain)
137
+ *
138
+ * @param {Receipt[]} receipts - Receipt chain
139
+ * @returns {ReceiptManifest}
140
+ *
141
+ * @typedef {Object} ReceiptManifest
142
+ * @property {number} count - Number of receipts in chain
143
+ * @property {string} firstHash - Hash of first receipt
144
+ * @property {string} lastHash - Hash of last receipt
145
+ * @property {string} chainHash - Hash of entire chain
146
+ * @property {string} timestamp - ISO 8601 timestamp
147
+ */
148
+ export function createManifest(receipts) {
149
+ if (receipts.length === 0) {
150
+ return {
151
+ count: 0,
152
+ firstHash: null,
153
+ lastHash: null,
154
+ chainHash: null,
155
+ timestamp: new Date().toISOString()
156
+ };
157
+ }
158
+
159
+ // Chain hash = hash of all receipt hashes
160
+ const chainContent = receipts.map(r => r.hash).join('');
161
+ const chainHash = hashData(chainContent);
162
+
163
+ return {
164
+ count: receipts.length,
165
+ firstHash: receipts[0].hash,
166
+ lastHash: receipts[receipts.length - 1].hash,
167
+ chainHash,
168
+ timestamp: new Date().toISOString()
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Verify manifest matches receipts
174
+ *
175
+ * @param {ReceiptManifest} manifest - Manifest to verify
176
+ * @param {Receipt[]} receipts - Receipt chain
177
+ * @returns {boolean} - True if manifest is valid
178
+ */
179
+ export function verifyManifest(manifest, receipts) {
180
+ const recomputed = createManifest(receipts);
181
+ return (
182
+ manifest.count === recomputed.count &&
183
+ manifest.firstHash === recomputed.firstHash &&
184
+ manifest.lastHash === recomputed.lastHash &&
185
+ manifest.chainHash === recomputed.chainHash
186
+ );
187
+ }
188
+
189
+ export default {
190
+ hashData,
191
+ createReceipt,
192
+ buildReceiptChain,
193
+ verifyReceiptChain,
194
+ verifyObservation,
195
+ createManifest,
196
+ verifyManifest
197
+ };