mumpix 1.0.19 → 1.0.29
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 +42 -14
- package/README.md +185 -8
- package/bin/mumpix.js +1 -405
- package/examples/agent-memory.js +1 -1
- package/examples/basic.js +1 -1
- package/examples/behavioral-primitives.js +50 -0
- package/examples/verified-mode.js +1 -1
- package/package.json +17 -13
- package/scripts/test-license-modes.cjs +87 -0
- package/src/brp/index.js +1 -0
- package/src/collapse/index.js +1 -0
- package/src/core/MumpixDB.js +210 -322
- package/src/core/audit.js +1 -173
- package/src/core/auth.js +1 -232
- package/src/core/inverted-index.js +144 -0
- package/src/core/license.js +1 -267
- package/src/core/ml-dsa.mjs +1 -25
- package/src/core/ml-kem.mjs +1 -32
- package/src/core/recall.js +1 -176
- package/src/core/store.js +335 -286
- package/src/core/wal-writer.js +83 -0
- package/src/index.js +20 -34
- package/src/integrations/developer-sdk.js +1 -165
- package/src/integrations/langchain-official.js +1 -0
- package/src/integrations/langchain.js +1 -131
- package/src/integrations/llamaindex-official.js +1 -0
- package/src/integrations/llamaindex.js +1 -86
- package/src/integrations/vector-sidecar.js +325 -0
- package/src/rlp/index.js +1 -0
- package/src/temporal/engine.js +1 -1894
- package/src/temporal/indexes.js +1 -178
- package/src/temporal/operators.js +1 -186
- package/scripts/postinstall-auth.js +0 -101
package/src/core/store.js
CHANGED
|
@@ -1,64 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const { WalWriter } = require("./wal-writer");
|
|
7
|
+
const { InvertedIndex } = require("./inverted-index");
|
|
8
|
+
const { tokenize } = require("./recall");
|
|
9
|
+
|
|
10
|
+
function normalizeMeta(meta) {
|
|
11
|
+
if (!meta || typeof meta !== "object") return {};
|
|
12
|
+
const out = {};
|
|
13
|
+
if (Number.isFinite(meta.ts)) out.ts = Number(meta.ts);
|
|
14
|
+
if (typeof meta.workspace === "string" && meta.workspace.trim()) {
|
|
15
|
+
out.workspace = meta.workspace.trim();
|
|
16
|
+
}
|
|
17
|
+
if (typeof meta.repo === "string" && meta.repo.trim()) {
|
|
18
|
+
out.repo = meta.repo.trim();
|
|
19
|
+
}
|
|
20
|
+
if (typeof meta.source === "string" && meta.source.trim()) {
|
|
21
|
+
out.source = meta.source.trim();
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
class MumpixStore {
|
|
23
27
|
constructor(filePath) {
|
|
24
28
|
this.filePath = path.resolve(filePath);
|
|
25
|
-
this.walPath
|
|
26
|
-
this.lockPath = this.filePath
|
|
27
|
-
this.header
|
|
28
|
-
this.records
|
|
29
|
-
this._nextId
|
|
30
|
-
this._fd
|
|
31
|
-
this._lockFd
|
|
29
|
+
this.walPath = `${this.filePath}.wal`;
|
|
30
|
+
this.lockPath = `${this.filePath}.lock`;
|
|
31
|
+
this.header = null;
|
|
32
|
+
this.records = [];
|
|
33
|
+
this._nextId = 1;
|
|
34
|
+
this._fd = null;
|
|
35
|
+
this._lockFd = null;
|
|
32
36
|
this._strictIntegrity = false;
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
37
|
+
this._walWriter = null;
|
|
38
|
+
this._index = null;
|
|
39
|
+
this._recallIndexEnabled = true;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this._strictIntegrity = consistency === 'strict' || consistency === 'verified';
|
|
42
|
+
open(opts = {}) {
|
|
43
|
+
const consistency = opts.consistency || "eventual";
|
|
44
|
+
this._strictIntegrity = consistency === "strict" || consistency === "verified";
|
|
45
|
+
this._recallIndexEnabled = opts.recallIndex !== false;
|
|
43
46
|
this._acquireLock(opts);
|
|
44
|
-
|
|
45
|
-
if (consistency === 'verified') {
|
|
46
|
-
await this._initQuantumEngine();
|
|
47
|
-
}
|
|
48
47
|
|
|
49
48
|
if (fs.existsSync(this.filePath)) {
|
|
50
49
|
try {
|
|
51
50
|
this._load();
|
|
52
51
|
} catch (err) {
|
|
53
|
-
|
|
54
|
-
if (!recovered) {
|
|
52
|
+
if (!(this._canRecoverFromIntegrityError(err) && this._recoverFromTailCorruption())) {
|
|
55
53
|
this._releaseLock();
|
|
56
54
|
throw err;
|
|
57
55
|
}
|
|
58
56
|
this._load();
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
// Replay any uncommitted WAL
|
|
62
59
|
try {
|
|
63
60
|
this._replayWAL();
|
|
64
61
|
} catch (err) {
|
|
@@ -67,212 +64,267 @@ class MumpixStore {
|
|
|
67
64
|
}
|
|
68
65
|
} else {
|
|
69
66
|
this.header = {
|
|
70
|
-
v:
|
|
67
|
+
v: 1,
|
|
71
68
|
consistency,
|
|
72
69
|
created: Date.now(),
|
|
73
70
|
path: path.basename(this.filePath),
|
|
74
|
-
fileId: crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString(
|
|
75
|
-
hashAlg:
|
|
71
|
+
fileId: crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString("hex"),
|
|
72
|
+
hashAlg: "hmac-sha256",
|
|
76
73
|
};
|
|
77
74
|
this._writeHeader();
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
if (!this.header.fileId) {
|
|
81
|
-
this.header.fileId = crypto.randomUUID
|
|
78
|
+
this.header.fileId = crypto.randomUUID
|
|
79
|
+
? crypto.randomUUID()
|
|
80
|
+
: crypto.randomBytes(16).toString("hex");
|
|
82
81
|
this._rewriteFull();
|
|
83
82
|
}
|
|
84
83
|
if (!this.header.hashAlg) {
|
|
85
|
-
this.header.hashAlg =
|
|
84
|
+
this.header.hashAlg = "hmac-sha256";
|
|
86
85
|
this._rewriteFull();
|
|
87
86
|
}
|
|
88
|
-
|
|
89
|
-
// Update consistency if caller changed it
|
|
90
87
|
if (this.header.consistency !== consistency) {
|
|
91
88
|
this.header.consistency = consistency;
|
|
92
89
|
this._rewriteFull();
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
|
|
92
|
+
this._walWriter = new WalWriter(this.walPath, consistency, {
|
|
93
|
+
intervalMs: Number.isFinite(opts.walFlushIntervalMs) ? opts.walFlushIntervalMs : undefined,
|
|
94
|
+
maxPendingBytes: Number.isFinite(opts.walMaxPendingBytes) ? opts.walMaxPendingBytes : undefined,
|
|
95
|
+
});
|
|
96
|
+
|
|
96
97
|
try {
|
|
97
|
-
this._fd = fs.openSync(this.filePath,
|
|
98
|
+
this._fd = fs.openSync(this.filePath, "a");
|
|
98
99
|
} catch (err) {
|
|
99
100
|
this._releaseLock();
|
|
100
101
|
throw err;
|
|
101
102
|
}
|
|
103
|
+
|
|
104
|
+
this._initIndex();
|
|
102
105
|
return this;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
close() {
|
|
109
|
+
if (this._index) {
|
|
110
|
+
try {
|
|
111
|
+
this._index.persist();
|
|
112
|
+
} catch (_) {}
|
|
113
|
+
}
|
|
114
|
+
if (this._walWriter) {
|
|
115
|
+
this._walWriter.close({ flush: true });
|
|
116
|
+
this._walWriter = null;
|
|
117
|
+
}
|
|
106
118
|
if (this._fd !== null) {
|
|
107
119
|
fs.closeSync(this._fd);
|
|
108
120
|
this._fd = null;
|
|
109
121
|
}
|
|
110
122
|
this._releaseLock();
|
|
111
|
-
// Zero out sensitive key material in memory
|
|
112
|
-
if (this._signingKey) this._signingKey.fill(0);
|
|
113
|
-
this._signingKey = null;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
flush() {
|
|
126
|
+
if (this._walWriter) this._walWriter.flush();
|
|
127
|
+
if (this._fd !== null) fs.fdatasyncSync(this._fd);
|
|
128
|
+
if (this._index) this._index.persist();
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
write(content, meta = {}) {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
id: this._nextId++,
|
|
127
|
-
content: 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 } : {}),
|
|
133
|
-
};
|
|
132
|
+
const text = String(content).trim();
|
|
133
|
+
const cleanMeta = normalizeMeta(meta);
|
|
134
|
+
const record = this._buildRecord(text, cleanMeta);
|
|
135
|
+
|
|
136
|
+
this._appendWal({ op: "write", entry: record, ts: Date.now() });
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
try {
|
|
139
|
+
fs.writeSync(this._fd, `${JSON.stringify(record)}\n`, null, "utf8");
|
|
140
|
+
if (this._requiresPerRecordSync()) fs.fdatasyncSync(this._fd);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw err;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
this.
|
|
145
|
+
this._clearWAL();
|
|
146
|
+
this.records.push(record);
|
|
147
|
+
if (this._index) this._index.add(record);
|
|
148
|
+
return record;
|
|
149
|
+
}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const line = JSON.stringify(record) + '\n';
|
|
148
|
-
fs.writeSync(this._fd, line, null, 'utf8');
|
|
151
|
+
writeBatch(items) {
|
|
152
|
+
if (!Array.isArray(items) || !items.length) return [];
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
fs.fdatasyncSync(this._fd);
|
|
154
|
+
const records = items.map((item) => {
|
|
155
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
156
|
+
return this._buildRecord(String(item.content || "").trim(), normalizeMeta(item));
|
|
154
157
|
}
|
|
158
|
+
return this._buildRecord(String(item || "").trim(), {});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const walLines = records.map((record) =>
|
|
162
|
+
JSON.stringify({ op: "write", entry: record, ts: Date.now() })
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (this._walWriter) {
|
|
166
|
+
this._walWriter.appendBatch(walLines);
|
|
167
|
+
} else {
|
|
168
|
+
for (const line of walLines) this._appendWal(JSON.parse(line));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
fs.writeSync(
|
|
173
|
+
this._fd,
|
|
174
|
+
records.map((record) => JSON.stringify(record)).join("\n") + "\n",
|
|
175
|
+
null,
|
|
176
|
+
"utf8"
|
|
177
|
+
);
|
|
178
|
+
if (this._requiresPerRecordSync()) fs.fdatasyncSync(this._fd);
|
|
155
179
|
} catch (err) {
|
|
156
|
-
// WAL remains present; write will be replayed on next open.
|
|
157
180
|
throw err;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
|
-
// 4. Remove WAL entry (write succeeded)
|
|
161
183
|
this._clearWAL();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
for (const record of records) {
|
|
185
|
+
this.records.push(record);
|
|
186
|
+
if (this._index) this._index.add(record);
|
|
187
|
+
}
|
|
188
|
+
return records;
|
|
165
189
|
}
|
|
166
190
|
|
|
167
|
-
/**
|
|
168
|
-
* Clear all records — WAL-first.
|
|
169
|
-
*/
|
|
170
191
|
clear() {
|
|
171
192
|
const count = this.records.length;
|
|
172
|
-
|
|
173
|
-
this._appendWal({ op: 'clear', count, ts: Date.now() });
|
|
174
|
-
|
|
193
|
+
this._appendWal({ op: "clear", count, ts: Date.now() });
|
|
175
194
|
this.records = [];
|
|
176
195
|
this._nextId = 1;
|
|
196
|
+
if (this._index) this._index.reset();
|
|
177
197
|
this._rewriteFull();
|
|
178
198
|
this._clearWAL();
|
|
179
|
-
|
|
199
|
+
if (this._index) this._index.persist();
|
|
180
200
|
return count;
|
|
181
201
|
}
|
|
182
202
|
|
|
183
|
-
/**
|
|
184
|
-
* Return all records (immutable copy).
|
|
185
|
-
*/
|
|
186
203
|
all() {
|
|
187
|
-
return this.records.map(
|
|
204
|
+
return this.records.map((record) => ({ ...record }));
|
|
188
205
|
}
|
|
189
206
|
|
|
190
|
-
/**
|
|
191
|
-
* Return store metadata.
|
|
192
|
-
*/
|
|
193
207
|
stats() {
|
|
194
208
|
const stat = fs.existsSync(this.filePath) ? fs.statSync(this.filePath) : null;
|
|
195
209
|
return {
|
|
196
|
-
path:
|
|
210
|
+
path: this.filePath,
|
|
197
211
|
consistency: this.header.consistency,
|
|
198
|
-
records:
|
|
199
|
-
created:
|
|
200
|
-
sizeBytes:
|
|
201
|
-
version:
|
|
212
|
+
records: this.records.length,
|
|
213
|
+
created: this.header.created,
|
|
214
|
+
sizeBytes: stat ? stat.size : 0,
|
|
215
|
+
version: this.header.v,
|
|
216
|
+
index: this.indexStats(),
|
|
202
217
|
};
|
|
203
218
|
}
|
|
204
219
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
indexedCandidates(query, cap = 400) {
|
|
221
|
+
if (!this._index) return null;
|
|
222
|
+
return this._index.candidates(query, cap);
|
|
223
|
+
}
|
|
209
224
|
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
rebuildIndex() {
|
|
226
|
+
if (!this._recallIndexEnabled) return null;
|
|
227
|
+
this._index = new InvertedIndex(tokenize, this._indexPath());
|
|
228
|
+
for (const record of this.records) this._index.add(record);
|
|
229
|
+
this._index.persist();
|
|
230
|
+
return this.indexStats();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
indexStats() {
|
|
234
|
+
if (!this._index) {
|
|
235
|
+
return {
|
|
236
|
+
enabled: false,
|
|
237
|
+
terms: 0,
|
|
238
|
+
docCount: 0,
|
|
239
|
+
highWaterSeq: 0,
|
|
240
|
+
sidecarPath: this._indexPath(),
|
|
241
|
+
sizeBytes: 0,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return { enabled: true, ...this._index.stats() };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_buildRecord(text, meta) {
|
|
248
|
+
const record = {
|
|
249
|
+
id: this._nextId++,
|
|
250
|
+
content: text,
|
|
251
|
+
ts: Number.isFinite(meta.ts) ? Number(meta.ts) : Date.now(),
|
|
252
|
+
h: this._hash(text),
|
|
253
|
+
};
|
|
254
|
+
if (meta.workspace) record.workspace = meta.workspace;
|
|
255
|
+
if (meta.repo) record.repo = meta.repo;
|
|
256
|
+
if (meta.source) record.source = meta.source;
|
|
257
|
+
return record;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_requiresPerRecordSync() {
|
|
261
|
+
return this.header.consistency === "strict" || this.header.consistency === "verified";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_initIndex() {
|
|
265
|
+
if (!this._recallIndexEnabled) {
|
|
266
|
+
this._index = null;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
this._index = new InvertedIndex(tokenize, this._indexPath());
|
|
271
|
+
const highWater = this._index.loadSidecar();
|
|
272
|
+
if (highWater > 0) {
|
|
273
|
+
this._index.hydrate(this.records.filter((record) => record.id <= highWater));
|
|
274
|
+
for (const record of this.records) {
|
|
275
|
+
if (record.id > highWater) this._index.add(record);
|
|
232
276
|
}
|
|
277
|
+
} else {
|
|
278
|
+
for (const record of this.records) this._index.add(record);
|
|
233
279
|
}
|
|
234
280
|
}
|
|
235
281
|
|
|
236
|
-
|
|
282
|
+
_indexPath() {
|
|
283
|
+
return `${this.filePath}.idx`;
|
|
284
|
+
}
|
|
237
285
|
|
|
238
286
|
_load() {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
287
|
+
const lines = fs
|
|
288
|
+
.readFileSync(this.filePath, "utf8")
|
|
289
|
+
.split("\n")
|
|
290
|
+
.filter((line) => line.trim());
|
|
242
291
|
if (!lines.length) throw new Error(`mumpix: corrupt or empty file: ${this.filePath}`);
|
|
243
292
|
|
|
244
293
|
this.header = JSON.parse(lines[0]);
|
|
245
|
-
if (this.header.v !==
|
|
246
|
-
throw new Error(`mumpix: unsupported file version ${this.header.v}`);
|
|
247
|
-
}
|
|
294
|
+
if (this.header.v !== 1) throw new Error(`mumpix: unsupported file version ${this.header.v}`);
|
|
248
295
|
|
|
249
296
|
this.records = [];
|
|
297
|
+
this._nextId = 1;
|
|
250
298
|
let lastId = 0;
|
|
251
|
-
|
|
252
|
-
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
|
|
253
301
|
for (let i = 1; i < lines.length; i++) {
|
|
254
302
|
try {
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
303
|
+
const record = JSON.parse(lines[i]);
|
|
304
|
+
if (
|
|
305
|
+
record &&
|
|
306
|
+
Number.isInteger(record.id) &&
|
|
307
|
+
typeof record.content === "string" &&
|
|
308
|
+
record.content.length > 0
|
|
309
|
+
) {
|
|
310
|
+
const ordered = record.id > lastId;
|
|
311
|
+
const duplicate = seen.has(record.id);
|
|
312
|
+
const validHash = this._verifyHash(record);
|
|
313
|
+
if (!ordered || duplicate || !validHash) {
|
|
314
|
+
const message = validHash
|
|
315
|
+
? `mumpix: invalid record order/id at id=${record.id}`
|
|
316
|
+
: `mumpix: integrity hash mismatch for id=${record.id}`;
|
|
317
|
+
if (this._strictIntegrity) throw new Error(message);
|
|
265
318
|
continue;
|
|
266
319
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
lastId =
|
|
271
|
-
if (
|
|
320
|
+
|
|
321
|
+
this.records.push(record);
|
|
322
|
+
seen.add(record.id);
|
|
323
|
+
lastId = record.id;
|
|
324
|
+
if (record.id >= this._nextId) this._nextId = record.id + 1;
|
|
272
325
|
}
|
|
273
326
|
} catch (err) {
|
|
274
327
|
if (this._strictIntegrity) throw err;
|
|
275
|
-
// skip corrupt lines in eventual mode
|
|
276
328
|
}
|
|
277
329
|
}
|
|
278
330
|
}
|
|
@@ -280,80 +332,92 @@ class MumpixStore {
|
|
|
280
332
|
_replayWAL() {
|
|
281
333
|
if (!fs.existsSync(this.walPath)) return;
|
|
282
334
|
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
335
|
+
const lines = fs
|
|
336
|
+
.readFileSync(this.walPath, "utf8")
|
|
337
|
+
.split("\n")
|
|
338
|
+
.filter((line) => line.trim());
|
|
339
|
+
let changed = false;
|
|
286
340
|
let lastId = this.records.length ? this.records[this.records.length - 1].id : 0;
|
|
287
|
-
const
|
|
341
|
+
const seen = new Set(this.records.map((record) => record.id));
|
|
288
342
|
|
|
289
343
|
for (const line of lines) {
|
|
290
344
|
try {
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
if (!
|
|
298
|
-
this.records.push(
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
dirty = true;
|
|
345
|
+
const event = JSON.parse(line);
|
|
346
|
+
if (event.op === "write" && event.entry) {
|
|
347
|
+
const record = event.entry;
|
|
348
|
+
const duplicate = seen.has(record.id);
|
|
349
|
+
const ordered = Number.isInteger(record.id) && record.id > lastId;
|
|
350
|
+
const validHash = this._verifyHash(record);
|
|
351
|
+
if (!duplicate && ordered && validHash) {
|
|
352
|
+
this.records.push(record);
|
|
353
|
+
if (record.id >= this._nextId) this._nextId = record.id + 1;
|
|
354
|
+
seen.add(record.id);
|
|
355
|
+
lastId = record.id;
|
|
356
|
+
changed = true;
|
|
304
357
|
} else if (this._strictIntegrity) {
|
|
305
|
-
throw new Error(`mumpix: rejected WAL write id=${
|
|
358
|
+
throw new Error(`mumpix: rejected WAL write id=${record.id}`);
|
|
306
359
|
}
|
|
307
|
-
} else if (
|
|
360
|
+
} else if (event.op === "clear") {
|
|
308
361
|
this.records = [];
|
|
309
362
|
this._nextId = 1;
|
|
310
|
-
|
|
311
|
-
prevH = '0';
|
|
363
|
+
seen.clear();
|
|
312
364
|
lastId = 0;
|
|
313
|
-
|
|
365
|
+
changed = true;
|
|
314
366
|
}
|
|
315
367
|
} catch (err) {
|
|
316
368
|
if (this._strictIntegrity) throw err;
|
|
317
|
-
// skip corrupt WAL lines in eventual mode
|
|
318
369
|
}
|
|
319
370
|
}
|
|
320
371
|
|
|
321
|
-
if (
|
|
372
|
+
if (changed) this._rewriteFull();
|
|
322
373
|
this._clearWAL();
|
|
323
374
|
}
|
|
324
375
|
|
|
325
376
|
_writeHeader() {
|
|
326
377
|
const dir = path.dirname(this.filePath);
|
|
327
378
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
328
|
-
fs.writeFileSync(this.filePath, JSON.stringify(this.header)
|
|
379
|
+
fs.writeFileSync(this.filePath, `${JSON.stringify(this.header)}\n`, "utf8");
|
|
329
380
|
}
|
|
330
381
|
|
|
331
382
|
_rewriteFull() {
|
|
332
|
-
|
|
333
|
-
|
|
383
|
+
if (this._walWriter) {
|
|
384
|
+
try {
|
|
385
|
+
this._walWriter.flush();
|
|
386
|
+
} catch (_) {}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const tmp = `${this.filePath}.tmp.${process.pid}`;
|
|
334
390
|
const lines = [JSON.stringify(this.header)];
|
|
335
|
-
for (const
|
|
336
|
-
fs.writeFileSync(tmp, lines.join(
|
|
391
|
+
for (const record of this.records) lines.push(JSON.stringify(record));
|
|
392
|
+
fs.writeFileSync(tmp, `${lines.join("\n")}\n`, "utf8");
|
|
337
393
|
fs.renameSync(tmp, this.filePath);
|
|
338
394
|
|
|
339
|
-
// Re-open append FD if needed
|
|
340
395
|
if (this._fd !== null) {
|
|
341
396
|
fs.closeSync(this._fd);
|
|
342
|
-
this._fd = fs.openSync(this.filePath,
|
|
397
|
+
this._fd = fs.openSync(this.filePath, "a");
|
|
343
398
|
}
|
|
344
399
|
}
|
|
345
400
|
|
|
346
401
|
_clearWAL() {
|
|
347
|
-
if (
|
|
348
|
-
|
|
402
|
+
if (this._walWriter) {
|
|
403
|
+
this._walWriter.unlink({ flush: this._requiresPerRecordSync() });
|
|
404
|
+
} else {
|
|
405
|
+
try {
|
|
406
|
+
fs.unlinkSync(this.walPath);
|
|
407
|
+
} catch (_) {}
|
|
349
408
|
}
|
|
350
409
|
}
|
|
351
410
|
|
|
352
|
-
_appendWal(
|
|
353
|
-
const line = JSON.stringify(
|
|
354
|
-
|
|
411
|
+
_appendWal(event) {
|
|
412
|
+
const line = JSON.stringify(event);
|
|
413
|
+
if (this._walWriter) {
|
|
414
|
+
this._walWriter.append(line);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const fd = fs.openSync(this.walPath, "a");
|
|
355
419
|
try {
|
|
356
|
-
fs.writeSync(fd, line
|
|
420
|
+
fs.writeSync(fd, `${line}\n`, null, "utf8");
|
|
357
421
|
fs.fdatasyncSync(fd);
|
|
358
422
|
} finally {
|
|
359
423
|
fs.closeSync(fd);
|
|
@@ -362,54 +426,62 @@ class MumpixStore {
|
|
|
362
426
|
|
|
363
427
|
_acquireLock(opts = {}) {
|
|
364
428
|
if (opts.lock === false) return;
|
|
365
|
-
const staleMs = Number.isFinite(opts.lockStaleMs) ? Number(opts.lockStaleMs) :
|
|
429
|
+
const staleMs = Number.isFinite(opts.lockStaleMs) ? Number(opts.lockStaleMs) : 300000;
|
|
366
430
|
const payload = JSON.stringify({ pid: process.pid, ts: Date.now() });
|
|
367
431
|
|
|
368
432
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
369
433
|
try {
|
|
370
|
-
this._lockFd = fs.openSync(this.lockPath,
|
|
371
|
-
fs.writeSync(this._lockFd, payload, null,
|
|
434
|
+
this._lockFd = fs.openSync(this.lockPath, "wx");
|
|
435
|
+
fs.writeSync(this._lockFd, payload, null, "utf8");
|
|
372
436
|
fs.fdatasyncSync(this._lockFd);
|
|
373
437
|
return;
|
|
374
438
|
} catch (err) {
|
|
375
|
-
if (!err || err.code !==
|
|
439
|
+
if (!err || err.code !== "EEXIST") throw err;
|
|
376
440
|
try {
|
|
377
|
-
const
|
|
378
|
-
if (Date.now() -
|
|
441
|
+
const stat = fs.statSync(this.lockPath);
|
|
442
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
379
443
|
fs.unlinkSync(this.lockPath);
|
|
380
444
|
continue;
|
|
381
445
|
}
|
|
382
446
|
} catch (_) {
|
|
383
|
-
// lock disappeared between checks; retry once
|
|
384
447
|
continue;
|
|
385
448
|
}
|
|
386
449
|
throw new Error(`mumpix: database is locked: ${this.filePath}`);
|
|
387
450
|
}
|
|
388
451
|
}
|
|
452
|
+
|
|
389
453
|
throw new Error(`mumpix: failed to acquire lock: ${this.filePath}`);
|
|
390
454
|
}
|
|
391
455
|
|
|
392
456
|
_releaseLock() {
|
|
393
457
|
if (this._lockFd !== null) {
|
|
394
|
-
try {
|
|
458
|
+
try {
|
|
459
|
+
fs.closeSync(this._lockFd);
|
|
460
|
+
} catch (_) {}
|
|
395
461
|
this._lockFd = null;
|
|
396
462
|
}
|
|
397
|
-
try {
|
|
463
|
+
try {
|
|
464
|
+
fs.unlinkSync(this.lockPath);
|
|
465
|
+
} catch (_) {}
|
|
398
466
|
}
|
|
399
467
|
|
|
400
468
|
_canRecoverFromIntegrityError(err) {
|
|
401
469
|
if (!this._strictIntegrity) return false;
|
|
402
470
|
if (!fs.existsSync(this.walPath)) return false;
|
|
403
|
-
const
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
471
|
+
const message = String((err && err.message) || err || "");
|
|
472
|
+
return (
|
|
473
|
+
message.includes("integrity hash mismatch") ||
|
|
474
|
+
message.includes("invalid record order/id") ||
|
|
475
|
+
message.includes("Unexpected token") ||
|
|
476
|
+
message.includes("corrupt or empty file")
|
|
477
|
+
);
|
|
408
478
|
}
|
|
409
479
|
|
|
410
480
|
_recoverFromTailCorruption() {
|
|
411
|
-
const
|
|
412
|
-
|
|
481
|
+
const lines = fs
|
|
482
|
+
.readFileSync(this.filePath, "utf8")
|
|
483
|
+
.split("\n")
|
|
484
|
+
.filter((line) => line.trim());
|
|
413
485
|
if (!lines.length) return false;
|
|
414
486
|
|
|
415
487
|
let header;
|
|
@@ -419,107 +491,84 @@ class MumpixStore {
|
|
|
419
491
|
return false;
|
|
420
492
|
}
|
|
421
493
|
|
|
422
|
-
const
|
|
423
|
-
const
|
|
494
|
+
const recovered = [];
|
|
495
|
+
const seen = new Set();
|
|
424
496
|
let lastId = 0;
|
|
425
|
-
let
|
|
497
|
+
let truncated = false;
|
|
426
498
|
|
|
427
499
|
for (let i = 1; i < lines.length; i++) {
|
|
428
500
|
try {
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
501
|
+
const record = JSON.parse(lines[i]);
|
|
502
|
+
const shape =
|
|
503
|
+
record &&
|
|
504
|
+
Number.isInteger(record.id) &&
|
|
505
|
+
typeof record.content === "string" &&
|
|
506
|
+
record.content.length > 0;
|
|
507
|
+
const ordered = shape && record.id > lastId;
|
|
508
|
+
const duplicate = shape && seen.has(record.id);
|
|
509
|
+
const validHash = shape && this._verifyHash(record);
|
|
510
|
+
if (!shape || !ordered || duplicate || !validHash) {
|
|
511
|
+
truncated = true;
|
|
436
512
|
break;
|
|
437
513
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
lastId =
|
|
514
|
+
recovered.push(record);
|
|
515
|
+
seen.add(record.id);
|
|
516
|
+
lastId = record.id;
|
|
441
517
|
} catch (_) {
|
|
442
|
-
|
|
518
|
+
truncated = true;
|
|
443
519
|
break;
|
|
444
520
|
}
|
|
445
521
|
}
|
|
446
522
|
|
|
447
|
-
if (!
|
|
448
|
-
this._rewriteSnapshot(header,
|
|
523
|
+
if (!truncated) return false;
|
|
524
|
+
this._rewriteSnapshot(header, recovered);
|
|
449
525
|
return true;
|
|
450
526
|
}
|
|
451
527
|
|
|
452
528
|
_rewriteSnapshot(header, records) {
|
|
453
|
-
const tmp = this.filePath
|
|
529
|
+
const tmp = `${this.filePath}.tmp.${process.pid}`;
|
|
454
530
|
const lines = [JSON.stringify(header)];
|
|
455
|
-
for (const
|
|
456
|
-
fs.writeFileSync(tmp, lines.join(
|
|
531
|
+
for (const record of records) lines.push(JSON.stringify(record));
|
|
532
|
+
fs.writeFileSync(tmp, `${lines.join("\n")}\n`, "utf8");
|
|
457
533
|
fs.renameSync(tmp, this.filePath);
|
|
458
534
|
}
|
|
459
535
|
|
|
460
|
-
_hash(
|
|
461
|
-
const secret = process.env.MUMPIX_INTEGRITY_SECRET
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
_verifyHash(record, prevH = '0') {
|
|
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;
|
|
536
|
+
_hash(content) {
|
|
537
|
+
const secret = process.env.MUMPIX_INTEGRITY_SECRET;
|
|
538
|
+
if (secret && secret.trim()) {
|
|
539
|
+
return `hmac256:${crypto
|
|
540
|
+
.createHmac("sha256", secret)
|
|
541
|
+
.update(String(content), "utf8")
|
|
542
|
+
.digest("hex")}`;
|
|
543
|
+
}
|
|
475
544
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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;
|
|
545
|
+
let hash = 2166136261;
|
|
546
|
+
for (let i = 0; i < content.length; i++) {
|
|
547
|
+
hash ^= content.charCodeAt(i);
|
|
548
|
+
hash = Math.imul(hash, 16777619);
|
|
488
549
|
}
|
|
550
|
+
return `0x${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
551
|
+
}
|
|
489
552
|
|
|
490
|
-
|
|
491
|
-
if (record.h
|
|
553
|
+
_verifyHash(record) {
|
|
554
|
+
if (!record || typeof record.content !== "string" || typeof record.h !== "string") {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
if (record.h.startsWith("hmac256:")) {
|
|
492
558
|
const secret = process.env.MUMPIX_INTEGRITY_SECRET;
|
|
493
|
-
|
|
494
|
-
const digest = crypto
|
|
495
|
-
.createHmac('sha256', secret)
|
|
496
|
-
.update(String(record.content), 'utf8')
|
|
497
|
-
.digest('hex');
|
|
498
|
-
return `hmac256:${digest}` === record.h;
|
|
559
|
+
return !!(secret && secret.trim()) && this._hash(record.content) === record.h;
|
|
499
560
|
}
|
|
500
|
-
|
|
501
|
-
// Backward compatibility for legacy FNV-1a records while migrating.
|
|
502
561
|
return this._hashLegacy(record.content) === record.h;
|
|
503
562
|
}
|
|
504
563
|
|
|
505
|
-
_hashLegacy(
|
|
506
|
-
let
|
|
507
|
-
for (let i = 0; i <
|
|
508
|
-
|
|
509
|
-
|
|
564
|
+
_hashLegacy(content) {
|
|
565
|
+
let hash = 2166136261;
|
|
566
|
+
for (let i = 0; i < content.length; i++) {
|
|
567
|
+
hash ^= content.charCodeAt(i);
|
|
568
|
+
hash = Math.imul(hash, 16777619);
|
|
510
569
|
}
|
|
511
|
-
return
|
|
570
|
+
return `0x${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
512
571
|
}
|
|
513
572
|
}
|
|
514
573
|
|
|
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
|
-
|
|
525
574
|
module.exports = { MumpixStore };
|