mumpix 1.0.18 → 1.0.20
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/CHANGELOG.md +12 -0
- package/README.md +64 -0
- package/bin/mumpix.js +0 -0
- package/examples/temporal-memory.js +80 -0
- package/package.json +7 -3
- package/src/core/MumpixDB.js +158 -10
- package/src/core/license.js +16 -21
- package/src/core/store.js +109 -24
- package/src/index.js +8 -0
- package/src/temporal/engine.js +1894 -0
- package/src/temporal/indexes.js +178 -0
- package/src/temporal/operators.js +186 -0
package/src/core/store.js
CHANGED
|
@@ -30,14 +30,21 @@ class MumpixStore {
|
|
|
30
30
|
this._fd = null; // open file descriptor for appends
|
|
31
31
|
this._lockFd = null; // lock file descriptor
|
|
32
32
|
this._strictIntegrity = false;
|
|
33
|
+
this._mlDsa = null; // ML-DSA-65 engine for Verified mode
|
|
34
|
+
this._signingKey = null; // Private key for signing (Verified mode)
|
|
35
|
+
this._publicKey = null; // Public key for verification (Verified mode)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
// ── Public ──────────────────────────────────────
|
|
36
39
|
|
|
37
|
-
open(opts = {}) {
|
|
40
|
+
async open(opts = {}) {
|
|
38
41
|
const consistency = opts.consistency || 'eventual';
|
|
39
42
|
this._strictIntegrity = consistency === 'strict' || consistency === 'verified';
|
|
40
43
|
this._acquireLock(opts);
|
|
44
|
+
|
|
45
|
+
if (consistency === 'verified') {
|
|
46
|
+
await this._initQuantumEngine();
|
|
47
|
+
}
|
|
41
48
|
|
|
42
49
|
if (fs.existsSync(this.filePath)) {
|
|
43
50
|
try {
|
|
@@ -101,21 +108,37 @@ class MumpixStore {
|
|
|
101
108
|
this._fd = null;
|
|
102
109
|
}
|
|
103
110
|
this._releaseLock();
|
|
111
|
+
// Zero out sensitive key material in memory
|
|
112
|
+
if (this._signingKey) this._signingKey.fill(0);
|
|
113
|
+
this._signingKey = null;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
/**
|
|
107
117
|
* Append a record. WAL-first for crash safety.
|
|
108
118
|
* Returns the new record.
|
|
109
119
|
*/
|
|
110
|
-
write(content) {
|
|
120
|
+
write(content, meta = {}) {
|
|
111
121
|
const trimmed = content.trim();
|
|
122
|
+
const sanitizedMeta = sanitizeMeta(meta);
|
|
123
|
+
const prevH = this.records.length > 0 ? this.records[this.records.length - 1].h : '0';
|
|
124
|
+
|
|
112
125
|
const record = {
|
|
113
126
|
id: this._nextId++,
|
|
114
127
|
content: trimmed,
|
|
115
|
-
ts: Date.now(),
|
|
116
|
-
h: this._hash(trimmed),
|
|
128
|
+
ts: Number.isFinite(sanitizedMeta.ts) ? Number(sanitizedMeta.ts) : Date.now(),
|
|
129
|
+
h: this._hash(trimmed, prevH, this._nextId - 1),
|
|
130
|
+
...(sanitizedMeta.workspace ? { workspace: sanitizedMeta.workspace } : {}),
|
|
131
|
+
...(sanitizedMeta.repo ? { repo: sanitizedMeta.repo } : {}),
|
|
132
|
+
...(sanitizedMeta.source ? { source: sanitizedMeta.source } : {}),
|
|
117
133
|
};
|
|
118
134
|
|
|
135
|
+
// If Verified mode and engine/keys are ready, sign the hash chain link
|
|
136
|
+
if (this._mlDsa && this._signingKey) {
|
|
137
|
+
const msg = Buffer.from(record.h, 'utf8');
|
|
138
|
+
const sig = this._mlDsa.sign(msg, this._signingKey);
|
|
139
|
+
record.sig = Buffer.from(sig).toString('hex');
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
// 1. Write to WAL and force it to disk before touching main file.
|
|
120
143
|
this._appendWal({ op: 'write', entry: record, ts: Date.now() });
|
|
121
144
|
|
|
@@ -179,6 +202,37 @@ class MumpixStore {
|
|
|
179
202
|
};
|
|
180
203
|
}
|
|
181
204
|
|
|
205
|
+
async _initQuantumEngine() {
|
|
206
|
+
const mlDsaPath = 'file://' + path.resolve(__dirname, 'ml-dsa.mjs');
|
|
207
|
+
const { ml_dsa65 } = await import(mlDsaPath);
|
|
208
|
+
this._mlDsa = ml_dsa65;
|
|
209
|
+
|
|
210
|
+
const keyPath = this.filePath + '.key';
|
|
211
|
+
if (fs.existsSync(keyPath)) {
|
|
212
|
+
// Load existing key
|
|
213
|
+
const keys = JSON.parse(fs.readFileSync(keyPath, 'utf8'));
|
|
214
|
+
this._signingKey = Buffer.from(keys.sk, 'hex');
|
|
215
|
+
this._publicKey = Buffer.from(keys.pk, 'hex');
|
|
216
|
+
} else {
|
|
217
|
+
// Generate fresh ML-DSA-65 keypair for this database
|
|
218
|
+
console.log(`[Quantum] Generating fresh Level 3 keypair for ${path.basename(this.filePath)}...`);
|
|
219
|
+
const { publicKey, secretKey } = this._mlDsa.keygen();
|
|
220
|
+
this._signingKey = secretKey;
|
|
221
|
+
this._publicKey = publicKey;
|
|
222
|
+
|
|
223
|
+
// Persist locally
|
|
224
|
+
fs.writeFileSync(keyPath, JSON.stringify({
|
|
225
|
+
pk: Buffer.from(publicKey).toString('hex'),
|
|
226
|
+
sk: Buffer.from(secretKey).toString('hex')
|
|
227
|
+
}), 'utf8');
|
|
228
|
+
|
|
229
|
+
if (this.header) {
|
|
230
|
+
this.header.pk = Buffer.from(publicKey).toString('hex');
|
|
231
|
+
this._rewriteFull();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
182
236
|
// ── Private ─────────────────────────────────────
|
|
183
237
|
|
|
184
238
|
_load() {
|
|
@@ -194,6 +248,7 @@ class MumpixStore {
|
|
|
194
248
|
|
|
195
249
|
this.records = [];
|
|
196
250
|
let lastId = 0;
|
|
251
|
+
let prevH = '0';
|
|
197
252
|
const seenIds = new Set();
|
|
198
253
|
for (let i = 1; i < lines.length; i++) {
|
|
199
254
|
try {
|
|
@@ -201,7 +256,7 @@ class MumpixStore {
|
|
|
201
256
|
if (r && Number.isInteger(r.id) && typeof r.content === 'string' && r.content.length > 0) {
|
|
202
257
|
const monotonic = r.id > lastId;
|
|
203
258
|
const duplicate = seenIds.has(r.id);
|
|
204
|
-
const hashOk = this._verifyHash(r);
|
|
259
|
+
const hashOk = this._verifyHash(r, prevH);
|
|
205
260
|
if (!monotonic || duplicate || !hashOk) {
|
|
206
261
|
const msg = !hashOk
|
|
207
262
|
? `mumpix: integrity hash mismatch for id=${r.id}`
|
|
@@ -210,6 +265,7 @@ class MumpixStore {
|
|
|
210
265
|
continue;
|
|
211
266
|
}
|
|
212
267
|
this.records.push(r);
|
|
268
|
+
prevH = r.h;
|
|
213
269
|
seenIds.add(r.id);
|
|
214
270
|
lastId = r.id;
|
|
215
271
|
if (r.id >= this._nextId) this._nextId = r.id + 1;
|
|
@@ -237,11 +293,12 @@ class MumpixStore {
|
|
|
237
293
|
const candidate = entry.entry;
|
|
238
294
|
const exists = seenIds.has(candidate.id);
|
|
239
295
|
const monotonic = Number.isInteger(candidate.id) && candidate.id > lastId;
|
|
240
|
-
const hashOk = this._verifyHash(candidate);
|
|
296
|
+
const hashOk = this._verifyHash(candidate, prevH);
|
|
241
297
|
if (!exists && monotonic && hashOk) {
|
|
242
298
|
this.records.push(entry.entry);
|
|
243
299
|
if (entry.entry.id >= this._nextId) this._nextId = entry.entry.id + 1;
|
|
244
300
|
seenIds.add(entry.entry.id);
|
|
301
|
+
prevH = entry.entry.h;
|
|
245
302
|
lastId = entry.entry.id;
|
|
246
303
|
dirty = true;
|
|
247
304
|
} else if (this._strictIntegrity) {
|
|
@@ -251,6 +308,7 @@ class MumpixStore {
|
|
|
251
308
|
this.records = [];
|
|
252
309
|
this._nextId = 1;
|
|
253
310
|
seenIds.clear();
|
|
311
|
+
prevH = '0';
|
|
254
312
|
lastId = 0;
|
|
255
313
|
dirty = true;
|
|
256
314
|
}
|
|
@@ -399,31 +457,48 @@ class MumpixStore {
|
|
|
399
457
|
fs.renameSync(tmp, this.filePath);
|
|
400
458
|
}
|
|
401
459
|
|
|
402
|
-
_hash(s) {
|
|
403
|
-
const secret = process.env.MUMPIX_INTEGRITY_SECRET;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.digest('hex');
|
|
409
|
-
return `hmac256:${digest}`;
|
|
410
|
-
}
|
|
411
|
-
let h = 0x811c9dc5;
|
|
412
|
-
for (let i = 0; i < s.length; i++) {
|
|
413
|
-
h ^= s.charCodeAt(i);
|
|
414
|
-
h = Math.imul(h, 0x01000193);
|
|
415
|
-
}
|
|
416
|
-
return '0x' + (h >>> 0).toString(16).padStart(8, '0');
|
|
460
|
+
_hash(s, prevH = '0', id = 0) {
|
|
461
|
+
const secret = process.env.MUMPIX_INTEGRITY_SECRET || '';
|
|
462
|
+
const h = crypto.createHmac('sha256', secret)
|
|
463
|
+
.update(String(s) + String(prevH) + String(id))
|
|
464
|
+
.digest('hex');
|
|
465
|
+
return `sha256:${h}`;
|
|
417
466
|
}
|
|
418
467
|
|
|
419
|
-
_verifyHash(record) {
|
|
468
|
+
_verifyHash(record, prevH = '0') {
|
|
420
469
|
if (!record || typeof record.content !== 'string' || typeof record.h !== 'string') return false;
|
|
470
|
+
|
|
471
|
+
// Support modern SHA-256 chaining
|
|
472
|
+
if (record.h.startsWith('sha256:')) {
|
|
473
|
+
const match = this._hash(record.content, prevH, record.id) === record.h;
|
|
474
|
+
if (!match) return false;
|
|
475
|
+
|
|
476
|
+
// In Verified mode, also check the ML-DSA signature if present
|
|
477
|
+
if (this.header.consistency === 'verified' && this._mlDsa && this._publicKey) {
|
|
478
|
+
if (!record.sig) return false; // Signature mandatory in Verified mode
|
|
479
|
+
try {
|
|
480
|
+
const msg = Buffer.from(record.h, 'utf8');
|
|
481
|
+
const sig = Buffer.from(record.sig, 'hex');
|
|
482
|
+
return this._mlDsa.verify(sig, msg, this._publicKey);
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Support legacy hmac256 (non-chained)
|
|
421
491
|
if (record.h.startsWith('hmac256:')) {
|
|
422
492
|
const secret = process.env.MUMPIX_INTEGRITY_SECRET;
|
|
423
493
|
if (!secret || !secret.trim()) return false;
|
|
424
|
-
|
|
494
|
+
const digest = crypto
|
|
495
|
+
.createHmac('sha256', secret)
|
|
496
|
+
.update(String(record.content), 'utf8')
|
|
497
|
+
.digest('hex');
|
|
498
|
+
return `hmac256:${digest}` === record.h;
|
|
425
499
|
}
|
|
426
|
-
|
|
500
|
+
|
|
501
|
+
// Backward compatibility for legacy FNV-1a records while migrating.
|
|
427
502
|
return this._hashLegacy(record.content) === record.h;
|
|
428
503
|
}
|
|
429
504
|
|
|
@@ -437,4 +512,14 @@ class MumpixStore {
|
|
|
437
512
|
}
|
|
438
513
|
}
|
|
439
514
|
|
|
515
|
+
function sanitizeMeta(meta) {
|
|
516
|
+
if (!meta || typeof meta !== 'object') return {};
|
|
517
|
+
const out = {};
|
|
518
|
+
if (Number.isFinite(meta.ts)) out.ts = Number(meta.ts);
|
|
519
|
+
if (typeof meta.workspace === 'string' && meta.workspace.trim()) out.workspace = meta.workspace.trim();
|
|
520
|
+
if (typeof meta.repo === 'string' && meta.repo.trim()) out.repo = meta.repo.trim();
|
|
521
|
+
if (typeof meta.source === 'string' && meta.source.trim()) out.source = meta.source.trim();
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
440
525
|
module.exports = { MumpixStore };
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,9 @@ const { MumpixStore } = require('./core/store');
|
|
|
19
19
|
const { MumpixAudit } = require('./core/audit');
|
|
20
20
|
const { recall, recallMany, tokenize } = require('./core/recall');
|
|
21
21
|
const { MumpixDevClient, MumpixApiError } = require('./integrations/developer-sdk');
|
|
22
|
+
const temporal = require('./temporal/operators');
|
|
23
|
+
const temporalEngine = require('./temporal/engine');
|
|
24
|
+
const temporalIndexes = require('./temporal/indexes');
|
|
22
25
|
|
|
23
26
|
// Convenience alias: Mumpix.open() === MumpixDB.open()
|
|
24
27
|
const Mumpix = MumpixDB;
|
|
@@ -37,6 +40,11 @@ module.exports = {
|
|
|
37
40
|
recallMany,
|
|
38
41
|
tokenize,
|
|
39
42
|
|
|
43
|
+
// Deterministic temporal event operators
|
|
44
|
+
temporal,
|
|
45
|
+
temporalEngine,
|
|
46
|
+
temporalIndexes,
|
|
47
|
+
|
|
40
48
|
// Developer SDK (HTTP client for Mumpix services)
|
|
41
49
|
MumpixDevClient,
|
|
42
50
|
MumpixApiError,
|