@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.
- package/.eslintrc.cjs +10 -0
- package/IMPLEMENTATION-SUMMARY.md +478 -0
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/capability-map.md +90 -0
- package/config/alert-rules.yml +269 -0
- package/config/prometheus.yml +136 -0
- package/dashboards/grafana-unrdf.json +798 -0
- package/dashboards/unrdf-workflow-dashboard.json +295 -0
- package/docs/OBSERVABILITY-PATTERNS.md +681 -0
- package/docs/OBSERVABILITY-RUNBOOK.md +554 -0
- package/examples/observability-demo.mjs +334 -0
- package/package.json +46 -0
- package/src/advanced-metrics.mjs +413 -0
- package/src/alerts/alert-manager.mjs +436 -0
- package/src/custom-events.mjs +558 -0
- package/src/distributed-tracing.mjs +352 -0
- package/src/exporters/grafana-exporter.mjs +415 -0
- package/src/index.mjs +61 -0
- package/src/metrics/workflow-metrics.mjs +346 -0
- package/src/receipts/anchor.mjs +155 -0
- package/src/receipts/index.mjs +62 -0
- package/src/receipts/merkle-tree.mjs +188 -0
- package/src/receipts/receipt-chain.mjs +209 -0
- package/src/receipts/receipt-schema.mjs +128 -0
- package/src/receipts/tamper-detection.mjs +219 -0
- package/test/advanced-metrics.test.mjs +302 -0
- package/test/custom-events.test.mjs +387 -0
- package/test/distributed-tracing.test.mjs +314 -0
- package/validation/observability-validation.mjs +366 -0
- package/vitest.config.mjs +25 -0
|
@@ -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
|
+
};
|