@frontmcp/storage-sqlite 0.1.0
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/LICENSE +201 -0
- package/encryption.d.ts +31 -0
- package/encryption.d.ts.map +1 -0
- package/esm/index.mjs +533 -0
- package/esm/package.json +57 -0
- package/index.d.ts +17 -0
- package/index.d.ts.map +1 -0
- package/index.js +553 -0
- package/package.json +57 -0
- package/sqlite-elicitation.store.d.ts +112 -0
- package/sqlite-elicitation.store.d.ts.map +1 -0
- package/sqlite-event.store.d.ts +69 -0
- package/sqlite-event.store.d.ts.map +1 -0
- package/sqlite-kv.store.d.ts +98 -0
- package/sqlite-kv.store.d.ts.map +1 -0
- package/sqlite-session.store.d.ts +55 -0
- package/sqlite-session.store.d.ts.map +1 -0
- package/sqlite.options.d.ts +51 -0
- package/sqlite.options.d.ts.map +1 -0
package/index.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// libs/storage-sqlite/src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
SqliteElicitationStore: () => SqliteElicitationStore,
|
|
24
|
+
SqliteEventStore: () => SqliteEventStore,
|
|
25
|
+
SqliteKvStore: () => SqliteKvStore,
|
|
26
|
+
SqliteSessionStore: () => SqliteSessionStore,
|
|
27
|
+
decryptValue: () => decryptValue,
|
|
28
|
+
deriveEncryptionKey: () => deriveEncryptionKey,
|
|
29
|
+
encryptValue: () => encryptValue,
|
|
30
|
+
sqliteStorageOptionsSchema: () => sqliteStorageOptionsSchema
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// libs/storage-sqlite/src/encryption.ts
|
|
35
|
+
var import_utils = require("@frontmcp/utils");
|
|
36
|
+
var HKDF_SALT = new TextEncoder().encode("frontmcp-sqlite-storage-v1");
|
|
37
|
+
var HKDF_INFO = new TextEncoder().encode("aes-256-gcm-value-encryption");
|
|
38
|
+
var KEY_LENGTH = 32;
|
|
39
|
+
function deriveEncryptionKey(secret) {
|
|
40
|
+
const ikm = new TextEncoder().encode(secret);
|
|
41
|
+
return (0, import_utils.hkdfSha256)(ikm, HKDF_SALT, HKDF_INFO, KEY_LENGTH);
|
|
42
|
+
}
|
|
43
|
+
var SEPARATOR = ":";
|
|
44
|
+
function encryptValue(key, plaintext) {
|
|
45
|
+
const iv = (0, import_utils.randomBytes)(12);
|
|
46
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
47
|
+
const { ciphertext, tag } = (0, import_utils.encryptAesGcm)(key, plaintextBytes, iv);
|
|
48
|
+
return [(0, import_utils.base64urlEncode)(iv), (0, import_utils.base64urlEncode)(tag), (0, import_utils.base64urlEncode)(ciphertext)].join(SEPARATOR);
|
|
49
|
+
}
|
|
50
|
+
function decryptValue(key, encrypted) {
|
|
51
|
+
const parts = encrypted.split(SEPARATOR);
|
|
52
|
+
if (parts.length !== 3) {
|
|
53
|
+
throw new Error("Invalid encrypted value format");
|
|
54
|
+
}
|
|
55
|
+
const iv = (0, import_utils.base64urlDecode)(parts[0]);
|
|
56
|
+
const tag = (0, import_utils.base64urlDecode)(parts[1]);
|
|
57
|
+
const ciphertext = (0, import_utils.base64urlDecode)(parts[2]);
|
|
58
|
+
const plaintext = (0, import_utils.decryptAesGcm)(key, ciphertext, iv, tag);
|
|
59
|
+
return new TextDecoder().decode(plaintext);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// libs/storage-sqlite/src/sqlite-kv.store.ts
|
|
63
|
+
var SqliteKvStore = class {
|
|
64
|
+
db;
|
|
65
|
+
encryptionKey = null;
|
|
66
|
+
cleanupTimer = null;
|
|
67
|
+
stmts = null;
|
|
68
|
+
constructor(options) {
|
|
69
|
+
const BetterSqlite3 = require("better-sqlite3");
|
|
70
|
+
try {
|
|
71
|
+
this.db = new BetterSqlite3(options.path);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
+
throw new Error(`SqliteKvStore: failed to open database at "${options.path}": ${message}`);
|
|
75
|
+
}
|
|
76
|
+
if (options.walMode !== false) {
|
|
77
|
+
this.db.pragma("journal_mode = WAL");
|
|
78
|
+
}
|
|
79
|
+
if (options.encryption?.secret) {
|
|
80
|
+
this.encryptionKey = deriveEncryptionKey(options.encryption.secret);
|
|
81
|
+
}
|
|
82
|
+
this.initSchema();
|
|
83
|
+
this.prepareStatements();
|
|
84
|
+
const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4;
|
|
85
|
+
if (cleanupInterval > 0) {
|
|
86
|
+
this.cleanupTimer = setInterval(() => this.purgeExpired(), cleanupInterval);
|
|
87
|
+
if (this.cleanupTimer.unref) {
|
|
88
|
+
this.cleanupTimer.unref();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
initSchema() {
|
|
93
|
+
this.db.exec(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
95
|
+
key TEXT PRIMARY KEY,
|
|
96
|
+
value TEXT NOT NULL,
|
|
97
|
+
expires_at INTEGER
|
|
98
|
+
);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_kv_expires ON kv(expires_at) WHERE expires_at IS NOT NULL;
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
prepareStatements() {
|
|
103
|
+
this.stmts = {
|
|
104
|
+
get: this.db.prepare("SELECT value, expires_at FROM kv WHERE key = ?"),
|
|
105
|
+
set: this.db.prepare("INSERT OR REPLACE INTO kv (key, value, expires_at) VALUES (?, ?, ?)"),
|
|
106
|
+
del: this.db.prepare("DELETE FROM kv WHERE key = ?"),
|
|
107
|
+
has: this.db.prepare("SELECT 1 FROM kv WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)"),
|
|
108
|
+
keys: this.db.prepare("SELECT key FROM kv WHERE (expires_at IS NULL OR expires_at > ?)"),
|
|
109
|
+
keysPattern: this.db.prepare("SELECT key FROM kv WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > ?)"),
|
|
110
|
+
cleanup: this.db.prepare("DELETE FROM kv WHERE expires_at IS NOT NULL AND expires_at <= ?"),
|
|
111
|
+
ttl: this.db.prepare("SELECT expires_at FROM kv WHERE key = ?"),
|
|
112
|
+
expire: this.db.prepare("UPDATE kv SET expires_at = ? WHERE key = ?")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Return prepared statements, throwing if not yet initialized.
|
|
117
|
+
*/
|
|
118
|
+
prepared() {
|
|
119
|
+
if (!this.stmts) {
|
|
120
|
+
throw new Error("SqliteKvStore: prepared statements not initialized. Was prepareStatements() called?");
|
|
121
|
+
}
|
|
122
|
+
return this.stmts;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get a value by key.
|
|
126
|
+
* Returns null if key doesn't exist or is expired.
|
|
127
|
+
*/
|
|
128
|
+
get(key) {
|
|
129
|
+
const stmts = this.prepared();
|
|
130
|
+
const row = stmts.get.get(key);
|
|
131
|
+
if (!row) return null;
|
|
132
|
+
if (row.expires_at !== null && row.expires_at <= Date.now()) {
|
|
133
|
+
stmts.del.run(key);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (this.encryptionKey) {
|
|
137
|
+
return decryptValue(this.encryptionKey, row.value);
|
|
138
|
+
}
|
|
139
|
+
return row.value;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get a value and parse it as JSON.
|
|
143
|
+
*/
|
|
144
|
+
getJSON(key) {
|
|
145
|
+
const value = this.get(key);
|
|
146
|
+
if (value === null) return null;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(value);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Set a key-value pair with optional TTL.
|
|
155
|
+
*
|
|
156
|
+
* @param key - The key
|
|
157
|
+
* @param value - The value to store
|
|
158
|
+
* @param ttlMs - Time to live in milliseconds (optional)
|
|
159
|
+
*/
|
|
160
|
+
set(key, value, ttlMs) {
|
|
161
|
+
const expiresAt = ttlMs !== void 0 ? Date.now() + ttlMs : null;
|
|
162
|
+
const storedValue = this.encryptionKey ? encryptValue(this.encryptionKey, value) : value;
|
|
163
|
+
this.prepared().set.run(key, storedValue, expiresAt);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Set a key-value pair with JSON serialization and optional TTL.
|
|
167
|
+
*/
|
|
168
|
+
setJSON(key, value, ttlMs) {
|
|
169
|
+
this.set(key, JSON.stringify(value), ttlMs);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Delete a key.
|
|
173
|
+
*/
|
|
174
|
+
del(key) {
|
|
175
|
+
this.prepared().del.run(key);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if a key exists (and is not expired).
|
|
179
|
+
*/
|
|
180
|
+
has(key) {
|
|
181
|
+
const row = this.prepared().has.get(key, Date.now());
|
|
182
|
+
return row !== void 0;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* List keys matching an optional glob-like pattern.
|
|
186
|
+
* Pattern uses SQL LIKE syntax: `%` for any characters, `_` for single character.
|
|
187
|
+
*
|
|
188
|
+
* @param pattern - Optional pattern (uses SQL LIKE). Without pattern, returns all keys.
|
|
189
|
+
* @returns Array of matching key strings
|
|
190
|
+
*/
|
|
191
|
+
keys(pattern) {
|
|
192
|
+
const stmts = this.prepared();
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
let rows;
|
|
195
|
+
if (pattern) {
|
|
196
|
+
rows = stmts.keysPattern.all(pattern, now);
|
|
197
|
+
} else {
|
|
198
|
+
rows = stmts.keys.all(now);
|
|
199
|
+
}
|
|
200
|
+
return rows.map((r) => r.key);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Set TTL on an existing key.
|
|
204
|
+
*
|
|
205
|
+
* @param key - The key to set expiry on
|
|
206
|
+
* @param ttlMs - Time to live in milliseconds
|
|
207
|
+
* @returns true if key exists and TTL was set, false if key doesn't exist
|
|
208
|
+
*/
|
|
209
|
+
expire(key, ttlMs) {
|
|
210
|
+
const expiresAt = Date.now() + ttlMs;
|
|
211
|
+
const result = this.prepared().expire.run(expiresAt, key);
|
|
212
|
+
return result.changes > 0;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get remaining TTL for a key in milliseconds.
|
|
216
|
+
*
|
|
217
|
+
* @returns TTL in ms, -1 if no expiry, -2 if key doesn't exist
|
|
218
|
+
*/
|
|
219
|
+
ttl(key) {
|
|
220
|
+
const stmts = this.prepared();
|
|
221
|
+
const row = stmts.ttl.get(key);
|
|
222
|
+
if (!row) return -2;
|
|
223
|
+
if (row.expires_at === null) return -1;
|
|
224
|
+
const remaining = row.expires_at - Date.now();
|
|
225
|
+
if (remaining <= 0) {
|
|
226
|
+
stmts.del.run(key);
|
|
227
|
+
return -2;
|
|
228
|
+
}
|
|
229
|
+
return remaining;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Purge all expired keys.
|
|
233
|
+
* Called periodically by the cleanup timer.
|
|
234
|
+
*/
|
|
235
|
+
purgeExpired() {
|
|
236
|
+
const result = this.prepared().cleanup.run(Date.now());
|
|
237
|
+
return result.changes;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Close the database connection and stop cleanup timer.
|
|
241
|
+
*/
|
|
242
|
+
close() {
|
|
243
|
+
if (this.cleanupTimer) {
|
|
244
|
+
clearInterval(this.cleanupTimer);
|
|
245
|
+
this.cleanupTimer = null;
|
|
246
|
+
}
|
|
247
|
+
this.db.close();
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get the underlying database instance (for advanced use cases / testing).
|
|
251
|
+
*/
|
|
252
|
+
getDatabase() {
|
|
253
|
+
return this.db;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// libs/storage-sqlite/src/sqlite-session.store.ts
|
|
258
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
259
|
+
var SqliteSessionStore = class {
|
|
260
|
+
kv;
|
|
261
|
+
keyPrefix;
|
|
262
|
+
defaultTtlMs;
|
|
263
|
+
constructor(options) {
|
|
264
|
+
this.kv = new SqliteKvStore(options);
|
|
265
|
+
this.keyPrefix = options.keyPrefix ?? "mcp:session:";
|
|
266
|
+
this.defaultTtlMs = options.defaultTtlMs ?? 36e5;
|
|
267
|
+
}
|
|
268
|
+
sessionKey(sessionId) {
|
|
269
|
+
return `${this.keyPrefix}${sessionId}`;
|
|
270
|
+
}
|
|
271
|
+
async get(sessionId) {
|
|
272
|
+
return this.kv.getJSON(this.sessionKey(sessionId));
|
|
273
|
+
}
|
|
274
|
+
async set(sessionId, session, ttlMs) {
|
|
275
|
+
this.kv.setJSON(this.sessionKey(sessionId), session, ttlMs ?? this.defaultTtlMs);
|
|
276
|
+
}
|
|
277
|
+
async delete(sessionId) {
|
|
278
|
+
this.kv.del(this.sessionKey(sessionId));
|
|
279
|
+
}
|
|
280
|
+
async exists(sessionId) {
|
|
281
|
+
return this.kv.has(this.sessionKey(sessionId));
|
|
282
|
+
}
|
|
283
|
+
allocId() {
|
|
284
|
+
return (0, import_utils2.randomUUID)();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Close the underlying SQLite connection.
|
|
288
|
+
*/
|
|
289
|
+
close() {
|
|
290
|
+
this.kv.close();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get the underlying KV store (for testing/advanced use).
|
|
294
|
+
*/
|
|
295
|
+
getKvStore() {
|
|
296
|
+
return this.kv;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// libs/storage-sqlite/src/sqlite-elicitation.store.ts
|
|
301
|
+
var import_node_events = require("node:events");
|
|
302
|
+
var SqliteElicitationStore = class {
|
|
303
|
+
kv;
|
|
304
|
+
emitter;
|
|
305
|
+
keyPrefix;
|
|
306
|
+
constructor(options) {
|
|
307
|
+
this.kv = new SqliteKvStore(options);
|
|
308
|
+
this.emitter = new import_node_events.EventEmitter();
|
|
309
|
+
this.emitter.setMaxListeners(100);
|
|
310
|
+
this.keyPrefix = options.keyPrefix ?? "mcp:elicit:";
|
|
311
|
+
}
|
|
312
|
+
pendingKey(sessionId) {
|
|
313
|
+
return `${this.keyPrefix}pending:${sessionId}`;
|
|
314
|
+
}
|
|
315
|
+
fallbackKey(elicitId) {
|
|
316
|
+
return `${this.keyPrefix}fallback:${elicitId}`;
|
|
317
|
+
}
|
|
318
|
+
resolvedKey(elicitId) {
|
|
319
|
+
return `${this.keyPrefix}resolved:${elicitId}`;
|
|
320
|
+
}
|
|
321
|
+
resultChannel(elicitId) {
|
|
322
|
+
return `result:${elicitId}`;
|
|
323
|
+
}
|
|
324
|
+
fallbackResultChannel(elicitId) {
|
|
325
|
+
return `fallback-result:${elicitId}`;
|
|
326
|
+
}
|
|
327
|
+
async setPending(record) {
|
|
328
|
+
const ttlMs = record.expiresAt - Date.now();
|
|
329
|
+
if (ttlMs <= 0) return;
|
|
330
|
+
this.kv.setJSON(this.pendingKey(record.sessionId), record, ttlMs);
|
|
331
|
+
}
|
|
332
|
+
async getPending(sessionId) {
|
|
333
|
+
return this.kv.getJSON(this.pendingKey(sessionId));
|
|
334
|
+
}
|
|
335
|
+
async deletePending(sessionId) {
|
|
336
|
+
this.kv.del(this.pendingKey(sessionId));
|
|
337
|
+
}
|
|
338
|
+
async subscribeResult(elicitId, callback, _sessionId) {
|
|
339
|
+
const channel = this.resultChannel(elicitId);
|
|
340
|
+
const handler = (result) => callback(result);
|
|
341
|
+
this.emitter.on(channel, handler);
|
|
342
|
+
return async () => {
|
|
343
|
+
this.emitter.removeListener(channel, handler);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
async publishResult(elicitId, sessionId, result) {
|
|
347
|
+
this.kv.del(this.pendingKey(sessionId));
|
|
348
|
+
this.emitter.emit(this.resultChannel(elicitId), result);
|
|
349
|
+
}
|
|
350
|
+
async setPendingFallback(record) {
|
|
351
|
+
const elicitId = record["elicitId"];
|
|
352
|
+
if (!elicitId) return;
|
|
353
|
+
this.kv.setJSON(this.fallbackKey(elicitId), record, 3e5);
|
|
354
|
+
}
|
|
355
|
+
async getPendingFallback(elicitId, _sessionId) {
|
|
356
|
+
return this.kv.getJSON(this.fallbackKey(elicitId));
|
|
357
|
+
}
|
|
358
|
+
async deletePendingFallback(elicitId) {
|
|
359
|
+
this.kv.del(this.fallbackKey(elicitId));
|
|
360
|
+
}
|
|
361
|
+
async setResolvedResult(elicitId, result, _sessionId) {
|
|
362
|
+
this.kv.setJSON(this.resolvedKey(elicitId), result, 3e5);
|
|
363
|
+
}
|
|
364
|
+
async getResolvedResult(elicitId, _sessionId) {
|
|
365
|
+
return this.kv.getJSON(this.resolvedKey(elicitId));
|
|
366
|
+
}
|
|
367
|
+
async deleteResolvedResult(elicitId) {
|
|
368
|
+
this.kv.del(this.resolvedKey(elicitId));
|
|
369
|
+
}
|
|
370
|
+
async subscribeFallbackResult(elicitId, callback, _sessionId) {
|
|
371
|
+
const channel = this.fallbackResultChannel(elicitId);
|
|
372
|
+
const handler = (result) => callback(result);
|
|
373
|
+
this.emitter.on(channel, handler);
|
|
374
|
+
return async () => {
|
|
375
|
+
this.emitter.removeListener(channel, handler);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async publishFallbackResult(elicitId, _sessionId, result) {
|
|
379
|
+
this.emitter.emit(this.fallbackResultChannel(elicitId), result);
|
|
380
|
+
}
|
|
381
|
+
async destroy() {
|
|
382
|
+
this.emitter.removeAllListeners();
|
|
383
|
+
this.kv.close();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get the underlying KV store (for testing/advanced use).
|
|
387
|
+
*/
|
|
388
|
+
getKvStore() {
|
|
389
|
+
return this.kv;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// libs/storage-sqlite/src/sqlite-event.store.ts
|
|
394
|
+
var SqliteEventStore = class {
|
|
395
|
+
db;
|
|
396
|
+
maxEvents;
|
|
397
|
+
ttlMs;
|
|
398
|
+
counters = /* @__PURE__ */ new Map();
|
|
399
|
+
cleanupTimer = null;
|
|
400
|
+
stmts = null;
|
|
401
|
+
constructor(options) {
|
|
402
|
+
const BetterSqlite3 = require("better-sqlite3");
|
|
403
|
+
try {
|
|
404
|
+
this.db = new BetterSqlite3(options.path);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
407
|
+
throw new Error(`SqliteEventStore: failed to open database at "${options.path}": ${message}`);
|
|
408
|
+
}
|
|
409
|
+
if (options.walMode !== false) {
|
|
410
|
+
this.db.pragma("journal_mode = WAL");
|
|
411
|
+
}
|
|
412
|
+
this.maxEvents = options.maxEvents ?? 1e4;
|
|
413
|
+
this.ttlMs = options.ttlMs ?? 3e5;
|
|
414
|
+
this.initSchema();
|
|
415
|
+
this.prepareStatements();
|
|
416
|
+
const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4;
|
|
417
|
+
if (cleanupInterval > 0) {
|
|
418
|
+
this.cleanupTimer = setInterval(() => this.evictExpired(), cleanupInterval);
|
|
419
|
+
if (this.cleanupTimer.unref) {
|
|
420
|
+
this.cleanupTimer.unref();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
initSchema() {
|
|
425
|
+
this.db.exec(`
|
|
426
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
427
|
+
id TEXT PRIMARY KEY,
|
|
428
|
+
stream_id TEXT NOT NULL,
|
|
429
|
+
message TEXT NOT NULL,
|
|
430
|
+
created_at INTEGER NOT NULL
|
|
431
|
+
);
|
|
432
|
+
CREATE INDEX IF NOT EXISTS idx_events_stream ON events(stream_id, created_at);
|
|
433
|
+
CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at);
|
|
434
|
+
`);
|
|
435
|
+
}
|
|
436
|
+
prepareStatements() {
|
|
437
|
+
this.stmts = {
|
|
438
|
+
insert: this.db.prepare("INSERT INTO events (id, stream_id, message, created_at) VALUES (?, ?, ?, ?)"),
|
|
439
|
+
getAfter: this.db.prepare(
|
|
440
|
+
"SELECT id, message, created_at FROM events WHERE stream_id = ? AND rowid > (SELECT rowid FROM events WHERE id = ?) AND created_at > ? ORDER BY rowid ASC"
|
|
441
|
+
),
|
|
442
|
+
getStreamId: this.db.prepare("SELECT stream_id FROM events WHERE id = ?"),
|
|
443
|
+
count: this.db.prepare("SELECT COUNT(*) as count FROM events"),
|
|
444
|
+
evictOldest: this.db.prepare(
|
|
445
|
+
"DELETE FROM events WHERE id IN (SELECT id FROM events ORDER BY created_at ASC LIMIT ?)"
|
|
446
|
+
),
|
|
447
|
+
evictExpired: this.db.prepare("DELETE FROM events WHERE created_at <= ?"),
|
|
448
|
+
getLastId: this.db.prepare("SELECT id FROM events WHERE stream_id = ? ORDER BY rowid DESC LIMIT 1")
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Return prepared statements, throwing if not yet initialized.
|
|
453
|
+
*/
|
|
454
|
+
prepared() {
|
|
455
|
+
if (!this.stmts) {
|
|
456
|
+
throw new Error("SqliteEventStore: prepared statements not initialized. Was prepareStatements() called?");
|
|
457
|
+
}
|
|
458
|
+
return this.stmts;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get the next counter value for a stream, rehydrating from SQLite on first use.
|
|
462
|
+
*/
|
|
463
|
+
getNextCounter(streamId) {
|
|
464
|
+
let counter = this.counters.get(streamId);
|
|
465
|
+
if (counter === void 0) {
|
|
466
|
+
const stmts = this.prepared();
|
|
467
|
+
const row = stmts.getLastId.get(streamId);
|
|
468
|
+
if (row) {
|
|
469
|
+
const lastSegment = row.id.split(":").pop();
|
|
470
|
+
counter = lastSegment ? parseInt(lastSegment, 10) : 0;
|
|
471
|
+
if (isNaN(counter)) counter = 0;
|
|
472
|
+
} else {
|
|
473
|
+
counter = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
counter += 1;
|
|
477
|
+
this.counters.set(streamId, counter);
|
|
478
|
+
return counter;
|
|
479
|
+
}
|
|
480
|
+
async storeEvent(streamId, message) {
|
|
481
|
+
const stmts = this.prepared();
|
|
482
|
+
this.evictExpired();
|
|
483
|
+
const counter = this.getNextCounter(streamId);
|
|
484
|
+
const id = `${streamId}:${counter}`;
|
|
485
|
+
const now = Date.now();
|
|
486
|
+
stmts.insert.run(id, streamId, JSON.stringify(message), now);
|
|
487
|
+
const countRow = stmts.count.get();
|
|
488
|
+
if (countRow.count > this.maxEvents) {
|
|
489
|
+
const excess = countRow.count - this.maxEvents;
|
|
490
|
+
stmts.evictOldest.run(excess);
|
|
491
|
+
}
|
|
492
|
+
return id;
|
|
493
|
+
}
|
|
494
|
+
async replayEventsAfter(lastEventId, { send }) {
|
|
495
|
+
const stmts = this.prepared();
|
|
496
|
+
const streamRow = stmts.getStreamId.get(lastEventId);
|
|
497
|
+
if (!streamRow) {
|
|
498
|
+
return "default-stream";
|
|
499
|
+
}
|
|
500
|
+
const streamId = streamRow.stream_id;
|
|
501
|
+
const expiryCutoff = Date.now() - this.ttlMs;
|
|
502
|
+
const rows = stmts.getAfter.all(streamId, lastEventId, expiryCutoff);
|
|
503
|
+
for (const row of rows) {
|
|
504
|
+
await send(row.id, JSON.parse(row.message));
|
|
505
|
+
}
|
|
506
|
+
return streamId;
|
|
507
|
+
}
|
|
508
|
+
evictExpired() {
|
|
509
|
+
const stmts = this.prepared();
|
|
510
|
+
const cutoff = Date.now() - this.ttlMs;
|
|
511
|
+
stmts.evictExpired.run(cutoff);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Close the database connection and stop cleanup timer.
|
|
515
|
+
*/
|
|
516
|
+
close() {
|
|
517
|
+
if (this.cleanupTimer) {
|
|
518
|
+
clearInterval(this.cleanupTimer);
|
|
519
|
+
this.cleanupTimer = null;
|
|
520
|
+
}
|
|
521
|
+
this.db.close();
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Get the current number of stored events.
|
|
525
|
+
*/
|
|
526
|
+
get size() {
|
|
527
|
+
const stmts = this.prepared();
|
|
528
|
+
const row = stmts.count.get();
|
|
529
|
+
return row.count;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// libs/storage-sqlite/src/sqlite.options.ts
|
|
534
|
+
var import_zod = require("zod");
|
|
535
|
+
var sqliteStorageOptionsSchema = import_zod.z.object({
|
|
536
|
+
path: import_zod.z.string().min(1),
|
|
537
|
+
encryption: import_zod.z.object({
|
|
538
|
+
secret: import_zod.z.string().min(1)
|
|
539
|
+
}).optional(),
|
|
540
|
+
ttlCleanupIntervalMs: import_zod.z.number().int().positive().optional().default(6e4),
|
|
541
|
+
walMode: import_zod.z.boolean().optional().default(true)
|
|
542
|
+
});
|
|
543
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
544
|
+
0 && (module.exports = {
|
|
545
|
+
SqliteElicitationStore,
|
|
546
|
+
SqliteEventStore,
|
|
547
|
+
SqliteKvStore,
|
|
548
|
+
SqliteSessionStore,
|
|
549
|
+
decryptValue,
|
|
550
|
+
deriveEncryptionKey,
|
|
551
|
+
encryptValue,
|
|
552
|
+
sqliteStorageOptionsSchema
|
|
553
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frontmcp/storage-sqlite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SQLite storage backend for FrontMCP - local session, elicitation, and event persistence without Redis",
|
|
5
|
+
"author": "AgentFront <info@agentfront.dev>",
|
|
6
|
+
"homepage": "https://docs.agentfront.dev",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"sqlite",
|
|
11
|
+
"storage",
|
|
12
|
+
"session",
|
|
13
|
+
"local",
|
|
14
|
+
"agentfront",
|
|
15
|
+
"frontmcp"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/agentfront/frontmcp.git",
|
|
20
|
+
"directory": "libs/storage-sqlite"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/agentfront/frontmcp/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"main": "./index.js",
|
|
27
|
+
"module": "./esm/index.mjs",
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
".": {
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./index.d.ts",
|
|
35
|
+
"default": "./index.js"
|
|
36
|
+
},
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./index.d.ts",
|
|
39
|
+
"default": "./esm/index.mjs"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@frontmcp/utils": "0.9.0",
|
|
48
|
+
"better-sqlite3": "^12.6.2"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"zod": "^4.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
}
|
|
57
|
+
}
|