@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
package/src/lite.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file knowledge-engine/lite.mjs
|
|
3
|
+
* @description Minimal RDF functionality entry point for bundle size optimization
|
|
4
|
+
*
|
|
5
|
+
* This lite bundle exports ONLY the core RDF primitives from n3:
|
|
6
|
+
* - Store: In-memory quad storage
|
|
7
|
+
* - Parser: Turtle/N-Triples/N-Quads parsing
|
|
8
|
+
* - Writer: RDF serialization
|
|
9
|
+
* - DataFactory: RDF term creation
|
|
10
|
+
*
|
|
11
|
+
* EXCLUDES (for 60%+ bundle reduction):
|
|
12
|
+
* - @comunica/query-sparql (~2.5MB)
|
|
13
|
+
* - isolated-vm (~5MB native bindings)
|
|
14
|
+
* - testcontainers (dev-only)
|
|
15
|
+
* - eyereasoner
|
|
16
|
+
* - rdf-validate-shacl
|
|
17
|
+
* - All hook/policy infrastructure
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Minimal import for basic RDF operations
|
|
21
|
+
* import { Store, Parser, Writer, DataFactory, parseTurtle, toTurtle } from 'unrdf/knowledge-engine/lite';
|
|
22
|
+
*
|
|
23
|
+
* const store = createStore();
|
|
24
|
+
* const { namedNode, literal, quad } = DataFactory;
|
|
25
|
+
*
|
|
26
|
+
* store.addQuad(quad(
|
|
27
|
+
* namedNode('http://example.org/alice'),
|
|
28
|
+
* namedNode('http://xmlns.com/foaf/0.1/name'),
|
|
29
|
+
* literal('Alice')
|
|
30
|
+
* ));
|
|
31
|
+
*
|
|
32
|
+
* @module knowledge-engine/lite
|
|
33
|
+
* @see {@link https://github.com/rdfjs/N3.js} for N3.js documentation
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* JUSTIFIED N3 USAGE (μ(O) Compliance):
|
|
38
|
+
* This lite.mjs module is a justified boundary for N3 exports because:
|
|
39
|
+
* 1. It provides streaming parsing with backpressure control (N3.Parser)
|
|
40
|
+
* 2. It's explicitly designed for lightweight RDF operations without Oxigraph
|
|
41
|
+
* 3. Users opting into lite.mjs accept N3-based implementation
|
|
42
|
+
*
|
|
43
|
+
* For full Oxigraph-based functionality, use the main knowledge-engine exports.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
// Core N3 exports - the essential RDF primitives
|
|
47
|
+
export { Parser, Writer, UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
48
|
+
import { createStore as _createStore } from '@unrdf/oxigraph'; // Oxigraph Store implementation
|
|
49
|
+
export { _createStore as createStore };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a Turtle string into a Store (lite version - no OTEL tracing)
|
|
53
|
+
* @param {string} ttl - The Turtle string to parse
|
|
54
|
+
* @param {string} [baseIRI='http://example.org/'] - Base IRI for resolving relative URIs
|
|
55
|
+
* @returns {Promise<import('n3').Store>} Promise resolving to a Store containing the parsed quads
|
|
56
|
+
* @throws {TypeError} If ttl is not a string
|
|
57
|
+
* @throws {Error} If parsing fails
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const ttl = `
|
|
61
|
+
* @prefix ex: <http://example.org/> .
|
|
62
|
+
* ex:alice ex:knows ex:bob .
|
|
63
|
+
* `;
|
|
64
|
+
* const store = await parseTurtle(ttl);
|
|
65
|
+
*/
|
|
66
|
+
export async function parseTurtle(ttl, baseIRI = 'http://example.org/') {
|
|
67
|
+
if (typeof ttl !== 'string') {
|
|
68
|
+
throw new TypeError('parseTurtle: ttl must be a string');
|
|
69
|
+
}
|
|
70
|
+
if (typeof baseIRI !== 'string') {
|
|
71
|
+
throw new TypeError('parseTurtle: baseIRI must be a string');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { Parser } = await import('n3');
|
|
75
|
+
const parser = new Parser({ baseIRI });
|
|
76
|
+
const quads = parser.parse(ttl);
|
|
77
|
+
return _createStore(quads);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Serialize a Store to Turtle format (lite version - no OTEL tracing)
|
|
82
|
+
* @param {import('n3').Store} store - The store to serialize
|
|
83
|
+
* @param {Object} [options={}] - Serialization options
|
|
84
|
+
* @param {Object} [options.prefixes] - Prefix mappings for compact output
|
|
85
|
+
* @param {string} [options.baseIRI] - Base IRI for the output
|
|
86
|
+
* @returns {Promise<string>} Promise resolving to the Turtle string
|
|
87
|
+
* @throws {TypeError} If store is not a valid Store instance
|
|
88
|
+
* @throws {Error} If serialization fails
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const turtle = await toTurtle(store, {
|
|
92
|
+
* prefixes: { ex: 'http://example.org/' }
|
|
93
|
+
* });
|
|
94
|
+
*/
|
|
95
|
+
export async function toTurtle(store, options = {}) {
|
|
96
|
+
if (!store || typeof store.getQuads !== 'function') {
|
|
97
|
+
throw new TypeError('toTurtle: store must be a valid Store instance');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { Writer } = await import('n3');
|
|
101
|
+
const writer = new Writer({
|
|
102
|
+
format: 'Turtle',
|
|
103
|
+
prefixes: options.prefixes || {},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const quads = store.getQuads();
|
|
107
|
+
writer.addQuads(quads);
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
writer.end((error, result) => {
|
|
111
|
+
if (error) {
|
|
112
|
+
reject(new Error(`Failed to serialize to Turtle: ${error.message}`));
|
|
113
|
+
} else {
|
|
114
|
+
if (options.baseIRI) {
|
|
115
|
+
result = `@base <${options.baseIRI}> .\n\n${result}`;
|
|
116
|
+
}
|
|
117
|
+
resolve(result);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Serialize a Store to N-Quads format (lite version - no OTEL tracing)
|
|
125
|
+
* @param {import('n3').Store} store - The store to serialize
|
|
126
|
+
* @param {Object} [options={}] - Serialization options
|
|
127
|
+
* @returns {Promise<string>} Promise resolving to the N-Quads string
|
|
128
|
+
* @throws {TypeError} If store is not a valid Store instance
|
|
129
|
+
* @throws {Error} If serialization fails
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* const nquads = await toNQuads(store);
|
|
133
|
+
*/
|
|
134
|
+
export async function toNQuads(store, options = {}) {
|
|
135
|
+
if (!store || typeof store.getQuads !== 'function') {
|
|
136
|
+
throw new TypeError('toNQuads: store must be a valid Store instance');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { Writer } = await import('n3');
|
|
140
|
+
const writer = new Writer({
|
|
141
|
+
format: 'N-Quads',
|
|
142
|
+
...options,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const quads = store.getQuads();
|
|
146
|
+
writer.addQuads(quads);
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
writer.end((error, result) => {
|
|
150
|
+
if (error) {
|
|
151
|
+
reject(new Error(`Failed to serialize to N-Quads: ${error.message}`));
|
|
152
|
+
} else {
|
|
153
|
+
resolve(result);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a quad using DataFactory (convenience function)
|
|
161
|
+
* @param {string} subject - Subject IRI
|
|
162
|
+
* @param {string} predicate - Predicate IRI
|
|
163
|
+
* @param {string|{value: string, language?: string, datatype?: string}} object - Object value
|
|
164
|
+
* @param {string} [graph] - Optional graph IRI
|
|
165
|
+
* @returns {import('n3').Quad} The created quad
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const q = createQuad(
|
|
169
|
+
* 'http://example.org/alice',
|
|
170
|
+
* 'http://xmlns.com/foaf/0.1/name',
|
|
171
|
+
* { value: 'Alice', language: 'en' }
|
|
172
|
+
* );
|
|
173
|
+
*/
|
|
174
|
+
export function createQuad(subject, predicate, object, graph) {
|
|
175
|
+
const { DataFactory } = require('n3');
|
|
176
|
+
const { namedNode, literal, quad, defaultGraph } = DataFactory;
|
|
177
|
+
|
|
178
|
+
const subjectNode = namedNode(subject);
|
|
179
|
+
const predicateNode = namedNode(predicate);
|
|
180
|
+
|
|
181
|
+
let objectNode;
|
|
182
|
+
if (typeof object === 'string') {
|
|
183
|
+
// Check if it looks like a URI
|
|
184
|
+
if (
|
|
185
|
+
object.startsWith('http://') ||
|
|
186
|
+
object.startsWith('https://') ||
|
|
187
|
+
object.startsWith('urn:')
|
|
188
|
+
) {
|
|
189
|
+
objectNode = namedNode(object);
|
|
190
|
+
} else {
|
|
191
|
+
objectNode = literal(object);
|
|
192
|
+
}
|
|
193
|
+
} else if (typeof object === 'object') {
|
|
194
|
+
if (object.language) {
|
|
195
|
+
objectNode = literal(object.value, object.language);
|
|
196
|
+
} else if (object.datatype) {
|
|
197
|
+
objectNode = literal(object.value, namedNode(object.datatype));
|
|
198
|
+
} else {
|
|
199
|
+
objectNode = literal(object.value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const graphNode = graph ? namedNode(graph) : defaultGraph();
|
|
204
|
+
return quad(subjectNode, predicateNode, objectNode, graphNode);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Bundle size comparison (approximate):
|
|
209
|
+
*
|
|
210
|
+
* Full knowledge-engine: ~8-10MB
|
|
211
|
+
* - @comunica/query-sparql: ~2.5MB
|
|
212
|
+
* - isolated-vm: ~5MB (native)
|
|
213
|
+
* - eyereasoner: ~500KB
|
|
214
|
+
* - rdf-validate-shacl: ~200KB
|
|
215
|
+
* - n3: ~150KB
|
|
216
|
+
* - hooks/policy: ~100KB
|
|
217
|
+
*
|
|
218
|
+
* Lite knowledge-engine: ~150KB
|
|
219
|
+
* - n3 only: ~150KB
|
|
220
|
+
*
|
|
221
|
+
* Savings: ~98% for basic RDF operations
|
|
222
|
+
*/
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Browser-compatible Lockchain Writer
|
|
3
|
+
* @module lockchain-writer-browser
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Browser-compatible version of the lockchain writer that stores entries in memory
|
|
7
|
+
* or browser storage instead of Git commits. Provides cryptographic integrity
|
|
8
|
+
* verification without Git dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID, createHash, _fs } from './browser-shims.mjs';
|
|
12
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
13
|
+
import { _blake3 } from '@noble/hashes/blake3.js';
|
|
14
|
+
import { utf8ToBytes, bytesToHex } from '@noble/hashes/utils.js';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Schema for lockchain entry
|
|
19
|
+
*/
|
|
20
|
+
const LockchainEntrySchema = z.object({
|
|
21
|
+
id: z.string().uuid(),
|
|
22
|
+
timestamp: z.number(),
|
|
23
|
+
receipt: z.any(), // Transaction receipt
|
|
24
|
+
signature: z.object({
|
|
25
|
+
algorithm: z.string(),
|
|
26
|
+
value: z.string(),
|
|
27
|
+
publicKey: z.string().optional(),
|
|
28
|
+
}),
|
|
29
|
+
previousHash: z.string().optional().nullable(),
|
|
30
|
+
merkleRoot: z.string().optional(),
|
|
31
|
+
storageKey: z.string().optional(), // Browser storage key instead of git commit
|
|
32
|
+
storageRef: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Schema for lockchain configuration
|
|
37
|
+
*/
|
|
38
|
+
const LockchainConfigSchema = z.object({
|
|
39
|
+
storageType: z.enum(['memory', 'localStorage', 'sessionStorage', 'indexedDB']).default('memory'),
|
|
40
|
+
algorithm: z.enum(['sha256', 'sha3-256', 'blake3']).default('sha3-256'),
|
|
41
|
+
batchSize: z.number().int().positive().default(10),
|
|
42
|
+
enableMerkle: z.boolean().default(true),
|
|
43
|
+
enablePersistence: z.boolean().default(true),
|
|
44
|
+
storagePrefix: z.string().default('lockchain'),
|
|
45
|
+
maxAgeMs: z.number().int().positive().optional(), // Time to live
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Browser-compatible Lockchain Writer
|
|
50
|
+
*/
|
|
51
|
+
export class BrowserLockchainWriter {
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
*/
|
|
55
|
+
constructor(config = {}) {
|
|
56
|
+
const validatedConfig = LockchainConfigSchema.parse({
|
|
57
|
+
...config,
|
|
58
|
+
// Override Git-specific options for browser
|
|
59
|
+
enableGitAnchoring: false,
|
|
60
|
+
gitRepo: undefined,
|
|
61
|
+
});
|
|
62
|
+
this.config = validatedConfig;
|
|
63
|
+
|
|
64
|
+
// Initialize storage
|
|
65
|
+
this.entries = [];
|
|
66
|
+
this.lastHash = null;
|
|
67
|
+
this.processingBatches = new Set();
|
|
68
|
+
|
|
69
|
+
// Browser storage API
|
|
70
|
+
this.storage = this._initStorage();
|
|
71
|
+
|
|
72
|
+
// Load existing entries
|
|
73
|
+
this._loadEntries().catch(err => {
|
|
74
|
+
console.warn('Failed to load entries during initialization:', err.message);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize browser storage according to config
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
_initStorage() {
|
|
83
|
+
switch (this.config.storageType) {
|
|
84
|
+
case 'localStorage':
|
|
85
|
+
return {
|
|
86
|
+
get: key => globalThis?.localStorage?.getItem(`${this.config.storagePrefix}_${key}`),
|
|
87
|
+
set: (key, value) =>
|
|
88
|
+
globalThis?.localStorage?.setItem(`${this.config.storagePrefix}_${key}`, value),
|
|
89
|
+
remove: key =>
|
|
90
|
+
globalThis?.localStorage?.removeItem(`${this.config.storagePrefix}_${key}`),
|
|
91
|
+
};
|
|
92
|
+
case 'sessionStorage':
|
|
93
|
+
return {
|
|
94
|
+
get: key => globalThis?.sessionStorage?.getItem(`${this.config.storagePrefix}_${key}`),
|
|
95
|
+
set: (key, value) =>
|
|
96
|
+
globalThis?.sessionStorage?.setItem(`${this.config.storagePrefix}_${key}`, value),
|
|
97
|
+
remove: key =>
|
|
98
|
+
globalThis?.sessionStorage?.removeItem(`${this.config.storagePrefix}_${key}`),
|
|
99
|
+
};
|
|
100
|
+
case 'indexedDB':
|
|
101
|
+
// Simplified IndexedDB implementation would go here
|
|
102
|
+
console.warn('IndexedDB storage not yet implemented, falling back to memory');
|
|
103
|
+
return this._initMemoryStorage();
|
|
104
|
+
default:
|
|
105
|
+
return this._initMemoryStorage();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Initialize memory-based storage
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_initMemoryStorage() {
|
|
114
|
+
const memoryStorage = new Map();
|
|
115
|
+
return {
|
|
116
|
+
get: key => memoryStorage.get(key),
|
|
117
|
+
set: (key, value) => memoryStorage.set(key, value),
|
|
118
|
+
remove: key => memoryStorage.delete(key),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Load existing entries from storage
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
async _loadEntries() {
|
|
127
|
+
try {
|
|
128
|
+
const entriesKey = `${this.config.storagePrefix}_entries`;
|
|
129
|
+
const entriesData = this.storage.get(entriesKey);
|
|
130
|
+
|
|
131
|
+
if (entriesData) {
|
|
132
|
+
this.entries = JSON.parse(entriesData);
|
|
133
|
+
|
|
134
|
+
// Restore last hash
|
|
135
|
+
const lastEntry = this.entries[this.entries.length - 1];
|
|
136
|
+
if (lastEntry) {
|
|
137
|
+
this.lastHash = await this._calculateEntryHash(lastEntry);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.warn('Failed to load existing entries:', error.message);
|
|
142
|
+
this.entries = [];
|
|
143
|
+
this.lastHash = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Save entries to storage
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
async _saveEntries() {
|
|
152
|
+
try {
|
|
153
|
+
const entriesKey = `${this.config.storagePrefix}_entries`;
|
|
154
|
+
this.storage.set(entriesKey, JSON.stringify(this.entries));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.warn('Failed to save entries:', error.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Write a receipt to the lockchain
|
|
162
|
+
* @param {Object} receipt - Transaction receipt
|
|
163
|
+
* @param {Object} [options] - Write options
|
|
164
|
+
* @returns {Promise<Object>} Lockchain entry
|
|
165
|
+
*/
|
|
166
|
+
async writeReceipt(receipt, options = {}) {
|
|
167
|
+
const entryId = randomUUID();
|
|
168
|
+
const timestamp = Date.now();
|
|
169
|
+
|
|
170
|
+
// Create lockchain entry
|
|
171
|
+
const entry = {
|
|
172
|
+
id: entryId,
|
|
173
|
+
timestamp,
|
|
174
|
+
receipt: this._serializeReceipt(receipt),
|
|
175
|
+
signature: await this._signEntry(receipt, entryId, timestamp),
|
|
176
|
+
previousHash: this.lastHash || null,
|
|
177
|
+
merkleRoot: options.merkleRoot,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Validate entry
|
|
181
|
+
const validatedEntry = LockchainEntrySchema.parse(entry);
|
|
182
|
+
|
|
183
|
+
// Store entry
|
|
184
|
+
await this._storeEntry(validatedEntry);
|
|
185
|
+
|
|
186
|
+
// Update hash chain
|
|
187
|
+
this.lastHash = await this._calculateEntryHash(validatedEntry);
|
|
188
|
+
|
|
189
|
+
// Persist to storage
|
|
190
|
+
await this._saveEntries();
|
|
191
|
+
|
|
192
|
+
return validatedEntry;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Write multiple receipts in a batch
|
|
197
|
+
* @param {Array} receipts - Array of transaction receipts
|
|
198
|
+
* @param {Object} [options] - Batch options
|
|
199
|
+
* @returns {Promise<Array>} Array of lockchain entries
|
|
200
|
+
*/
|
|
201
|
+
async writeReceiptBatch(receipts, _options = {}) {
|
|
202
|
+
const batchId = `batch_${randomUUID().slice(0, 8)}`;
|
|
203
|
+
const validatedReceipts = receipts.map(r => (typeof r === 'string' ? JSON.parse(r) : r));
|
|
204
|
+
|
|
205
|
+
const batchPromise = this._processBatch(validatedReceipts, batchId);
|
|
206
|
+
this.processingBatches.add(batchId);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const entries = await batchPromise;
|
|
210
|
+
this.processingBatches.delete(batchId);
|
|
211
|
+
return entries;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.processingBatches.delete(batchId);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Process a batch of receipts
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
async _processBatch(receipts, batchId) {
|
|
223
|
+
const entries = [];
|
|
224
|
+
const merkleRoot = this.config.enableMerkle ? await this._calculateMerkleRoot(receipts) : null;
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
227
|
+
const receipt = receipts[i];
|
|
228
|
+
const entry = await this.writeReceipt(receipt, {
|
|
229
|
+
batchId,
|
|
230
|
+
batchIndex: i,
|
|
231
|
+
merkleRoot,
|
|
232
|
+
});
|
|
233
|
+
entries.push(entry);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return entries;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Calculate Merkle root from receipts
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
async _calculateMerkleRoot(receipts) {
|
|
244
|
+
const hashes = await Promise.all(
|
|
245
|
+
receipts.map(async receipt => {
|
|
246
|
+
const serialized = this._serializeReceipt(receipt);
|
|
247
|
+
return bytesToHex(sha3_256(utf8ToBytes(JSON.stringify(serialized))));
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Simple binary tree merkle calculation
|
|
252
|
+
let current = hashes;
|
|
253
|
+
while (current.length > 1) {
|
|
254
|
+
const next = [];
|
|
255
|
+
for (let i = 0; i < current.length; i += 2) {
|
|
256
|
+
const left = current[i];
|
|
257
|
+
const right = current[i + 1] || current[i]; // Pad with self if odd
|
|
258
|
+
const combined = left + right;
|
|
259
|
+
next.push(bytesToHex(sha3_256(utf8ToBytes(combined))));
|
|
260
|
+
}
|
|
261
|
+
current = next;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return current[0];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Store a single entry
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
async _storeEntry(entry) {
|
|
272
|
+
// Store individual entry
|
|
273
|
+
const storageKey = `entry_${entry.id}`;
|
|
274
|
+
this.storage.set(storageKey, JSON.stringify(entry));
|
|
275
|
+
|
|
276
|
+
// Update entries array
|
|
277
|
+
this.entries.push({
|
|
278
|
+
...entry,
|
|
279
|
+
storageKey,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Serialize receipt for storage
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
_serializeReceipt(receipt) {
|
|
288
|
+
if (typeof receipt === 'string') {
|
|
289
|
+
return JSON.parse(receipt);
|
|
290
|
+
}
|
|
291
|
+
return receipt;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Sign an entry
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
async _signEntry(receipt, entryId, timestamp) {
|
|
299
|
+
const hash = createHash(this.config.algorithm);
|
|
300
|
+
const signature = hash.update(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
receipt,
|
|
303
|
+
entryId,
|
|
304
|
+
timestamp,
|
|
305
|
+
previousHash: this.lastHash,
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
algorithm: this.config.algorithm,
|
|
311
|
+
value: await signature.digest('hex'),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Calculate hash for an entry
|
|
317
|
+
* @private
|
|
318
|
+
*/
|
|
319
|
+
async _calculateEntryHash(entry) {
|
|
320
|
+
const hash = createHash(this.config.algorithm);
|
|
321
|
+
return await hash.update(JSON.stringify(entry)).digest('hex');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Verify lockchain integrity
|
|
326
|
+
* @returns {Promise<Object>} Verification result
|
|
327
|
+
*/
|
|
328
|
+
async verifyIntegrity() {
|
|
329
|
+
const results = [];
|
|
330
|
+
let previousHash = null;
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
333
|
+
const entry = this.entries[i];
|
|
334
|
+
const expectedHash = await this._calculateEntryHash(entry);
|
|
335
|
+
|
|
336
|
+
const isValid =
|
|
337
|
+
expectedHash === entry.previousHash &&
|
|
338
|
+
(previousHash === null || entry.previousHash === previousHash);
|
|
339
|
+
|
|
340
|
+
results.push({
|
|
341
|
+
index: i,
|
|
342
|
+
entryId: entry.id,
|
|
343
|
+
valid: isValid,
|
|
344
|
+
hash: expectedHash,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (!isValid) {
|
|
348
|
+
break; // Chain is broken
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
previousHash = expectedHash;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const isValid = results.every(r => r.valid);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
valid: isValid,
|
|
358
|
+
totalEntries: this.entries.length,
|
|
359
|
+
results,
|
|
360
|
+
verificationTimestamp: Date.now(),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get lockchain statistics
|
|
366
|
+
* @returns {Object} Statistics
|
|
367
|
+
*/
|
|
368
|
+
getStats() {
|
|
369
|
+
return {
|
|
370
|
+
totalEntries: this.entries.length,
|
|
371
|
+
processingBatches: this.processingBatches.size,
|
|
372
|
+
config: this.config,
|
|
373
|
+
lastHash: this.lastHash,
|
|
374
|
+
storageType: this.config.storageType,
|
|
375
|
+
oldestEntry: this.entries.length > 0 ? this.entries[0].timestamp : null,
|
|
376
|
+
newestEntry: this.entries.length > 0 ? this.entries[this.entries.length - 1].timestamp : null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Clear all entries (use with caution)
|
|
382
|
+
*/
|
|
383
|
+
async clear() {
|
|
384
|
+
this.entries = [];
|
|
385
|
+
this.lastHash = null;
|
|
386
|
+
this.processingBatches.clear();
|
|
387
|
+
|
|
388
|
+
if (this.config.enablePersistence) {
|
|
389
|
+
// Clear persisted entries
|
|
390
|
+
this.entries.forEach(entry => {
|
|
391
|
+
if (entry.storageKey) {
|
|
392
|
+
this.storage.remove(entry.storageKey);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const entriesKey = `${this.config.storagePrefix}_entries`;
|
|
397
|
+
this.storage.remove(entriesKey);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Create a browser-compatible lockchain writer
|
|
404
|
+
* @param {Object} [config] - Lockchain configuration
|
|
405
|
+
* @returns {BrowserLockchainWriter} New writer instance
|
|
406
|
+
*/
|
|
407
|
+
export function createBrowserLockchainWriter(config = {}) {
|
|
408
|
+
return new BrowserLockchainWriter(config);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Export for compatibility
|
|
412
|
+
export { BrowserLockchainWriter as LockchainWriter };
|
|
413
|
+
|
|
414
|
+
export default BrowserLockchainWriter;
|