@yamo/memory-mesh 2.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 +80 -0
- package/bin/memory_mesh.js +69 -0
- package/bin/scrubber.js +81 -0
- package/index.d.ts +111 -0
- package/lib/adapters/index.js +3 -0
- package/lib/embeddings/factory.js +150 -0
- package/lib/embeddings/index.js +2 -0
- package/lib/embeddings/service.js +586 -0
- package/lib/index.js +18 -0
- package/lib/lancedb/client.js +631 -0
- package/lib/lancedb/config.js +215 -0
- package/lib/lancedb/errors.js +144 -0
- package/lib/lancedb/index.js +4 -0
- package/lib/lancedb/schema.js +197 -0
- package/lib/memory/index.js +3 -0
- package/lib/memory/memory-context-manager.js +388 -0
- package/lib/memory/memory-mesh.js +910 -0
- package/lib/memory/memory-translator.js +130 -0
- package/lib/memory/migrate-memory.js +227 -0
- package/lib/memory/migrate-to-v2.js +120 -0
- package/lib/memory/scorer.js +85 -0
- package/lib/memory/vector-memory.js +364 -0
- package/lib/privacy/audit-logger.js +176 -0
- package/lib/privacy/dlp-redactor.js +72 -0
- package/lib/privacy/index.js +10 -0
- package/lib/reporting/skill-report-generator.js +283 -0
- package/lib/scrubber/.gitkeep +1 -0
- package/lib/scrubber/config/defaults.js +62 -0
- package/lib/scrubber/errors/scrubber-error.js +43 -0
- package/lib/scrubber/index.js +25 -0
- package/lib/scrubber/scrubber.js +130 -0
- package/lib/scrubber/stages/chunker.js +103 -0
- package/lib/scrubber/stages/metadata-annotator.js +74 -0
- package/lib/scrubber/stages/normalizer.js +59 -0
- package/lib/scrubber/stages/semantic-filter.js +61 -0
- package/lib/scrubber/stages/structural-cleaner.js +82 -0
- package/lib/scrubber/stages/validator.js +66 -0
- package/lib/scrubber/telemetry.js +66 -0
- package/lib/scrubber/utils/hash.js +39 -0
- package/lib/scrubber/utils/html-parser.js +45 -0
- package/lib/scrubber/utils/pattern-matcher.js +63 -0
- package/lib/scrubber/utils/token-counter.js +31 -0
- package/lib/search/filter.js +275 -0
- package/lib/search/hybrid.js +137 -0
- package/lib/search/index.js +3 -0
- package/lib/search/pattern-miner.js +160 -0
- package/lib/utils/error-sanitizer.js +84 -0
- package/lib/utils/handoff-validator.js +85 -0
- package/lib/utils/index.js +4 -0
- package/lib/utils/spinner.js +190 -0
- package/lib/utils/streaming-client.js +128 -0
- package/package.json +39 -0
- package/skills/SKILL.md +462 -0
- package/skills/skill-scrubber.yamo +41 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vector Memory Manager
|
|
8
|
+
* Lightweight vector database for YAMO Skills
|
|
9
|
+
* Uses simple cosine similarity for semantic search
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This is a lightweight implementation. For production with large datasets,
|
|
12
|
+
* consider integrating LanceDB: npm install @lancedb/lancedb
|
|
13
|
+
*/
|
|
14
|
+
class VectorMemory {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.storePath = options.storePath || path.join(__dirname, '..', 'data', 'vector_memory.json');
|
|
17
|
+
this.embeddingDim = options.embeddingDim || 384; // Default for sentence transformers
|
|
18
|
+
this.maxEntries = options.maxEntries || 10000;
|
|
19
|
+
this.ensureStorageDirectory();
|
|
20
|
+
this.load();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure storage directory exists
|
|
25
|
+
*/
|
|
26
|
+
ensureStorageDirectory() {
|
|
27
|
+
const dir = path.dirname(this.storePath);
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load existing memory from disk
|
|
35
|
+
*/
|
|
36
|
+
load() {
|
|
37
|
+
if (fs.existsSync(this.storePath)) {
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(fs.readFileSync(this.storePath, 'utf8'));
|
|
40
|
+
this.memories = data.memories || [];
|
|
41
|
+
this.metadata = data.metadata || { version: '1.0', created: new Date().toISOString() };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
console.error('Failed to load vector memory:', message);
|
|
45
|
+
this.vectorStore = [];
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
this.memories = [];
|
|
49
|
+
this.metadata = { version: '1.0', created: new Date().toISOString() };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save memory to disk
|
|
55
|
+
*/
|
|
56
|
+
save() {
|
|
57
|
+
const data = {
|
|
58
|
+
metadata: {
|
|
59
|
+
...this.metadata,
|
|
60
|
+
last_updated: new Date().toISOString(),
|
|
61
|
+
entry_count: this.memories.length
|
|
62
|
+
},
|
|
63
|
+
memories: this.memories
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
fs.writeFileSync(this.storePath, JSON.stringify(data, null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate a simple embedding from text
|
|
71
|
+
* NOTE: This is a basic implementation. For production, use a proper embedding model like:
|
|
72
|
+
* - sentence-transformers (Python)
|
|
73
|
+
* - @xenova/transformers (JavaScript)
|
|
74
|
+
* - OpenAI embeddings API
|
|
75
|
+
*/
|
|
76
|
+
generateEmbedding(text) {
|
|
77
|
+
// Simple word-based embedding (not semantic, but functional for demo)
|
|
78
|
+
const words = text.toLowerCase().split(/\s+/);
|
|
79
|
+
const embedding = new Array(this.embeddingDim).fill(0);
|
|
80
|
+
|
|
81
|
+
// Hash each word and update embedding
|
|
82
|
+
words.forEach((word, idx) => {
|
|
83
|
+
const hash = crypto.createHash('sha256').update(word).digest();
|
|
84
|
+
for (let i = 0; i < this.embeddingDim; i++) {
|
|
85
|
+
embedding[i] += hash[i % hash.length] / 255;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Normalize
|
|
90
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
91
|
+
return embedding.map(val => val / (magnitude || 1));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate cosine similarity between two vectors
|
|
96
|
+
*/
|
|
97
|
+
cosineSimilarity(vecA, vecB) {
|
|
98
|
+
if (vecA.length !== vecB.length) {
|
|
99
|
+
throw new Error('Vectors must have same dimensions');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dotProduct = vecA.reduce((sum, val, i) => sum + val * vecB[i], 0);
|
|
103
|
+
const magnitudeA = Math.sqrt(vecA.reduce((sum, val) => sum + val * val, 0));
|
|
104
|
+
const magnitudeB = Math.sqrt(vecB.reduce((sum, val) => sum + val * val, 0));
|
|
105
|
+
|
|
106
|
+
return dotProduct / ((magnitudeA * magnitudeB) || 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Store a memory with automatic embedding generation
|
|
111
|
+
* @param {string} content - Text content to store
|
|
112
|
+
* @param {Object} metadata - Additional metadata
|
|
113
|
+
* @returns {Object} - Created memory entry
|
|
114
|
+
*/
|
|
115
|
+
store(content, metadata = {}) {
|
|
116
|
+
const embedding = this.generateEmbedding(content);
|
|
117
|
+
|
|
118
|
+
const memory = {
|
|
119
|
+
id: crypto.randomUUID(),
|
|
120
|
+
content,
|
|
121
|
+
embedding,
|
|
122
|
+
metadata: {
|
|
123
|
+
...metadata,
|
|
124
|
+
created_at: new Date().toISOString(),
|
|
125
|
+
content_length: content.length
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
this.memories.push(memory);
|
|
130
|
+
|
|
131
|
+
// Enforce max entries (FIFO)
|
|
132
|
+
if (this.memories.length > this.maxEntries) {
|
|
133
|
+
this.memories.shift();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.save();
|
|
137
|
+
return memory;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Search for similar memories
|
|
142
|
+
* @param {string} query - Search query
|
|
143
|
+
* @param {number} topK - Number of results to return
|
|
144
|
+
* @param {number} threshold - Minimum similarity threshold (0-1)
|
|
145
|
+
* @returns {Array} - Array of matching memories with scores
|
|
146
|
+
*/
|
|
147
|
+
search(query, topK = 5, threshold = 0.0) {
|
|
148
|
+
const queryEmbedding = this.generateEmbedding(query);
|
|
149
|
+
|
|
150
|
+
// Calculate similarity for all memories
|
|
151
|
+
const results = this.memories.map(memory => ({
|
|
152
|
+
...memory,
|
|
153
|
+
similarity: this.cosineSimilarity(queryEmbedding, memory.embedding)
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
// Filter by threshold and sort by similarity
|
|
157
|
+
return results
|
|
158
|
+
.filter(r => r.similarity >= threshold)
|
|
159
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
160
|
+
.slice(0, topK)
|
|
161
|
+
.map(({ embedding, ...rest }) => rest); // Remove embedding from results
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Retrieve memory by ID
|
|
166
|
+
*/
|
|
167
|
+
retrieve(id) {
|
|
168
|
+
return this.memories.find(m => m.id === id);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Delete memory by ID
|
|
173
|
+
*/
|
|
174
|
+
delete(id) {
|
|
175
|
+
const initialLength = this.memories.length;
|
|
176
|
+
this.memories = this.memories.filter(m => m.id !== id);
|
|
177
|
+
|
|
178
|
+
if (this.memories.length < initialLength) {
|
|
179
|
+
this.save();
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clear all memories
|
|
187
|
+
*/
|
|
188
|
+
clear() {
|
|
189
|
+
this.memories = [];
|
|
190
|
+
this.save();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get statistics about stored memories
|
|
195
|
+
*/
|
|
196
|
+
stats() {
|
|
197
|
+
return {
|
|
198
|
+
total_memories: this.memories.length,
|
|
199
|
+
embedding_dim: this.embeddingDim,
|
|
200
|
+
max_entries: this.maxEntries,
|
|
201
|
+
storage_path: this.storePath,
|
|
202
|
+
metadata: this.metadata,
|
|
203
|
+
avg_content_length: this.memories.length > 0
|
|
204
|
+
? Math.round(this.memories.reduce((sum, m) => sum + m.metadata.content_length, 0) / this.memories.length)
|
|
205
|
+
: 0
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Semantic clustering (group similar memories)
|
|
211
|
+
*/
|
|
212
|
+
cluster(similarityThreshold = 0.7) {
|
|
213
|
+
const clusters = [];
|
|
214
|
+
const processed = new Set();
|
|
215
|
+
|
|
216
|
+
this.memories.forEach((memory, idx) => {
|
|
217
|
+
if (processed.has(idx)) return;
|
|
218
|
+
|
|
219
|
+
const cluster = {
|
|
220
|
+
representative: memory.content.substring(0, 100),
|
|
221
|
+
members: [{ id: memory.id, content: memory.content }]
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Find similar memories
|
|
225
|
+
this.memories.forEach((other, otherIdx) => {
|
|
226
|
+
if (idx === otherIdx || processed.has(otherIdx)) return;
|
|
227
|
+
|
|
228
|
+
const similarity = this.cosineSimilarity(memory.embedding, other.embedding);
|
|
229
|
+
if (similarity >= similarityThreshold) {
|
|
230
|
+
cluster.members.push({ id: other.id, content: other.content });
|
|
231
|
+
processed.add(otherIdx);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
processed.add(idx);
|
|
236
|
+
clusters.push(cluster);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return clusters;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Export memories to JSON
|
|
244
|
+
*/
|
|
245
|
+
export() {
|
|
246
|
+
return {
|
|
247
|
+
metadata: this.metadata,
|
|
248
|
+
memories: this.memories.map(({ embedding, ...rest }) => rest)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Import memories from JSON
|
|
254
|
+
*/
|
|
255
|
+
import(data) {
|
|
256
|
+
if (data.memories) {
|
|
257
|
+
data.memories.forEach(memory => {
|
|
258
|
+
if (!memory.embedding) {
|
|
259
|
+
memory.embedding = this.generateEmbedding(memory.content);
|
|
260
|
+
}
|
|
261
|
+
this.memories.push(memory);
|
|
262
|
+
});
|
|
263
|
+
this.save();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// CLI usage
|
|
269
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
270
|
+
const memory = new VectorMemory();
|
|
271
|
+
const command = process.argv[2];
|
|
272
|
+
|
|
273
|
+
if (command === 'store') {
|
|
274
|
+
const content = process.argv.slice(3).join(' ');
|
|
275
|
+
if (!content) {
|
|
276
|
+
console.log('Usage: node vector_memory.js store <content>');
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = memory.store(content, { source: 'cli' });
|
|
281
|
+
console.log('✅ Stored:', result.id);
|
|
282
|
+
console.log(' Content:', content.substring(0, 100) + '...');
|
|
283
|
+
|
|
284
|
+
} else if (command === 'search') {
|
|
285
|
+
const query = process.argv.slice(3).join(' ');
|
|
286
|
+
if (!query) {
|
|
287
|
+
console.log('Usage: node vector_memory.js search <query>');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const results = memory.search(query, 5);
|
|
292
|
+
console.log(`\n🔍 Search results for: "${query}"\n`);
|
|
293
|
+
|
|
294
|
+
if (results.length === 0) {
|
|
295
|
+
console.log('No results found');
|
|
296
|
+
} else {
|
|
297
|
+
results.forEach((result, idx) => {
|
|
298
|
+
console.log(`${idx + 1}. [${(result.similarity * 100).toFixed(1)}%] ${result.content.substring(0, 100)}...`);
|
|
299
|
+
console.log(` ID: ${result.id}`);
|
|
300
|
+
console.log(` Created: ${result.metadata.created_at}`);
|
|
301
|
+
console.log('');
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
} else if (command === 'stats') {
|
|
306
|
+
const stats = memory.stats();
|
|
307
|
+
console.log('\n📊 Vector Memory Statistics\n');
|
|
308
|
+
console.log(`Total Memories: ${stats.total_memories}`);
|
|
309
|
+
console.log(`Embedding Dimensions: ${stats.embedding_dim}`);
|
|
310
|
+
console.log(`Max Entries: ${stats.max_entries}`);
|
|
311
|
+
console.log(`Average Content Length: ${stats.avg_content_length} chars`);
|
|
312
|
+
console.log(`Storage Path: ${stats.storage_path}`);
|
|
313
|
+
|
|
314
|
+
} else if (command === 'cluster') {
|
|
315
|
+
const clusters = memory.cluster(0.7);
|
|
316
|
+
console.log(`\n🔗 Found ${clusters.length} clusters\n`);
|
|
317
|
+
|
|
318
|
+
clusters.forEach((cluster, idx) => {
|
|
319
|
+
console.log(`Cluster ${idx + 1}: ${cluster.members.length} memories`);
|
|
320
|
+
console.log(` Representative: ${cluster.representative}...`);
|
|
321
|
+
console.log('');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
} else if (command === 'clear') {
|
|
325
|
+
memory.clear();
|
|
326
|
+
console.log('✅ All memories cleared');
|
|
327
|
+
|
|
328
|
+
} else if (command === 'test') {
|
|
329
|
+
console.log('🧪 Testing Vector Memory\n');
|
|
330
|
+
|
|
331
|
+
// Store some test memories
|
|
332
|
+
console.log('Storing test memories...');
|
|
333
|
+
memory.store('How to implement authentication in Node.js using JWT', { type: 'tutorial' });
|
|
334
|
+
memory.store('Best practices for securing API endpoints', { type: 'security' });
|
|
335
|
+
memory.store('Node.js JWT authentication guide', { type: 'tutorial' });
|
|
336
|
+
memory.store('How to use Docker for development', { type: 'devops' });
|
|
337
|
+
memory.store('Setting up HTTPS with Let\'s Encrypt', { type: 'security' });
|
|
338
|
+
|
|
339
|
+
console.log('✅ Stored 5 test memories\n');
|
|
340
|
+
|
|
341
|
+
// Search
|
|
342
|
+
console.log('Searching for "JWT authentication"...');
|
|
343
|
+
const results = memory.search('JWT authentication', 3);
|
|
344
|
+
console.log(`Found ${results.length} results:\n`);
|
|
345
|
+
|
|
346
|
+
results.forEach((result, idx) => {
|
|
347
|
+
console.log(`${idx + 1}. [${(result.similarity * 100).toFixed(1)}%] ${result.content}`);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
console.log('\n✅ Test completed');
|
|
351
|
+
|
|
352
|
+
} else {
|
|
353
|
+
console.log('YAMO Vector Memory CLI');
|
|
354
|
+
console.log('Usage:');
|
|
355
|
+
console.log(' node vector_memory.js store <content> - Store a memory');
|
|
356
|
+
console.log(' node vector_memory.js search <query> - Search for similar memories');
|
|
357
|
+
console.log(' node vector_memory.js stats - Show statistics');
|
|
358
|
+
console.log(' node vector_memory.js cluster - Find semantic clusters');
|
|
359
|
+
console.log(' node vector_memory.js clear - Clear all memories');
|
|
360
|
+
console.log(' node vector_memory.js test - Run test');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export default VectorMemory;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tamper-Proof Audit Logger
|
|
8
|
+
* Implements blockchain-like integrity using HMAC chain with file locking
|
|
9
|
+
*/
|
|
10
|
+
class AuditLogger {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
this.logPath = options.logPath || path.join(process.cwd(), 'logs', 'audit.log');
|
|
14
|
+
this.lockPath = this.logPath + '.lock';
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
this.lockTimeout = options.lockTimeout || 5000;
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
this.lockRetryDelay = options.lockRetryDelay || 50;
|
|
19
|
+
this.secret = process.env.AUDIT_SECRET || this.generateSecret();
|
|
20
|
+
this.ensureLogDirectory();
|
|
21
|
+
|
|
22
|
+
if (!process.env.AUDIT_SECRET) {
|
|
23
|
+
console.warn('⚠️ Using auto-generated AUDIT_SECRET. Set AUDIT_SECRET in .env for production');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ensureLogDirectory() {
|
|
28
|
+
const logDir = path.dirname(this.logPath);
|
|
29
|
+
if (!fs.existsSync(logDir)) {
|
|
30
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
generateSecret() {
|
|
35
|
+
return crypto.randomBytes(32).toString('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
acquireLock() {
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
const lockData = {
|
|
41
|
+
pid: process.pid,
|
|
42
|
+
timestamp: new Date().toISOString()
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
while (Date.now() - startTime < this.lockTimeout) {
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(this.lockPath, JSON.stringify(lockData), { flag: 'wx' });
|
|
48
|
+
return true;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
if (err && err.code === 'EEXIST') {
|
|
52
|
+
try {
|
|
53
|
+
const existingLock = JSON.parse(fs.readFileSync(this.lockPath, 'utf8'));
|
|
54
|
+
const lockAge = Date.now() - new Date(existingLock.timestamp).getTime();
|
|
55
|
+
if (lockAge > this.lockTimeout) {
|
|
56
|
+
fs.unlinkSync(this.lockPath);
|
|
57
|
+
}
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
while (Date.now() - start < this.lockRetryDelay) {}
|
|
61
|
+
} else {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
releaseLock() {
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(this.lockPath)) {
|
|
72
|
+
fs.unlinkSync(this.lockPath);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
console.error(`⚠️ Warning: Failed to release lock: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
withLock(fn) {
|
|
81
|
+
if (!this.acquireLock()) {
|
|
82
|
+
throw new Error(`Failed to acquire log file lock after ${this.lockTimeout}ms`);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return fn();
|
|
86
|
+
} finally {
|
|
87
|
+
this.releaseLock();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_getLastHashUnsafe() {
|
|
92
|
+
if (!fs.existsSync(this.logPath)) {
|
|
93
|
+
return '0000000000000000000000000000000000000000000000000000000000000000';
|
|
94
|
+
}
|
|
95
|
+
const lines = fs.readFileSync(this.logPath, 'utf8').split('\n').filter(Boolean);
|
|
96
|
+
if (lines.length === 0) return '0000000000000000000000000000000000000000000000000000000000000000';
|
|
97
|
+
try {
|
|
98
|
+
const lastEntry = JSON.parse(lines[lines.length - 1]);
|
|
99
|
+
return lastEntry.integrity_hash || '0000000000000000000000000000000000000000000000000000000000000000';
|
|
100
|
+
} catch {
|
|
101
|
+
return '0000000000000000000000000000000000000000000000000000000000000000';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getLastHash() {
|
|
106
|
+
return this.withLock(() => this._getLastHashUnsafe());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log(event) {
|
|
110
|
+
return this.withLock(() => {
|
|
111
|
+
const entry = {
|
|
112
|
+
'@timestamp': new Date().toISOString(),
|
|
113
|
+
sequence: this._getSequenceNumberUnsafe(),
|
|
114
|
+
prev_hash: this._getLastHashUnsafe(),
|
|
115
|
+
...event
|
|
116
|
+
};
|
|
117
|
+
const hash = crypto.createHmac('sha256', this.secret).update(JSON.stringify(entry)).digest('hex');
|
|
118
|
+
entry.integrity_hash = hash;
|
|
119
|
+
fs.appendFileSync(this.logPath, JSON.stringify(entry) + '\n');
|
|
120
|
+
return entry;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_getSequenceNumberUnsafe() {
|
|
125
|
+
if (!fs.existsSync(this.logPath)) return 1;
|
|
126
|
+
const lines = fs.readFileSync(this.logPath, 'utf8').split('\n').filter(Boolean);
|
|
127
|
+
return lines.length + 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
verify() {
|
|
131
|
+
if (!fs.existsSync(this.logPath)) {
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
return { valid: true, errors: [], total: 0, message: 'No audit log exists' };
|
|
134
|
+
}
|
|
135
|
+
const lines = fs.readFileSync(this.logPath, 'utf8').split('\n').filter(Boolean);
|
|
136
|
+
const errors = [];
|
|
137
|
+
let prevHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
try {
|
|
140
|
+
const entry = JSON.parse(lines[i]);
|
|
141
|
+
const { integrity_hash, ...data } = entry;
|
|
142
|
+
if (entry.prev_hash !== prevHash) {
|
|
143
|
+
errors.push({ line: i + 1, type: 'chain_broken', message: `Previous hash mismatch.` });
|
|
144
|
+
}
|
|
145
|
+
const expected = crypto.createHmac('sha256', this.secret).update(JSON.stringify(data)).digest('hex');
|
|
146
|
+
if (expected !== integrity_hash) {
|
|
147
|
+
errors.push({ line: i + 1, type: 'tampered', message: `Integrity hash mismatch.` });
|
|
148
|
+
}
|
|
149
|
+
prevHash = integrity_hash;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
+
errors.push({ line: i + 1, type: 'parse_error', message: `Failed to parse JSON: ${message}` });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
valid: errors.length === 0,
|
|
157
|
+
errors,
|
|
158
|
+
total: lines.length,
|
|
159
|
+
// @ts-ignore
|
|
160
|
+
message: errors.length === 0 ? `✅ All ${lines.length} entries verified successfully` : `❌ ${errors.length} issues found`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export() {
|
|
165
|
+
if (!fs.existsSync(this.logPath)) return [];
|
|
166
|
+
const lines = fs.readFileSync(this.logPath, 'utf8').split('\n').filter(Boolean);
|
|
167
|
+
return lines.map(line => JSON.parse(line));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
tail(count = 10) {
|
|
171
|
+
const entries = this.export();
|
|
172
|
+
return entries.slice(-count);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default AuditLogger;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DLP (Data Loss Prevention) Redactor
|
|
7
|
+
*/
|
|
8
|
+
class DLPRedactor {
|
|
9
|
+
constructor(patternsPath = null) {
|
|
10
|
+
this.patterns = this.getMinimalPatterns();
|
|
11
|
+
this.counters = {};
|
|
12
|
+
this.redactionMap = {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getMinimalPatterns() {
|
|
16
|
+
return {
|
|
17
|
+
pii: {
|
|
18
|
+
email: {
|
|
19
|
+
pattern: String.raw`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`,
|
|
20
|
+
token_prefix: "EMAIL"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
secrets: {
|
|
24
|
+
generic_api_key: {
|
|
25
|
+
pattern: String.raw`(?:api[_-]?key)[\s:=]+['"]?([a-zA-Z0-9_\-\.]{20,})['"]?`,
|
|
26
|
+
token_prefix: "API_KEY",
|
|
27
|
+
case_insensitive: true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
internal_assets: {}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
redact(text, privacyLevel = 'medium') {
|
|
35
|
+
if (!text) return { sanitized: '', redactionMap: {}, findings: [], stats: {} };
|
|
36
|
+
let sanitized = text;
|
|
37
|
+
const findings = [];
|
|
38
|
+
this.counters = {};
|
|
39
|
+
this.redactionMap = {};
|
|
40
|
+
return {
|
|
41
|
+
sanitized,
|
|
42
|
+
redactionMap: this.redactionMap,
|
|
43
|
+
findings,
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
stats: {
|
|
46
|
+
original_length: text.length,
|
|
47
|
+
sanitized_length: sanitized.length,
|
|
48
|
+
redactions: findings.length,
|
|
49
|
+
privacy_level: privacyLevel
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
rehydrate(text, redactionMap = null) {
|
|
55
|
+
if (!text) return '';
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getCategoriesToScan(privacyLevel) {
|
|
60
|
+
return ['pii', 'secrets'];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
generateAuditLog(result, metadata = {}) {
|
|
64
|
+
return {
|
|
65
|
+
'@timestamp': new Date().toISOString(),
|
|
66
|
+
event: { kind: 'event', category: 'process', type: 'info', action: 'dlp_redaction' },
|
|
67
|
+
message: `DLP scan completed`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default DLPRedactor;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import DLPRedactorImport from './dlp-redactor.js';
|
|
2
|
+
import AuditLoggerImport from './audit-logger.js';
|
|
3
|
+
|
|
4
|
+
export const DLPRedactor = DLPRedactorImport;
|
|
5
|
+
export const AuditLogger = AuditLoggerImport;
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
DLPRedactor: DLPRedactorImport,
|
|
9
|
+
AuditLogger: AuditLoggerImport
|
|
10
|
+
};
|