@monlite/core 0.10.0 → 1.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/README.md CHANGED
@@ -551,6 +551,40 @@ Write your own against the `MonlitePlugin` interface (`init` / `afterWrite` /
551
551
 
552
552
  ---
553
553
 
554
+ ## The local backend for AI agents
555
+
556
+ monlite aims to be your **entire local data layer** — one embedded `.db`, one
557
+ install — collapsing the services you'd otherwise run (Mongo, Qdrant, Redis) for
558
+ a local/edge/desktop agent. Documents and vectors are core + a plugin; the
559
+ Redis-style primitives are small companion packages:
560
+
561
+ | Package | Replaces (locally) | Provides |
562
+ |---|---|---|
563
+ | [`@monlite/kv`](https://www.npmjs.com/package/@monlite/kv) | Redis cache | Synchronous `get/set/incr` KV with TTLs |
564
+ | [`@monlite/queue`](https://www.npmjs.com/package/@monlite/queue) | Redis / BullMQ | Durable job queue — retries, backoff, delays, priorities, concurrency |
565
+ | [`@monlite/cron`](https://www.npmjs.com/package/@monlite/cron) | cron / scheduler | Persisted cron schedules; composes with the queue |
566
+
567
+ ```ts
568
+ import { kv } from "@monlite/kv";
569
+ import { createQueue } from "@monlite/queue";
570
+ import { createCron } from "@monlite/cron";
571
+
572
+ const cache = kv(db);
573
+ cache.set("session:42", { user: "ali" }, { ttl: 60_000 });
574
+
575
+ const queue = createQueue(db, { maxAttempts: 3 });
576
+ queue.process("email", async (job) => send(job.payload), { concurrency: 5 });
577
+ queue.add("email", { to: "ali@example.com" });
578
+
579
+ createCron(db).schedule("nightly", "0 0 * * *", () => queue.add("report", {}));
580
+ ```
581
+
582
+ These target **local / edge / desktop** runtimes — not a distributed cloud-scale
583
+ Redis/Mongo/Qdrant replacement. For scale, keep the real services and
584
+ [`@monlite/sync`](https://www.npmjs.com/package/@monlite/sync) to them.
585
+
586
+ ---
587
+
554
588
  ## Drivers & zero dependencies
555
589
 
556
590
  monlite talks to SQLite through a tiny driver adapter, so it runs on two
@@ -578,6 +612,30 @@ based on your Node version and whether you want the extra dependency.
578
612
 
579
613
  ---
580
614
 
615
+ ## Encryption at rest
616
+
617
+ Encrypt the whole database file with a key. Install the drop-in cipher driver
618
+ and pass an `encryption` option:
619
+
620
+ ```bash
621
+ npm install better-sqlite3-multiple-ciphers
622
+ ```
623
+
624
+ ```ts
625
+ const db = createDb("./secure.db", { encryption: { key: process.env.DB_KEY } });
626
+ // ...use db exactly as normal — everything on disk is encrypted.
627
+
628
+ db.rekey(newKey); // rotate the key
629
+ ```
630
+
631
+ - A **wrong or missing key throws `MonliteEncryptionError`** when opening.
632
+ - Optional `cipher` selects the scheme (`"sqlcipher"`, `"chacha20"`,
633
+ `"aes256cbc"`, …); the default is ChaCha20-Poly1305.
634
+ - Encryption requires `better-sqlite3-multiple-ciphers` (a drop-in for
635
+ `better-sqlite3`) and is **not** available on the `node:sqlite` backend.
636
+
637
+ ---
638
+
581
639
  ## How it works
582
640
 
583
641
  Every collection is a single SQLite table:
@@ -610,6 +668,22 @@ future-proofing.
610
668
 
611
669
  ---
612
670
 
671
+ ## Examples
672
+
673
+ Runnable demos live in [`examples/`](examples/): a notes app (CRUD + full-text
674
+ search + live queries), AI-agent memory (vector + hybrid search), and local-first
675
+ sync. `cd examples && npm install && node notes.mjs`.
676
+
677
+ ## Benchmarks
678
+
679
+ [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) compares monlite to the raw SQLite
680
+ driver, NeDB, and lowdb (`pnpm bench` to reproduce). In short: ~150k–250k
681
+ ops/sec, roughly 2× the raw-driver overhead for the full document API, and it
682
+ **stays flat on indexed reads where JSON-file stores degrade** (lowdb point reads
683
+ are ~15× slower at 10k docs).
684
+
685
+ ---
686
+
613
687
  ## License
614
688
 
615
689
  MIT 🌙
package/dist/index.cjs CHANGED
@@ -119,6 +119,12 @@ var MonliteQueryError = class extends MonliteError {
119
119
  this.name = "MonliteQueryError";
120
120
  }
121
121
  };
122
+ var MonliteEncryptionError = class extends MonliteError {
123
+ constructor(message, options) {
124
+ super(message, options);
125
+ this.name = "MonliteEncryptionError";
126
+ }
127
+ };
122
128
  var MonliteConstraintError = class extends MonliteError {
123
129
  collection;
124
130
  constructor(message, options) {
@@ -1317,6 +1323,7 @@ var AutoIndexer = class {
1317
1323
 
1318
1324
  // src/driver/better-sqlite3.ts
1319
1325
  var STMT_CACHE_MAX = 256;
1326
+ var quote = (s) => s.replace(/'/g, "''");
1320
1327
  var BetterSqlite3Driver = class {
1321
1328
  name = "better-sqlite3";
1322
1329
  raw;
@@ -1327,6 +1334,9 @@ var BetterSqlite3Driver = class {
1327
1334
  this.raw = new BetterSqlite3(filename, {
1328
1335
  readonly: options.readonly ?? false
1329
1336
  });
1337
+ if (options.encryption) {
1338
+ this.applyKey(options.encryption.key, options.encryption.cipher);
1339
+ }
1330
1340
  this.raw.pragma("foreign_keys = ON");
1331
1341
  this.raw.pragma(`busy_timeout = ${options.busyTimeout ?? 5e3}`);
1332
1342
  if (!options.readonly && (options.wal ?? true)) {
@@ -1355,6 +1365,28 @@ var BetterSqlite3Driver = class {
1355
1365
  transaction(fn) {
1356
1366
  return this.raw.transaction(fn)();
1357
1367
  }
1368
+ /** Apply the encryption key and verify it by reading the schema. */
1369
+ applyKey(key, cipher) {
1370
+ if (cipher) this.raw.pragma(`cipher='${quote(cipher)}'`);
1371
+ this.raw.pragma(`key='${quote(key)}'`);
1372
+ try {
1373
+ this.raw.exec("SELECT count(*) FROM sqlite_master");
1374
+ } catch (err) {
1375
+ this.raw.close();
1376
+ throw new MonliteEncryptionError(
1377
+ "Failed to open the encrypted database: the key is incorrect, or the file is not encrypted.",
1378
+ { cause: err }
1379
+ );
1380
+ }
1381
+ }
1382
+ rekey(key, cipher) {
1383
+ if (cipher) this.raw.pragma(`cipher='${quote(cipher)}'`);
1384
+ const mode = String(this.raw.pragma("journal_mode", { simple: true }));
1385
+ const wasWal = mode.toLowerCase() === "wal";
1386
+ if (wasWal) this.raw.pragma("journal_mode = DELETE");
1387
+ this.raw.pragma(`rekey='${quote(key)}'`);
1388
+ if (wasWal) this.raw.pragma("journal_mode = WAL");
1389
+ }
1358
1390
  close() {
1359
1391
  this.cache.clear();
1360
1392
  this.raw.close();
@@ -1370,6 +1402,11 @@ var NodeSqliteDriver = class {
1370
1402
  cache = /* @__PURE__ */ new Map();
1371
1403
  depth = 0;
1372
1404
  constructor(nodeSqlite, filename, options) {
1405
+ if (options.encryption) {
1406
+ throw new MonliteError(
1407
+ "Encryption is not supported on the node:sqlite backend. Use better-sqlite3 with the better-sqlite3-multiple-ciphers package."
1408
+ );
1409
+ }
1373
1410
  this.verbose = options.verbose;
1374
1411
  const { DatabaseSync } = nodeSqlite;
1375
1412
  this.raw = new DatabaseSync(filename, {
@@ -1445,6 +1482,14 @@ function loadBetterSqlite3() {
1445
1482
  return null;
1446
1483
  }
1447
1484
  }
1485
+ function loadCipherSqlite3() {
1486
+ try {
1487
+ const mod = req("better-sqlite3-multiple-ciphers");
1488
+ return mod?.default ?? mod;
1489
+ } catch {
1490
+ return null;
1491
+ }
1492
+ }
1448
1493
  function loadNodeSqlite() {
1449
1494
  try {
1450
1495
  return req("node:sqlite");
@@ -1454,6 +1499,20 @@ function loadNodeSqlite() {
1454
1499
  }
1455
1500
  function createDriver(filename, options = {}) {
1456
1501
  const choice = options.driver ?? "auto";
1502
+ if (options.encryption) {
1503
+ if (choice === "node:sqlite") {
1504
+ throw new MonliteError(
1505
+ `Encryption is not supported on the node:sqlite backend. Use better-sqlite3 with the better-sqlite3-multiple-ciphers package.`
1506
+ );
1507
+ }
1508
+ const cipher = loadCipherSqlite3();
1509
+ if (!cipher) {
1510
+ throw new MonliteError(
1511
+ `Encryption requires the "better-sqlite3-multiple-ciphers" package (a drop-in for better-sqlite3). Run \`npm install better-sqlite3-multiple-ciphers\`.`
1512
+ );
1513
+ }
1514
+ return new BetterSqlite3Driver(cipher, filename, options);
1515
+ }
1457
1516
  if (choice === "better-sqlite3") {
1458
1517
  const mod = loadBetterSqlite3();
1459
1518
  if (!mod) {
@@ -1893,6 +1952,7 @@ var Monlite = class {
1893
1952
  $sync;
1894
1953
  collections = /* @__PURE__ */ new Map();
1895
1954
  plugins;
1955
+ encrypted;
1896
1956
  closed = false;
1897
1957
  constructor(filename, options = {}) {
1898
1958
  this.driver = createDriver(filename, {
@@ -1901,8 +1961,10 @@ var Monlite = class {
1901
1961
  wal: options.wal,
1902
1962
  busyTimeout: options.busyTimeout,
1903
1963
  allowExtensions: options.allowExtensions,
1964
+ encryption: options.encryption,
1904
1965
  verbose: options.verbose
1905
1966
  });
1967
+ this.encrypted = options.encryption !== void 0;
1906
1968
  this.autoIndexer = new AutoIndexer(
1907
1969
  this.driver,
1908
1970
  options.autoIndex ?? true,
@@ -2050,6 +2112,19 @@ var Monlite = class {
2050
2112
  this.driver.exec(`VACUUM INTO '${path.replace(/'/g, "''")}'`);
2051
2113
  return Promise.resolve();
2052
2114
  }
2115
+ /**
2116
+ * Rotate the encryption key. Only valid for a database opened with the
2117
+ * `encryption` option; throws otherwise. Pass `cipher` to also change scheme.
2118
+ */
2119
+ rekey(key, cipher) {
2120
+ this.assertOpen();
2121
+ if (!this.encrypted || !this.driver.rekey) {
2122
+ throw new MonliteError(
2123
+ "rekey() requires a database opened with the `encryption` option."
2124
+ );
2125
+ }
2126
+ this.driver.rekey(key, cipher);
2127
+ }
2053
2128
  /** Close the underlying SQLite connection. */
2054
2129
  $disconnect() {
2055
2130
  if (!this.closed) {
@@ -2069,6 +2144,7 @@ function createDb(filename, options) {
2069
2144
  exports.Collection = Collection;
2070
2145
  exports.Monlite = Monlite;
2071
2146
  exports.MonliteConstraintError = MonliteConstraintError;
2147
+ exports.MonliteEncryptionError = MonliteEncryptionError;
2072
2148
  exports.MonliteError = MonliteError;
2073
2149
  exports.MonliteForeignKeyError = MonliteForeignKeyError;
2074
2150
  exports.MonliteNotNullError = MonliteNotNullError;