@unrdf/observability 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,188 @@
1
+ /**
2
+ * Merkle Tree - Efficient batch verification for receipts
3
+ *
4
+ * Builds binary Merkle tree over receipt hashes:
5
+ * - O(log n) proof size for n receipts
6
+ * - Single root hash for batch verification
7
+ * - Efficient proof generation and verification
8
+ *
9
+ * @module @unrdf/observability/receipts/merkle-tree
10
+ */
11
+
12
+ import { blake3 } from 'hash-wasm';
13
+ import { MerkleProofSchema } from './receipt-schema.mjs';
14
+
15
+ /**
16
+ * MerkleTree - Binary Merkle tree for receipt batching
17
+ *
18
+ * @example
19
+ * const tree = new MerkleTree();
20
+ * tree.addReceipt(receipt1);
21
+ * tree.addReceipt(receipt2);
22
+ * const root = await tree.buildTree();
23
+ * const proof = await tree.generateProof(receipt1.id);
24
+ */
25
+ export class MerkleTree {
26
+ /**
27
+ * Create a new Merkle tree
28
+ */
29
+ constructor() {
30
+ this.receipts = [];
31
+ this.leaves = [];
32
+ this.tree = [];
33
+ this.root = null;
34
+ }
35
+
36
+ /**
37
+ * Add receipt to tree (before building)
38
+ * @param {Object} receipt - Receipt to add
39
+ */
40
+ addReceipt(receipt) {
41
+ if (!receipt || !receipt.id || !receipt.hash) {
42
+ throw new TypeError('addReceipt: receipt must have id and hash');
43
+ }
44
+ this.receipts.push(receipt);
45
+ this.root = null; // Invalidate tree
46
+ }
47
+
48
+ /**
49
+ * Build Merkle tree from receipts
50
+ * @returns {Promise<string>} Merkle root hash (64-char hex)
51
+ */
52
+ async buildTree() {
53
+ if (this.receipts.length === 0) {
54
+ throw new Error('Cannot build tree: no receipts added');
55
+ }
56
+
57
+ // Initialize leaves from receipt hashes
58
+ this.leaves = this.receipts.map(r => r.hash);
59
+ this.tree = [this.leaves];
60
+
61
+ // Build tree levels
62
+ let currentLevel = this.leaves;
63
+ while (currentLevel.length > 1) {
64
+ const nextLevel = [];
65
+ let i = 0;
66
+ while (i < currentLevel.length) {
67
+ if (i + 1 < currentLevel.length) {
68
+ // Hash pair
69
+ const left = currentLevel[i];
70
+ const right = currentLevel[i + 1];
71
+ const combined = left + ':' + right;
72
+ const pairHash = await blake3(combined);
73
+ nextLevel.push(pairHash);
74
+ i += 2;
75
+ } else {
76
+ // Odd node promoted to next level
77
+ nextLevel.push(currentLevel[i]);
78
+ i += 1;
79
+ }
80
+ }
81
+ this.tree.push(nextLevel);
82
+ currentLevel = nextLevel;
83
+ }
84
+
85
+ this.root = currentLevel[0];
86
+ return this.root;
87
+ }
88
+
89
+ /**
90
+ * Get Merkle root
91
+ * @returns {string|null}
92
+ */
93
+ getRoot() {
94
+ return this.root;
95
+ }
96
+
97
+ /**
98
+ * Generate Merkle proof for a receipt
99
+ *
100
+ * @param {string} receiptId - Receipt ID to prove
101
+ * @returns {Promise<Object>} Merkle proof
102
+ * @throws {Error} If receipt not found or tree not built
103
+ */
104
+ async generateProof(receiptId) {
105
+ if (!this.root) {
106
+ throw new Error('Tree not built: call buildTree() first');
107
+ }
108
+
109
+ const index = this.receipts.findIndex(r => r.id === receiptId);
110
+ if (index === -1) {
111
+ throw new Error('Receipt not found: ' + receiptId);
112
+ }
113
+
114
+ const receipt = this.receipts[index];
115
+ const siblings = [];
116
+
117
+ let idx = index;
118
+ let level = 0;
119
+ while (level < this.tree.length - 1) {
120
+ const levelData = this.tree[level];
121
+ const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
122
+
123
+ if (siblingIdx < levelData.length) {
124
+ siblings.push({
125
+ hash: levelData[siblingIdx],
126
+ position: idx % 2 === 0 ? 'right' : 'left',
127
+ });
128
+ }
129
+
130
+ idx = Math.floor(idx / 2);
131
+ level += 1;
132
+ }
133
+
134
+ const proof = {
135
+ receiptId: receipt.id,
136
+ receiptHash: receipt.hash,
137
+ root: this.root,
138
+ siblings,
139
+ index,
140
+ };
141
+
142
+ return MerkleProofSchema.parse(proof);
143
+ }
144
+
145
+ /**
146
+ * Verify a Merkle proof
147
+ *
148
+ * @param {Object} proof - Merkle proof to verify
149
+ * @returns {Promise<boolean>} Whether proof is valid
150
+ */
151
+ async verifyProof(proof) {
152
+ try {
153
+ MerkleProofSchema.parse(proof);
154
+ } catch (err) {
155
+ return false;
156
+ }
157
+
158
+ let currentHash = proof.receiptHash;
159
+
160
+ let i = 0;
161
+ while (i < proof.siblings.length) {
162
+ const sibling = proof.siblings[i];
163
+ const combined =
164
+ sibling.position === 'right'
165
+ ? currentHash + ':' + sibling.hash
166
+ : sibling.hash + ':' + currentHash;
167
+ currentHash = await blake3(combined);
168
+ i += 1;
169
+ }
170
+
171
+ return currentHash === proof.root;
172
+ }
173
+
174
+ /**
175
+ * Get tree info
176
+ * @returns {Object} Tree metadata
177
+ */
178
+ getTreeInfo() {
179
+ return {
180
+ receiptCount: this.receipts.length,
181
+ depth: this.tree.length - 1,
182
+ root: this.root,
183
+ leafCount: this.leaves.length,
184
+ };
185
+ }
186
+ }
187
+
188
+ export default MerkleTree;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Receipt Chain - Hash-chained receipt sequence
3
+ *
4
+ * Provides immutable audit trail via hash chaining:
5
+ * - Each receipt contains hash of previous receipt
6
+ * - Chain break detection (any modification invalidates subsequent hashes)
7
+ * - Temporal ordering enforcement
8
+ * - Complete provenance from genesis to current
9
+ *
10
+ * @module @unrdf/observability/receipts/receipt-chain
11
+ */
12
+
13
+ import { blake3 } from 'hash-wasm';
14
+ import { ReceiptSchema } from './receipt-schema.mjs';
15
+
16
+ /**
17
+ * ReceiptChain - Manages a chain of cryptographically linked receipts
18
+ *
19
+ * @example
20
+ * const chain = new ReceiptChain('audit-chain-1');
21
+ * await chain.append({
22
+ * operation: 'admit',
23
+ * payload: { delta: 'delta_001' },
24
+ * actor: 'system'
25
+ * });
26
+ */
27
+ export class ReceiptChain {
28
+ /**
29
+ * Create a new receipt chain
30
+ * @param {string} chainId - Unique chain identifier
31
+ */
32
+ constructor(chainId) {
33
+ if (!chainId || typeof chainId !== 'string') {
34
+ throw new TypeError('ReceiptChain requires a string chainId');
35
+ }
36
+ this.chainId = chainId;
37
+ this.receipts = [];
38
+ this.lastTimestamp = 0n;
39
+ }
40
+
41
+ /**
42
+ * Get chain length
43
+ * @returns {number}
44
+ */
45
+ get length() {
46
+ return this.receipts.length;
47
+ }
48
+
49
+ /**
50
+ * Get latest receipt
51
+ * @returns {Object|null}
52
+ */
53
+ getLatest() {
54
+ return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : null;
55
+ }
56
+
57
+ /**
58
+ * Generate deterministic receipt ID
59
+ * @param {string} operation - Operation type
60
+ * @param {bigint} timestamp - Timestamp in nanoseconds
61
+ * @returns {string}
62
+ * @private
63
+ */
64
+ _generateId(operation, timestamp) {
65
+ return `receipt-${operation}-${timestamp}-${this.receipts.length}`;
66
+ }
67
+
68
+ /**
69
+ * Compute canonical hash of receipt content
70
+ * @param {Object} content - Receipt content to hash
71
+ * @returns {Promise<string>} BLAKE3 hash (64-char hex)
72
+ * @private
73
+ */
74
+ async _computeHash(content) {
75
+ // Canonical JSON serialization (sorted keys)
76
+ const canonical = JSON.stringify(content, Object.keys(content).sort());
77
+ return await blake3(canonical);
78
+ }
79
+
80
+ /**
81
+ * Append a new receipt to the chain
82
+ *
83
+ * @param {Object} receiptData - Receipt data
84
+ * @param {string} receiptData.operation - Operation type
85
+ * @param {any} receiptData.payload - Operation payload
86
+ * @param {string} [receiptData.actor] - Actor who performed operation
87
+ * @param {bigint} [receiptData.timestamp_ns] - Custom timestamp (default: now)
88
+ * @param {Object} [receiptData.metadata] - Optional metadata
89
+ * @returns {Promise<Object>} Appended receipt
90
+ * @throws {Error} If receipt is invalid or chain is broken
91
+ */
92
+ async append(receiptData) {
93
+ const { operation, payload, actor, metadata } = receiptData;
94
+
95
+ // Input validation
96
+ if (!operation || typeof operation !== 'string') {
97
+ throw new TypeError('append: operation must be a non-empty string');
98
+ }
99
+ if (payload === undefined || payload === null) {
100
+ throw new TypeError('append: payload is required');
101
+ }
102
+
103
+ // Get timestamp (custom or now)
104
+ const timestamp_ns = receiptData.timestamp_ns || BigInt(Date.now()) * 1_000_000n;
105
+
106
+ // Enforce monotonic time
107
+ if (timestamp_ns <= this.lastTimestamp) {
108
+ throw new Error(`Timestamp not monotonic: ${timestamp_ns} <= ${this.lastTimestamp}`);
109
+ }
110
+
111
+ // Generate receipt ID
112
+ const id = this._generateId(operation, timestamp_ns);
113
+ const timestamp_iso = new Date(Number(timestamp_ns / 1_000_000n)).toISOString();
114
+
115
+ // Get previous receipt hash (or null for genesis)
116
+ const previousHash = this.getLatest()?.hash || null;
117
+
118
+ // Build canonical receipt object for hashing
119
+ const canonicalContent = {
120
+ id,
121
+ timestamp_ns: timestamp_ns.toString(),
122
+ timestamp_iso,
123
+ operation,
124
+ payload,
125
+ previousHash,
126
+ ...(actor && { actor }),
127
+ ...(metadata && { metadata }),
128
+ };
129
+
130
+ // Compute hash
131
+ const hash = await this._computeHash(canonicalContent);
132
+
133
+ // Create receipt
134
+ const receipt = {
135
+ id,
136
+ hash,
137
+ timestamp_ns: timestamp_ns.toString(),
138
+ timestamp_iso,
139
+ operation,
140
+ payload,
141
+ previousHash,
142
+ ...(actor && { actor }),
143
+ ...(metadata && { metadata }),
144
+ };
145
+
146
+ // Validate against schema
147
+ const validated = ReceiptSchema.parse(receipt);
148
+
149
+ // Append to chain
150
+ this.receipts.push(Object.freeze(validated));
151
+ this.lastTimestamp = timestamp_ns;
152
+
153
+ return validated;
154
+ }
155
+
156
+ /**
157
+ * Get receipt by index
158
+ * @param {number} index - Receipt index (0-based)
159
+ * @returns {Object|null}
160
+ */
161
+ getReceipt(index) {
162
+ return this.receipts[index] || null;
163
+ }
164
+
165
+ /**
166
+ * Get receipt by ID
167
+ * @param {string} id - Receipt ID
168
+ * @returns {Object|null}
169
+ */
170
+ getReceiptById(id) {
171
+ return this.receipts.find(r => r.id === id) || null;
172
+ }
173
+
174
+ /**
175
+ * Get all receipts (defensive copy)
176
+ * @returns {Array<Object>}
177
+ */
178
+ getAllReceipts() {
179
+ return [...this.receipts];
180
+ }
181
+
182
+ /**
183
+ * Serialize chain to JSON
184
+ * @returns {Object}
185
+ */
186
+ toJSON() {
187
+ return {
188
+ chainId: this.chainId,
189
+ length: this.receipts.length,
190
+ receipts: this.receipts,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Deserialize chain from JSON
196
+ * @param {Object} json - Serialized chain
197
+ * @returns {ReceiptChain}
198
+ */
199
+ static fromJSON(json) {
200
+ const chain = new ReceiptChain(json.chainId);
201
+ for (const receipt of json.receipts) {
202
+ chain.receipts.push(Object.freeze(receipt));
203
+ chain.lastTimestamp = BigInt(receipt.timestamp_ns);
204
+ }
205
+ return chain;
206
+ }
207
+ }
208
+
209
+ export default ReceiptChain;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Receipt Schema Definitions
3
+ *
4
+ * Zod schemas for tamper-evident receipt system
5
+ * Supports hash chaining, merkle proofs, and external anchoring
6
+ *
7
+ * @module @unrdf/observability/receipts/receipt-schema
8
+ */
9
+
10
+ import { z } from 'zod';
11
+
12
+ /**
13
+ * Receipt schema - cryptographic proof of an operation
14
+ *
15
+ * Fields:
16
+ * - id: Unique receipt identifier (UUID or deterministic)
17
+ * - hash: BLAKE3 hash of receipt content (64-char hex)
18
+ * - timestamp_ns: Nanosecond timestamp (BigInt as string)
19
+ * - timestamp_iso: ISO 8601 timestamp for human readability
20
+ * - operation: Operation type ('admit', 'freeze', 'publish', etc.)
21
+ * - payload: Operation-specific data (any JSON)
22
+ * - previousHash: Hash of previous receipt (null for genesis)
23
+ * - actor: Who performed the operation
24
+ * - metadata: Optional additional metadata
25
+ */
26
+ export const ReceiptSchema = z.object({
27
+ id: z.string().min(1),
28
+ hash: z.string().regex(/^[0-9a-f]{64}$/i, 'Must be 64-character hex hash'),
29
+ timestamp_ns: z.string().regex(/^\d+$/, 'Must be numeric string'),
30
+ timestamp_iso: z.string().datetime(),
31
+ operation: z.string().min(1),
32
+ payload: z.any(),
33
+ previousHash: z
34
+ .string()
35
+ .regex(/^[0-9a-f]{64}$/i)
36
+ .nullable(),
37
+ actor: z.string().min(1).optional(),
38
+ metadata: z.record(z.any()).optional(),
39
+ });
40
+
41
+ /**
42
+ * Merkle proof schema - proves receipt inclusion in tree
43
+ */
44
+ export const MerkleProofSchema = z.object({
45
+ receiptId: z.string(),
46
+ receiptHash: z.string().regex(/^[0-9a-f]{64}$/i),
47
+ root: z.string().regex(/^[0-9a-f]{64}$/i),
48
+ siblings: z.array(
49
+ z.object({
50
+ hash: z.string().regex(/^[0-9a-f]{64}$/i),
51
+ position: z.enum(['left', 'right']),
52
+ })
53
+ ),
54
+ index: z.number().int().nonnegative(),
55
+ });
56
+
57
+ /**
58
+ * Anchor schema - external timestamping/anchoring proof
59
+ */
60
+ export const AnchorSchema = z.object({
61
+ merkleRoot: z.string().regex(/^[0-9a-f]{64}$/i),
62
+ anchorType: z.enum(['blockchain', 'git', 'timestamp-service']),
63
+ anchorData: z.object({
64
+ // Blockchain
65
+ txHash: z.string().optional(),
66
+ blockNumber: z.number().optional(),
67
+ network: z.string().optional(),
68
+ // Git
69
+ commitSha: z.string().optional(),
70
+ repository: z.string().optional(),
71
+ // Timestamp service
72
+ timestampToken: z.string().optional(),
73
+ authority: z.string().optional(),
74
+ }),
75
+ timestamp: z.string().datetime(),
76
+ });
77
+
78
+ /**
79
+ * Verification result schema
80
+ */
81
+ export const VerificationResultSchema = z.object({
82
+ valid: z.boolean(),
83
+ receiptId: z.string().optional(),
84
+ errors: z.array(z.string()).default([]),
85
+ checks: z
86
+ .object({
87
+ hashIntegrity: z.boolean().optional(),
88
+ chainLink: z.boolean().optional(),
89
+ temporalOrder: z.boolean().optional(),
90
+ merkleProof: z.boolean().optional(),
91
+ })
92
+ .optional(),
93
+ });
94
+
95
+ /**
96
+ * Chain export schema - audit trail export
97
+ */
98
+ export const ChainExportSchema = z.object({
99
+ chainId: z.string(),
100
+ receiptCount: z.number().int().nonnegative(),
101
+ firstReceipt: z.string().datetime().optional(),
102
+ lastReceipt: z.string().datetime().optional(),
103
+ merkleRoot: z.string().regex(/^[0-9a-f]{64}$/i),
104
+ chainValid: z.boolean(),
105
+ receipts: z.array(ReceiptSchema),
106
+ exportedAt: z.string().datetime(),
107
+ anchor: AnchorSchema.optional(),
108
+ });
109
+
110
+ /**
111
+ * Type exports for TypeScript/JSDoc
112
+ */
113
+
114
+ /**
115
+ * @typedef {z.infer<typeof ReceiptSchema>} Receipt
116
+ * @typedef {z.infer<typeof MerkleProofSchema>} MerkleProof
117
+ * @typedef {z.infer<typeof AnchorSchema>} Anchor
118
+ * @typedef {z.infer<typeof VerificationResultSchema>} VerificationResult
119
+ * @typedef {z.infer<typeof ChainExportSchema>} ChainExport
120
+ */
121
+
122
+ export default {
123
+ ReceiptSchema,
124
+ MerkleProofSchema,
125
+ AnchorSchema,
126
+ VerificationResultSchema,
127
+ ChainExportSchema,
128
+ };