mumpix 1.0.5 → 1.0.6

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 ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 1.0.6 - 2026-02-27
4
+
5
+ ### Security hardening
6
+ - Added file identity metadata (`fileId`) and hash algorithm metadata (`hashAlg`) in headers for new/opened stores.
7
+ - Added strict integrity verification during load/WAL replay for `strict` and `verified` modes.
8
+ - Added monotonic/unique record ID validation in `strict` and `verified` modes.
9
+ - Added support for keyed record hashes (`hmac256`) when `MUMPIX_INTEGRITY_SECRET` is set.
10
+ - Kept backward compatibility for legacy hash records in migration scenarios.
11
+
12
+ ### Licensing and tier model
13
+ - Updated mode capability gating:
14
+ - Community/Free: `eventual`
15
+ - Developer/Teams: `eventual`, `strict`
16
+ - Compliance/Enterprise: `eventual`, `strict`, `verified`
17
+ - Removed free-tier write-count enforcement; gating now focuses on production capabilities.
18
+ - Added optional file-bound license context support (`fid`) for stronger license binding.
19
+
20
+ ### Reliability
21
+ - Hardened strict-mode behavior to fail fast on malformed or tampered WAL/record entries.
22
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mumpix",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "SQLite for AI — embedded, zero-config memory database for AI agents and LLM applications",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -34,8 +34,9 @@
34
34
  "src/",
35
35
  "bin/",
36
36
  "examples/",
37
+ "CHANGELOG.md",
37
38
  "README.md",
38
39
  "LICENSE"
39
40
  ],
40
41
  "homepage": "https://mumpixdb.com"
41
- }
42
+ }
@@ -59,16 +59,16 @@ class MumpixDB {
59
59
  throw new Error(`Invalid consistency mode "${consistency}". Valid: ${VALID_MODES.join(', ')}`);
60
60
  }
61
61
 
62
+ const store = new MumpixStore(filePath);
63
+ store.open({ consistency });
64
+
62
65
  // License Initialization & Check
63
66
  const license = new LicenseManager(opts.licenseKey);
67
+ license.setFileContext(store.header && store.header.fileId ? store.header.fileId : null);
64
68
  await license.init();
65
69
 
66
- if (consistency === 'verified') {
67
- license.checkLimit('mode', 'verified');
68
- }
69
-
70
- const store = new MumpixStore(filePath);
71
- store.open({ consistency });
70
+ // Capability-gated consistency mode check (Option B model)
71
+ license.checkLimit('mode', consistency);
72
72
 
73
73
  let auditLog = null;
74
74
  if (consistency === 'verified') {
@@ -16,6 +16,25 @@ class LicenseManager {
16
16
  this.verified = false;
17
17
  this.userId = 'guest';
18
18
  this._ml_dsa = null;
19
+ this.fileId = null;
20
+ }
21
+
22
+ // Capability matrix for Option B model.
23
+ // free/community: eventual only
24
+ // developer/teams: eventual + strict
25
+ // compliance/enterprise: eventual + strict + verified
26
+ _capsForTier(tier) {
27
+ const t = String(tier || 'free').toLowerCase();
28
+ if (t === 'developer') return { modes: ['eventual', 'strict'] };
29
+ if (t === 'teams' || t === 'team') return { modes: ['eventual', 'strict'] };
30
+ if (t === 'compliance' || t === 'enterprise' || t === 'verified' || t === 'pro') {
31
+ return { modes: ['eventual', 'strict', 'verified'] };
32
+ }
33
+ return { modes: ['eventual'] };
34
+ }
35
+
36
+ setFileContext(fileId) {
37
+ this.fileId = fileId ? String(fileId) : null;
19
38
  }
20
39
 
21
40
  async init() {
@@ -54,6 +73,12 @@ class LicenseManager {
54
73
  throw new Error('License key has expired');
55
74
  }
56
75
 
76
+ // Optional binding for newly issued licenses.
77
+ // Backward-compatible: if fid missing from payload, accept legacy license.
78
+ if (payload.fid && this.fileId && String(payload.fid) !== String(this.fileId)) {
79
+ throw new Error('License key is bound to a different file');
80
+ }
81
+
57
82
  this.userId = payload.id;
58
83
  this.tier = payload.tier || 'free';
59
84
  this.expiry = payload.exp;
@@ -67,13 +92,22 @@ class LicenseManager {
67
92
  }
68
93
 
69
94
  checkLimit(type, currentCount) {
70
- if (this.tier === 'free') {
71
- if (type === 'records' && currentCount >= 100) {
72
- throw new Error('MumpixDB Free Tier Limit Reached: 100 records. Upgrade at mumpixdb.com for unlimited.');
73
- }
74
- if (type === 'mode' && currentCount === 'verified') {
75
- throw new Error('MumpixDB "verified" consistency mode is a Pro feature. Upgrade at mumpixdb.com.');
95
+ // Option B: no record count gate. Gating is by capabilities/mode.
96
+ if (type === 'records') return true;
97
+
98
+ if (type === 'mode') {
99
+ const requested = String(currentCount || '').toLowerCase();
100
+ const caps = this._capsForTier(this.tier);
101
+ if (!caps.modes.includes(requested)) {
102
+ if (requested === 'strict') {
103
+ throw new Error('MumpixDB "strict" mode requires Developer tier or higher. Upgrade at mumpixdb.com.');
104
+ }
105
+ if (requested === 'verified') {
106
+ throw new Error('MumpixDB "verified" mode requires Compliance tier. Upgrade at mumpixdb.com.');
107
+ }
108
+ throw new Error(`MumpixDB mode "${requested}" is not available on your current tier.`);
76
109
  }
110
+ return true;
77
111
  }
78
112
  return true;
79
113
  }
package/src/core/store.js CHANGED
@@ -15,6 +15,7 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
+ const crypto = require('crypto');
18
19
 
19
20
  const MAGIC_VERSION = 1;
20
21
 
@@ -26,12 +27,14 @@ class MumpixStore {
26
27
  this.records = []; // { id, content, ts, h }
27
28
  this._nextId = 1;
28
29
  this._fd = null; // open file descriptor for appends
30
+ this._strictIntegrity = false;
29
31
  }
30
32
 
31
33
  // ── Public ──────────────────────────────────────
32
34
 
33
35
  open(opts = {}) {
34
36
  const consistency = opts.consistency || 'eventual';
37
+ this._strictIntegrity = consistency === 'strict' || consistency === 'verified';
35
38
 
36
39
  if (fs.existsSync(this.filePath)) {
37
40
  this._load();
@@ -43,10 +46,21 @@ class MumpixStore {
43
46
  consistency,
44
47
  created: Date.now(),
45
48
  path: path.basename(this.filePath),
49
+ fileId: crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex'),
50
+ hashAlg: 'hmac-sha256',
46
51
  };
47
52
  this._writeHeader();
48
53
  }
49
54
 
55
+ if (!this.header.fileId) {
56
+ this.header.fileId = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
57
+ this._rewriteFull();
58
+ }
59
+ if (!this.header.hashAlg) {
60
+ this.header.hashAlg = 'hmac-sha256';
61
+ this._rewriteFull();
62
+ }
63
+
50
64
  // Update consistency if caller changed it
51
65
  if (this.header.consistency !== consistency) {
52
66
  this.header.consistency = consistency;
@@ -70,11 +84,12 @@ class MumpixStore {
70
84
  * Returns the new record.
71
85
  */
72
86
  write(content) {
87
+ const trimmed = content.trim();
73
88
  const record = {
74
89
  id: this._nextId++,
75
- content: content.trim(),
90
+ content: trimmed,
76
91
  ts: Date.now(),
77
- h: this._hash(content),
92
+ h: this._hash(trimmed),
78
93
  };
79
94
 
80
95
  // 1. Write to WAL
@@ -150,14 +165,31 @@ class MumpixStore {
150
165
  }
151
166
 
152
167
  this.records = [];
168
+ let lastId = 0;
169
+ const seenIds = new Set();
153
170
  for (let i = 1; i < lines.length; i++) {
154
171
  try {
155
172
  const r = JSON.parse(lines[i]);
156
- if (r && r.id && r.content) {
173
+ if (r && Number.isInteger(r.id) && typeof r.content === 'string' && r.content.length > 0) {
174
+ const monotonic = r.id > lastId;
175
+ const duplicate = seenIds.has(r.id);
176
+ const hashOk = this._verifyHash(r);
177
+ if (!monotonic || duplicate || !hashOk) {
178
+ const msg = !hashOk
179
+ ? `mumpix: integrity hash mismatch for id=${r.id}`
180
+ : `mumpix: invalid record order/id at id=${r.id}`;
181
+ if (this._strictIntegrity) throw new Error(msg);
182
+ continue;
183
+ }
157
184
  this.records.push(r);
185
+ seenIds.add(r.id);
186
+ lastId = r.id;
158
187
  if (r.id >= this._nextId) this._nextId = r.id + 1;
159
188
  }
160
- } catch (_) { /* skip corrupt lines */ }
189
+ } catch (err) {
190
+ if (this._strictIntegrity) throw err;
191
+ // skip corrupt lines in eventual mode
192
+ }
161
193
  }
162
194
  }
163
195
 
@@ -167,23 +199,37 @@ class MumpixStore {
167
199
  const raw = fs.readFileSync(this.walPath, 'utf8');
168
200
  const lines = raw.split('\n').filter(l => l.trim());
169
201
  let dirty = false;
202
+ let lastId = this.records.length ? this.records[this.records.length - 1].id : 0;
203
+ const seenIds = new Set(this.records.map(r => r.id));
170
204
 
171
205
  for (const line of lines) {
172
206
  try {
173
207
  const entry = JSON.parse(line);
174
208
  if (entry.op === 'write' && entry.entry) {
175
- const exists = this.records.find(r => r.id === entry.entry.id);
176
- if (!exists) {
209
+ const candidate = entry.entry;
210
+ const exists = seenIds.has(candidate.id);
211
+ const monotonic = Number.isInteger(candidate.id) && candidate.id > lastId;
212
+ const hashOk = this._verifyHash(candidate);
213
+ if (!exists && monotonic && hashOk) {
177
214
  this.records.push(entry.entry);
178
215
  if (entry.entry.id >= this._nextId) this._nextId = entry.entry.id + 1;
216
+ seenIds.add(entry.entry.id);
217
+ lastId = entry.entry.id;
179
218
  dirty = true;
219
+ } else if (this._strictIntegrity) {
220
+ throw new Error(`mumpix: rejected WAL write id=${candidate.id}`);
180
221
  }
181
222
  } else if (entry.op === 'clear') {
182
223
  this.records = [];
183
224
  this._nextId = 1;
225
+ seenIds.clear();
226
+ lastId = 0;
184
227
  dirty = true;
185
228
  }
186
- } catch (_) { /* skip corrupt WAL lines */ }
229
+ } catch (err) {
230
+ if (this._strictIntegrity) throw err;
231
+ // skip corrupt WAL lines in eventual mode
232
+ }
187
233
  }
188
234
 
189
235
  if (dirty) this._rewriteFull();
@@ -218,6 +264,34 @@ class MumpixStore {
218
264
  }
219
265
 
220
266
  _hash(s) {
267
+ const secret = process.env.MUMPIX_INTEGRITY_SECRET;
268
+ if (secret && secret.trim()) {
269
+ const digest = crypto
270
+ .createHmac('sha256', secret)
271
+ .update(String(s), 'utf8')
272
+ .digest('hex');
273
+ return `hmac256:${digest}`;
274
+ }
275
+ let h = 0x811c9dc5;
276
+ for (let i = 0; i < s.length; i++) {
277
+ h ^= s.charCodeAt(i);
278
+ h = Math.imul(h, 0x01000193);
279
+ }
280
+ return '0x' + (h >>> 0).toString(16).padStart(8, '0');
281
+ }
282
+
283
+ _verifyHash(record) {
284
+ if (!record || typeof record.content !== 'string' || typeof record.h !== 'string') return false;
285
+ if (record.h.startsWith('hmac256:')) {
286
+ const secret = process.env.MUMPIX_INTEGRITY_SECRET;
287
+ if (!secret || !secret.trim()) return false;
288
+ return this._hash(record.content) === record.h;
289
+ }
290
+ // Backward compatibility for legacy records while migrating.
291
+ return this._hashLegacy(record.content) === record.h;
292
+ }
293
+
294
+ _hashLegacy(s) {
221
295
  let h = 0x811c9dc5;
222
296
  for (let i = 0; i < s.length; i++) {
223
297
  h ^= s.charCodeAt(i);