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/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
- if (secret && secret.trim()) {
405
- const digest = crypto
406
- .createHmac('sha256', secret)
407
- .update(String(s), 'utf8')
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
- return this._hash(record.content) === record.h;
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
- // Backward compatibility for legacy records while migrating.
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,