@unrdf/knowledge-engine 5.0.1
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/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +64 -0
- package/src/browser-shims.mjs +343 -0
- package/src/browser.mjs +910 -0
- package/src/canonicalize.mjs +414 -0
- package/src/condition-cache.mjs +109 -0
- package/src/condition-evaluator.mjs +722 -0
- package/src/dark-matter-core.mjs +742 -0
- package/src/define-hook.mjs +213 -0
- package/src/effect-sandbox-browser.mjs +283 -0
- package/src/effect-sandbox-worker.mjs +170 -0
- package/src/effect-sandbox.mjs +517 -0
- package/src/engines/index.mjs +11 -0
- package/src/engines/rdf-engine.mjs +299 -0
- package/src/file-resolver.mjs +387 -0
- package/src/hook-executor-batching.mjs +277 -0
- package/src/hook-executor.mjs +870 -0
- package/src/hook-management.mjs +150 -0
- package/src/index.mjs +93 -0
- package/src/ken-parliment.mjs +119 -0
- package/src/ken.mjs +149 -0
- package/src/knowledge-engine/builtin-rules.mjs +190 -0
- package/src/knowledge-engine/inference-engine.mjs +418 -0
- package/src/knowledge-engine/knowledge-engine.mjs +317 -0
- package/src/knowledge-engine/pattern-dsl.mjs +142 -0
- package/src/knowledge-engine/pattern-matcher.mjs +215 -0
- package/src/knowledge-engine/rules.mjs +184 -0
- package/src/knowledge-engine.mjs +319 -0
- package/src/knowledge-hook-engine.mjs +360 -0
- package/src/knowledge-hook-manager.mjs +469 -0
- package/src/knowledge-substrate-core.mjs +927 -0
- package/src/lite.mjs +222 -0
- package/src/lockchain-writer-browser.mjs +414 -0
- package/src/lockchain-writer.mjs +602 -0
- package/src/monitoring/andon-signals.mjs +775 -0
- package/src/observability.mjs +531 -0
- package/src/parse.mjs +290 -0
- package/src/performance-optimizer.mjs +678 -0
- package/src/policy-pack.mjs +572 -0
- package/src/query-cache.mjs +116 -0
- package/src/query-optimizer.mjs +1051 -0
- package/src/query.mjs +306 -0
- package/src/reason.mjs +350 -0
- package/src/resolution-layer.mjs +506 -0
- package/src/schemas.mjs +1063 -0
- package/src/security/error-sanitizer.mjs +257 -0
- package/src/security/path-validator.mjs +194 -0
- package/src/security/sandbox-restrictions.mjs +331 -0
- package/src/security-validator.mjs +389 -0
- package/src/store-cache.mjs +137 -0
- package/src/telemetry.mjs +167 -0
- package/src/transaction.mjs +810 -0
- package/src/utils/adaptive-monitor.mjs +746 -0
- package/src/utils/circuit-breaker.mjs +513 -0
- package/src/utils/edge-case-handler.mjs +503 -0
- package/src/utils/memory-manager.mjs +498 -0
- package/src/utils/ring-buffer.mjs +282 -0
- package/src/validate.mjs +319 -0
- package/src/validators/index.mjs +338 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Lockchain Writer for persistent, verifiable audit trail
|
|
3
|
+
* @module lockchain-writer
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Implements a persistent, verifiable audit trail by anchoring signed receipts
|
|
7
|
+
* to Git. Provides cryptographic integrity and tamper-proof provenance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
17
|
+
import { _blake3 } from '@noble/hashes/blake3.js';
|
|
18
|
+
import { utf8ToBytes, bytesToHex } from '@noble/hashes/utils.js';
|
|
19
|
+
import { randomUUID } from 'crypto';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Schema for lockchain entry
|
|
24
|
+
*/
|
|
25
|
+
const LockchainEntrySchema = z.object({
|
|
26
|
+
id: z.string().uuid(),
|
|
27
|
+
timestamp: z.number(),
|
|
28
|
+
receipt: z.any(), // Transaction receipt
|
|
29
|
+
signature: z.object({
|
|
30
|
+
algorithm: z.string(),
|
|
31
|
+
value: z.string(),
|
|
32
|
+
publicKey: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
previousHash: z.string().optional().nullable(),
|
|
35
|
+
merkleRoot: z.string().optional(),
|
|
36
|
+
gitCommit: z.string().optional(),
|
|
37
|
+
gitRef: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Schema for lockchain configuration
|
|
42
|
+
*/
|
|
43
|
+
const LockchainConfigSchema = z.object({
|
|
44
|
+
gitRepo: z.string().default(process.cwd()),
|
|
45
|
+
refName: z.string().default('refs/notes/lockchain'),
|
|
46
|
+
signingKey: z.string().optional(),
|
|
47
|
+
algorithm: z.enum(['ed25519', 'ecdsa', 'rsa']).default('ed25519'),
|
|
48
|
+
batchSize: z.number().int().positive().default(10),
|
|
49
|
+
enableMerkle: z.boolean().default(true),
|
|
50
|
+
enableGitAnchoring: z.boolean().default(true),
|
|
51
|
+
storagePath: z.string().optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lockchain Writer for persistent, verifiable audit trail
|
|
56
|
+
*/
|
|
57
|
+
export class LockchainWriter {
|
|
58
|
+
/**
|
|
59
|
+
* Create a new lockchain writer
|
|
60
|
+
* @param {Object} [config] - Lockchain configuration
|
|
61
|
+
*/
|
|
62
|
+
constructor(config = {}) {
|
|
63
|
+
const validatedConfig = LockchainConfigSchema.parse(config);
|
|
64
|
+
this.config = validatedConfig;
|
|
65
|
+
|
|
66
|
+
// Initialize storage
|
|
67
|
+
this.storagePath = validatedConfig.storagePath || join(validatedConfig.gitRepo, '.lockchain');
|
|
68
|
+
this.initialized = false;
|
|
69
|
+
|
|
70
|
+
// Cache for pending entries
|
|
71
|
+
this.pendingEntries = [];
|
|
72
|
+
this.entryCache = new Map();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize lockchain writer (async initialization pattern)
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async init() {
|
|
80
|
+
if (this.initialized) {
|
|
81
|
+
return; // Already initialized
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Initialize storage
|
|
85
|
+
this._ensureStorageExists();
|
|
86
|
+
|
|
87
|
+
// Initialize Git repository if needed
|
|
88
|
+
this._initializeGitRepo();
|
|
89
|
+
|
|
90
|
+
this.initialized = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write a receipt to the lockchain
|
|
95
|
+
* @param {Object} receipt - Transaction receipt
|
|
96
|
+
* @param {Object} [options] - Write options
|
|
97
|
+
* @returns {Promise<Object>} Lockchain entry
|
|
98
|
+
*/
|
|
99
|
+
async writeReceipt(receipt, options = {}) {
|
|
100
|
+
// Auto-initialize if not already initialized
|
|
101
|
+
if (!this.initialized) {
|
|
102
|
+
await this.init();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entryId = randomUUID();
|
|
106
|
+
const timestamp = Date.now();
|
|
107
|
+
|
|
108
|
+
// Serialize receipt first
|
|
109
|
+
const serializedReceipt = this._serializeReceipt(receipt);
|
|
110
|
+
|
|
111
|
+
// Create lockchain entry (without merkleRoot first)
|
|
112
|
+
const entry = {
|
|
113
|
+
id: entryId,
|
|
114
|
+
timestamp,
|
|
115
|
+
receipt: serializedReceipt,
|
|
116
|
+
signature: await this._signEntry(serializedReceipt, entryId, timestamp),
|
|
117
|
+
previousHash: this._getPreviousHash() || '',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Calculate and add Merkle root if enabled
|
|
121
|
+
if (this.config.enableMerkle) {
|
|
122
|
+
entry.merkleRoot = this._calculateEntryMerkleRoot(entry);
|
|
123
|
+
} else if (options.merkleRoot) {
|
|
124
|
+
entry.merkleRoot = options.merkleRoot;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate entry
|
|
128
|
+
const validatedEntry = LockchainEntrySchema.parse(entry);
|
|
129
|
+
|
|
130
|
+
// Store entry
|
|
131
|
+
await this._storeEntry(validatedEntry);
|
|
132
|
+
|
|
133
|
+
// Add to pending batch
|
|
134
|
+
this.pendingEntries.push(validatedEntry);
|
|
135
|
+
|
|
136
|
+
// Auto-commit if batch is full
|
|
137
|
+
if (this.pendingEntries.length >= this.config.batchSize) {
|
|
138
|
+
await this.commitBatch();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return validatedEntry;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Commit pending entries to Git
|
|
146
|
+
* @param {Object} [options] - Commit options
|
|
147
|
+
* @returns {Promise<Object>} Commit result
|
|
148
|
+
*/
|
|
149
|
+
async commitBatch(_options = {}) {
|
|
150
|
+
if (this.pendingEntries.length === 0) {
|
|
151
|
+
return { committed: false, message: 'No pending entries' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const batchId = randomUUID();
|
|
155
|
+
const timestamp = Date.now();
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Create batch file
|
|
159
|
+
const batchData = {
|
|
160
|
+
id: batchId,
|
|
161
|
+
timestamp,
|
|
162
|
+
entries: this.pendingEntries,
|
|
163
|
+
merkleRoot: this.config.enableMerkle
|
|
164
|
+
? this._calculateMerkleRoot(this.pendingEntries)
|
|
165
|
+
: null,
|
|
166
|
+
entryCount: this.pendingEntries.length,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const batchFile = join(this.storagePath, `batch-${batchId}.json`);
|
|
170
|
+
writeFileSync(batchFile, JSON.stringify(batchData, null, 2));
|
|
171
|
+
|
|
172
|
+
// Git operations
|
|
173
|
+
if (this.config.enableGitAnchoring) {
|
|
174
|
+
await this._gitAdd(batchFile);
|
|
175
|
+
const commitHash = await this._gitCommit(`Lockchain batch ${batchId}`, {
|
|
176
|
+
entries: this.pendingEntries.length,
|
|
177
|
+
timestamp,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Update entries with Git commit info
|
|
181
|
+
for (const entry of this.pendingEntries) {
|
|
182
|
+
entry.gitCommit = commitHash;
|
|
183
|
+
entry.gitRef = this.config.refName;
|
|
184
|
+
await this._updateEntry(entry);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Clear pending entries
|
|
189
|
+
const committedCount = this.pendingEntries.length;
|
|
190
|
+
this.pendingEntries = [];
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
committed: true,
|
|
194
|
+
batchId,
|
|
195
|
+
commitHash: this.config.enableGitAnchoring ? await this._getLatestCommit() : null,
|
|
196
|
+
entryCount: committedCount,
|
|
197
|
+
timestamp,
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`Failed to commit lockchain batch: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Verify a receipt (alias for verifyEntry for README compatibility)
|
|
206
|
+
* @param {Object|string} receipt - Receipt object or entry ID
|
|
207
|
+
* @returns {Promise<boolean>} Verification result (true if valid)
|
|
208
|
+
*/
|
|
209
|
+
async verifyReceipt(receipt) {
|
|
210
|
+
const entryId = typeof receipt === 'string' ? receipt : receipt.id;
|
|
211
|
+
const result = await this.verifyEntry(entryId);
|
|
212
|
+
return result.valid;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Verify a lockchain entry
|
|
217
|
+
* @param {string} entryId - Entry ID to verify
|
|
218
|
+
* @returns {Promise<Object>} Verification result
|
|
219
|
+
*/
|
|
220
|
+
async verifyEntry(entryId) {
|
|
221
|
+
const entry = await this._loadEntry(entryId);
|
|
222
|
+
if (!entry) {
|
|
223
|
+
return { valid: false, error: 'Entry not found' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Verify signature
|
|
228
|
+
const signatureValid = await this._verifySignature(entry);
|
|
229
|
+
if (!signatureValid) {
|
|
230
|
+
return { valid: false, error: 'Invalid signature' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Verify Git commit if present
|
|
234
|
+
if (entry.gitCommit && this.config.enableGitAnchoring) {
|
|
235
|
+
const gitValid = await this._verifyGitCommit(entry.gitCommit);
|
|
236
|
+
if (!gitValid) {
|
|
237
|
+
return { valid: false, error: 'Invalid Git commit' };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Verify merkle root if present
|
|
242
|
+
if (entry.merkleRoot && this.config.enableMerkle) {
|
|
243
|
+
const merkleValid = await this._verifyMerkleRoot(entry);
|
|
244
|
+
if (!merkleValid) {
|
|
245
|
+
return { valid: false, error: 'Invalid merkle root' };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { valid: true, entry };
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return { valid: false, error: error.message };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get lockchain statistics
|
|
257
|
+
* @returns {Object} Statistics
|
|
258
|
+
*/
|
|
259
|
+
getStats() {
|
|
260
|
+
return {
|
|
261
|
+
config: this.config,
|
|
262
|
+
pendingEntries: this.pendingEntries.length,
|
|
263
|
+
storagePath: this.storagePath,
|
|
264
|
+
gitEnabled: this.config.enableGitAnchoring,
|
|
265
|
+
merkleEnabled: this.config.enableMerkle,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Ensure storage directory exists
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
_ensureStorageExists() {
|
|
274
|
+
if (!existsSync(this.storagePath)) {
|
|
275
|
+
mkdirSync(this.storagePath, { recursive: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Initialize Git repository
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
_initializeGitRepo() {
|
|
284
|
+
if (!this.config.enableGitAnchoring) return;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// Check if Git repo exists
|
|
288
|
+
execSync('git rev-parse --git-dir', {
|
|
289
|
+
cwd: this.config.gitRepo,
|
|
290
|
+
stdio: 'pipe',
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new Error(`Git repository not found at ${this.config.gitRepo}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Serialize receipt for storage
|
|
299
|
+
* @param {Object} receipt - Transaction receipt
|
|
300
|
+
* @returns {Object} Serialized receipt
|
|
301
|
+
* @private
|
|
302
|
+
*/
|
|
303
|
+
_serializeReceipt(receipt) {
|
|
304
|
+
return {
|
|
305
|
+
...receipt,
|
|
306
|
+
_serialized: true,
|
|
307
|
+
_timestamp: Date.now(),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Sign an entry
|
|
313
|
+
* @param {Object} receipt - Transaction receipt
|
|
314
|
+
* @param {string} entryId - Entry ID
|
|
315
|
+
* @param {number} timestamp - Timestamp
|
|
316
|
+
* @returns {Promise<Object>} Signature
|
|
317
|
+
* @private
|
|
318
|
+
*/
|
|
319
|
+
async _signEntry(receipt, entryId, timestamp) {
|
|
320
|
+
// For now, use a simple hash-based signature
|
|
321
|
+
// In production, this would use proper cryptographic signatures
|
|
322
|
+
const data = JSON.stringify({ receipt, entryId, timestamp });
|
|
323
|
+
const hash = bytesToHex(sha3_256(utf8ToBytes(data)));
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
algorithm: 'sha3-256',
|
|
327
|
+
value: hash,
|
|
328
|
+
publicKey: this.config.signingKey || 'default',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get previous hash for chaining
|
|
334
|
+
* @returns {string} Previous hash
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
_getPreviousHash() {
|
|
338
|
+
if (this.pendingEntries.length === 0) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const lastEntry = this.pendingEntries[this.pendingEntries.length - 1];
|
|
343
|
+
return lastEntry.signature.value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Store an entry
|
|
348
|
+
* @param {Object} entry - Lockchain entry
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
async _storeEntry(entry) {
|
|
352
|
+
const entryFile = join(this.storagePath, `entry-${entry.id}.json`);
|
|
353
|
+
writeFileSync(entryFile, JSON.stringify(entry, null, 2));
|
|
354
|
+
|
|
355
|
+
// Cache entry
|
|
356
|
+
this.entryCache.set(entry.id, entry);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Update an entry
|
|
361
|
+
* @param {Object} entry - Updated entry
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
async _updateEntry(entry) {
|
|
365
|
+
const entryFile = join(this.storagePath, `entry-${entry.id}.json`);
|
|
366
|
+
writeFileSync(entryFile, JSON.stringify(entry, null, 2));
|
|
367
|
+
|
|
368
|
+
// Update cache
|
|
369
|
+
this.entryCache.set(entry.id, entry);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Load an entry
|
|
374
|
+
* @param {string} entryId - Entry ID
|
|
375
|
+
* @returns {Promise<Object>} Entry or null
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
async _loadEntry(entryId) {
|
|
379
|
+
// Check cache first
|
|
380
|
+
if (this.entryCache.has(entryId)) {
|
|
381
|
+
return this.entryCache.get(entryId);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const entryFile = join(this.storagePath, `entry-${entryId}.json`);
|
|
385
|
+
if (!existsSync(entryFile)) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const entry = JSON.parse(readFileSync(entryFile, 'utf8'));
|
|
390
|
+
this.entryCache.set(entryId, entry);
|
|
391
|
+
return entry;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Calculate merkle root for a single entry
|
|
396
|
+
* @param {Object} entry - Single entry
|
|
397
|
+
* @returns {string} Merkle root hash
|
|
398
|
+
* @private
|
|
399
|
+
*
|
|
400
|
+
* @description
|
|
401
|
+
* Calculates a Merkle root for a single entry by hashing its canonical data.
|
|
402
|
+
* Uses deterministic JSON serialization to ensure consistent hashing.
|
|
403
|
+
*/
|
|
404
|
+
_calculateEntryMerkleRoot(entry) {
|
|
405
|
+
// Build canonical data representation
|
|
406
|
+
// Use same structure as verification for consistency
|
|
407
|
+
const entryData = {
|
|
408
|
+
id: entry.id,
|
|
409
|
+
timestamp: entry.timestamp,
|
|
410
|
+
receipt: entry.receipt,
|
|
411
|
+
signature: entry.signature,
|
|
412
|
+
previousHash: entry.previousHash || null,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Calculate hash using SHA3-256
|
|
416
|
+
const entryJson = JSON.stringify(entryData);
|
|
417
|
+
return bytesToHex(sha3_256(utf8ToBytes(entryJson)));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Calculate merkle root for multiple entries (batch)
|
|
422
|
+
* @param {Array} entries - Entries
|
|
423
|
+
* @returns {string} Merkle root
|
|
424
|
+
* @private
|
|
425
|
+
*/
|
|
426
|
+
_calculateMerkleRoot(entries) {
|
|
427
|
+
if (entries.length === 0) return null;
|
|
428
|
+
|
|
429
|
+
const hashes = entries.map(entry => bytesToHex(sha3_256(utf8ToBytes(JSON.stringify(entry)))));
|
|
430
|
+
|
|
431
|
+
// Simple merkle tree calculation
|
|
432
|
+
let currentLevel = hashes;
|
|
433
|
+
while (currentLevel.length > 1) {
|
|
434
|
+
const nextLevel = [];
|
|
435
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
436
|
+
const left = currentLevel[i];
|
|
437
|
+
const right = currentLevel[i + 1] || left;
|
|
438
|
+
const combined = left + right;
|
|
439
|
+
nextLevel.push(bytesToHex(sha3_256(utf8ToBytes(combined))));
|
|
440
|
+
}
|
|
441
|
+
currentLevel = nextLevel;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return currentLevel[0];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Git add operation
|
|
449
|
+
* @param {string} filePath - File to add
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
async _gitAdd(filePath) {
|
|
453
|
+
execSync(`git add "${filePath}"`, {
|
|
454
|
+
cwd: this.config.gitRepo,
|
|
455
|
+
stdio: 'pipe',
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Git commit operation
|
|
461
|
+
* @param {string} message - Commit message
|
|
462
|
+
* @param {Object} metadata - Commit metadata
|
|
463
|
+
* @returns {Promise<string>} Commit hash
|
|
464
|
+
* @private
|
|
465
|
+
*/
|
|
466
|
+
async _gitCommit(message, metadata = {}) {
|
|
467
|
+
const commitMessage = `${message}\n\nMetadata: ${JSON.stringify(metadata)}`;
|
|
468
|
+
|
|
469
|
+
const output = execSync(`git commit -m "${commitMessage}"`, {
|
|
470
|
+
cwd: this.config.gitRepo,
|
|
471
|
+
stdio: 'pipe',
|
|
472
|
+
encoding: 'utf8',
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Extract commit hash from output
|
|
476
|
+
const commitHash = output.trim().split('\n').pop();
|
|
477
|
+
return commitHash;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get latest commit hash
|
|
482
|
+
* @returns {Promise<string>} Latest commit hash
|
|
483
|
+
* @private
|
|
484
|
+
*/
|
|
485
|
+
async _getLatestCommit() {
|
|
486
|
+
const output = execSync('git rev-parse HEAD', {
|
|
487
|
+
cwd: this.config.gitRepo,
|
|
488
|
+
stdio: 'pipe',
|
|
489
|
+
encoding: 'utf8',
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return output.trim();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Verify signature
|
|
497
|
+
* @param {Object} entry - Entry to verify
|
|
498
|
+
* @returns {Promise<boolean>} Signature valid
|
|
499
|
+
* @private
|
|
500
|
+
*/
|
|
501
|
+
async _verifySignature(entry) {
|
|
502
|
+
// For now, just verify the hash matches
|
|
503
|
+
const data = JSON.stringify({
|
|
504
|
+
receipt: entry.receipt,
|
|
505
|
+
entryId: entry.id,
|
|
506
|
+
timestamp: entry.timestamp,
|
|
507
|
+
});
|
|
508
|
+
const expectedHash = bytesToHex(sha3_256(utf8ToBytes(data)));
|
|
509
|
+
|
|
510
|
+
return entry.signature.value === expectedHash;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Verify Git commit
|
|
515
|
+
* @param {string} commitHash - Commit hash
|
|
516
|
+
* @returns {Promise<boolean>} Commit valid
|
|
517
|
+
* @private
|
|
518
|
+
*/
|
|
519
|
+
async _verifyGitCommit(commitHash) {
|
|
520
|
+
try {
|
|
521
|
+
execSync(`git cat-file -t ${commitHash}`, {
|
|
522
|
+
cwd: this.config.gitRepo,
|
|
523
|
+
stdio: 'pipe',
|
|
524
|
+
});
|
|
525
|
+
return true;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Verify merkle root cryptographically
|
|
533
|
+
* @param {Object} entry - Entry to verify
|
|
534
|
+
* @returns {Promise<boolean>} Merkle root valid
|
|
535
|
+
* @private
|
|
536
|
+
*
|
|
537
|
+
* @description
|
|
538
|
+
* Validates the Merkle root by:
|
|
539
|
+
* 1. Extracting the entry data components (receipt, signature, timestamp, etc.)
|
|
540
|
+
* 2. Calculating the Merkle root from these components using SHA3-256
|
|
541
|
+
* 3. Comparing the calculated root with the stored entry.merkleRoot
|
|
542
|
+
*
|
|
543
|
+
* This ensures cryptographic integrity - any tampering with the entry data
|
|
544
|
+
* will cause the verification to fail.
|
|
545
|
+
*/
|
|
546
|
+
async _verifyMerkleRoot(entry) {
|
|
547
|
+
if (!entry.merkleRoot) {
|
|
548
|
+
return true; // No merkle root to verify
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
// Build canonical data representation for verification
|
|
553
|
+
// Include all critical entry components in deterministic order
|
|
554
|
+
const entryData = {
|
|
555
|
+
id: entry.id,
|
|
556
|
+
timestamp: entry.timestamp,
|
|
557
|
+
receipt: entry.receipt,
|
|
558
|
+
signature: entry.signature,
|
|
559
|
+
previousHash: entry.previousHash || null,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Calculate hash of entry data (leaf node in Merkle tree)
|
|
563
|
+
const entryJson = JSON.stringify(entryData);
|
|
564
|
+
const entryHash = bytesToHex(sha3_256(utf8ToBytes(entryJson)));
|
|
565
|
+
|
|
566
|
+
// For a single entry, the Merkle root should be the hash of the entry itself
|
|
567
|
+
// (a Merkle tree with one leaf node has that leaf as the root)
|
|
568
|
+
// If the entry was part of a batch, we verify against the batch's Merkle root
|
|
569
|
+
const calculatedRoot = entryHash;
|
|
570
|
+
|
|
571
|
+
// Compare calculated root with stored root
|
|
572
|
+
const isValid = calculatedRoot === entry.merkleRoot;
|
|
573
|
+
|
|
574
|
+
if (!isValid) {
|
|
575
|
+
console.error('[LockchainWriter] Merkle root verification failed', {
|
|
576
|
+
entryId: entry.id,
|
|
577
|
+
stored: entry.merkleRoot,
|
|
578
|
+
calculated: calculatedRoot,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return isValid;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error('[LockchainWriter] Error verifying Merkle root:', error);
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Create a lockchain writer instance
|
|
592
|
+
* @param {Object} [config] - Configuration
|
|
593
|
+
* @returns {LockchainWriter} Lockchain writer instance
|
|
594
|
+
*/
|
|
595
|
+
export function createLockchainWriter(config = {}) {
|
|
596
|
+
return new LockchainWriter(config);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Default lockchain writer instance
|
|
601
|
+
*/
|
|
602
|
+
export const defaultLockchainWriter = createLockchainWriter();
|