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 +22 -0
- package/package.json +3 -2
- package/src/core/MumpixDB.js +6 -6
- package/src/core/license.js +40 -6
- package/src/core/store.js +81 -7
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.
|
|
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
|
+
}
|
package/src/core/MumpixDB.js
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
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') {
|
package/src/core/license.js
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
90
|
+
content: trimmed,
|
|
76
91
|
ts: Date.now(),
|
|
77
|
-
h: this._hash(
|
|
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 (
|
|
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
|
|
176
|
-
|
|
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 (
|
|
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);
|