@monlite/core 1.0.0 → 1.2.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/dist/index.d.cts CHANGED
@@ -27,6 +27,24 @@ declare class Collection<T = Doc> {
27
27
  private columnDdl;
28
28
  /** Auto-additive migration: add declared columns missing from an existing table. */
29
29
  private migrateColumns;
30
+ private foreignKeysOn;
31
+ private declaredIndexDdl;
32
+ /**
33
+ * Reconcile the physical table to the declared schema, performing the changes
34
+ * the auto-additive path can't: **dropping** columns, **renaming** them, and
35
+ * **changing a column's type/constraints** — via a safe, transactional table
36
+ * rebuild that preserves data. Structured collections only.
37
+ *
38
+ * Pass `rename` to map an existing physical column to a new declared name, and
39
+ * `drop` to acknowledge columns that the new schema removes (an unacknowledged
40
+ * column drop throws, so data is never lost by accident).
41
+ *
42
+ * ```ts
43
+ * const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
44
+ * await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
45
+ * ```
46
+ */
47
+ $migrate(options?: MigrateOptions): Promise<void>;
30
48
  private rowToDoc;
31
49
  private encodeColumn;
32
50
  private insertColumns;
@@ -167,6 +185,13 @@ interface CollectionOptions {
167
185
  schema?: CollectionSchema;
168
186
  }
169
187
  type CollectionMode = "document" | "structured";
188
+ /** Options for {@link Collection.$migrate}. */
189
+ interface MigrateOptions {
190
+ /** Rename existing physical columns: `{ oldName: newDeclaredName }`. */
191
+ rename?: Record<string, string>;
192
+ /** Acknowledge physical columns to drop (not present in the new schema). */
193
+ drop?: string[];
194
+ }
170
195
  /** A column as reported by {@link Monlite.$schema}. */
171
196
  interface ColumnInfo {
172
197
  name: string;
@@ -357,12 +382,28 @@ interface GroupByArgs<T = Doc> {
357
382
  }
358
383
  type GroupByResult = Record<string, any>;
359
384
  type DriverName = "auto" | "better-sqlite3" | "node:sqlite";
385
+ /** Encryption-at-rest configuration (requires `better-sqlite3-multiple-ciphers`). */
386
+ interface EncryptionOptions {
387
+ /** The passphrase used to encrypt/decrypt the database file. */
388
+ key: string;
389
+ /**
390
+ * Cipher scheme to use (e.g. `"sqlcipher"`, `"chacha20"`, `"aes256cbc"`).
391
+ * Defaults to the library default (ChaCha20-Poly1305).
392
+ */
393
+ cipher?: string;
394
+ }
360
395
  interface MonliteOptions {
361
396
  /**
362
397
  * Which SQLite backend to use. `"auto"` (default) prefers `better-sqlite3`
363
398
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
364
399
  */
365
400
  driver?: DriverName;
401
+ /**
402
+ * Encrypt the database at rest. Requires the `better-sqlite3-multiple-ciphers`
403
+ * package (a drop-in for `better-sqlite3`); not supported on `node:sqlite`.
404
+ * Use `db.rekey(newKey)` to rotate the key.
405
+ */
406
+ encryption?: EncryptionOptions;
366
407
  /** Opt-in plugins (e.g. `@monlite/fts`). */
367
408
  plugins?: MonlitePlugin[];
368
409
  /**
@@ -414,6 +455,8 @@ interface Driver {
414
455
  /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
415
456
  transaction<T>(fn: () => T): T;
416
457
  close(): void;
458
+ /** Rotate the encryption key (encrypted backends only). */
459
+ rekey?(key: string, cipher?: string): void;
417
460
  /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
418
461
  readonly raw: any;
419
462
  }
@@ -613,6 +656,7 @@ declare class Monlite {
613
656
  readonly $sync?: SyncStore;
614
657
  private readonly collections;
615
658
  private readonly plugins;
659
+ private readonly encrypted;
616
660
  private closed;
617
661
  constructor(filename: string, options?: MonliteOptions);
618
662
  /** @internal Notify plugins that documents changed (post-commit). */
@@ -655,6 +699,11 @@ declare class Monlite {
655
699
  * `VACUUM INTO`). The destination file must not already exist.
656
700
  */
657
701
  backup(path: string): Promise<void>;
702
+ /**
703
+ * Rotate the encryption key. Only valid for a database opened with the
704
+ * `encryption` option; throws otherwise. Pass `cipher` to also change scheme.
705
+ */
706
+ rekey(key: string, cipher?: string): void;
658
707
  /** Close the underlying SQLite connection. */
659
708
  $disconnect(): Promise<void>;
660
709
  private assertOpen;
@@ -677,6 +726,12 @@ declare class MonliteQueryError extends MonliteError {
677
726
  cause?: unknown;
678
727
  });
679
728
  }
729
+ /** Thrown when an encrypted database can't be opened (wrong key, not encrypted). */
730
+ declare class MonliteEncryptionError extends MonliteError {
731
+ constructor(message: string, options?: {
732
+ cause?: unknown;
733
+ });
734
+ }
680
735
  /** A database constraint was violated (base class for the specific kinds). */
681
736
  declare class MonliteConstraintError extends MonliteError {
682
737
  readonly collection?: string;
@@ -717,4 +772,4 @@ declare function objectId(): string;
717
772
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
718
773
  declare function isObjectId(value: unknown): value is string;
719
774
 
720
- export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, Monlite, MonliteConstraintError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
775
+ export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
package/dist/index.d.ts CHANGED
@@ -27,6 +27,24 @@ declare class Collection<T = Doc> {
27
27
  private columnDdl;
28
28
  /** Auto-additive migration: add declared columns missing from an existing table. */
29
29
  private migrateColumns;
30
+ private foreignKeysOn;
31
+ private declaredIndexDdl;
32
+ /**
33
+ * Reconcile the physical table to the declared schema, performing the changes
34
+ * the auto-additive path can't: **dropping** columns, **renaming** them, and
35
+ * **changing a column's type/constraints** — via a safe, transactional table
36
+ * rebuild that preserves data. Structured collections only.
37
+ *
38
+ * Pass `rename` to map an existing physical column to a new declared name, and
39
+ * `drop` to acknowledge columns that the new schema removes (an unacknowledged
40
+ * column drop throws, so data is never lost by accident).
41
+ *
42
+ * ```ts
43
+ * const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
44
+ * await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
45
+ * ```
46
+ */
47
+ $migrate(options?: MigrateOptions): Promise<void>;
30
48
  private rowToDoc;
31
49
  private encodeColumn;
32
50
  private insertColumns;
@@ -167,6 +185,13 @@ interface CollectionOptions {
167
185
  schema?: CollectionSchema;
168
186
  }
169
187
  type CollectionMode = "document" | "structured";
188
+ /** Options for {@link Collection.$migrate}. */
189
+ interface MigrateOptions {
190
+ /** Rename existing physical columns: `{ oldName: newDeclaredName }`. */
191
+ rename?: Record<string, string>;
192
+ /** Acknowledge physical columns to drop (not present in the new schema). */
193
+ drop?: string[];
194
+ }
170
195
  /** A column as reported by {@link Monlite.$schema}. */
171
196
  interface ColumnInfo {
172
197
  name: string;
@@ -357,12 +382,28 @@ interface GroupByArgs<T = Doc> {
357
382
  }
358
383
  type GroupByResult = Record<string, any>;
359
384
  type DriverName = "auto" | "better-sqlite3" | "node:sqlite";
385
+ /** Encryption-at-rest configuration (requires `better-sqlite3-multiple-ciphers`). */
386
+ interface EncryptionOptions {
387
+ /** The passphrase used to encrypt/decrypt the database file. */
388
+ key: string;
389
+ /**
390
+ * Cipher scheme to use (e.g. `"sqlcipher"`, `"chacha20"`, `"aes256cbc"`).
391
+ * Defaults to the library default (ChaCha20-Poly1305).
392
+ */
393
+ cipher?: string;
394
+ }
360
395
  interface MonliteOptions {
361
396
  /**
362
397
  * Which SQLite backend to use. `"auto"` (default) prefers `better-sqlite3`
363
398
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
364
399
  */
365
400
  driver?: DriverName;
401
+ /**
402
+ * Encrypt the database at rest. Requires the `better-sqlite3-multiple-ciphers`
403
+ * package (a drop-in for `better-sqlite3`); not supported on `node:sqlite`.
404
+ * Use `db.rekey(newKey)` to rotate the key.
405
+ */
406
+ encryption?: EncryptionOptions;
366
407
  /** Opt-in plugins (e.g. `@monlite/fts`). */
367
408
  plugins?: MonlitePlugin[];
368
409
  /**
@@ -414,6 +455,8 @@ interface Driver {
414
455
  /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
415
456
  transaction<T>(fn: () => T): T;
416
457
  close(): void;
458
+ /** Rotate the encryption key (encrypted backends only). */
459
+ rekey?(key: string, cipher?: string): void;
417
460
  /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
418
461
  readonly raw: any;
419
462
  }
@@ -613,6 +656,7 @@ declare class Monlite {
613
656
  readonly $sync?: SyncStore;
614
657
  private readonly collections;
615
658
  private readonly plugins;
659
+ private readonly encrypted;
616
660
  private closed;
617
661
  constructor(filename: string, options?: MonliteOptions);
618
662
  /** @internal Notify plugins that documents changed (post-commit). */
@@ -655,6 +699,11 @@ declare class Monlite {
655
699
  * `VACUUM INTO`). The destination file must not already exist.
656
700
  */
657
701
  backup(path: string): Promise<void>;
702
+ /**
703
+ * Rotate the encryption key. Only valid for a database opened with the
704
+ * `encryption` option; throws otherwise. Pass `cipher` to also change scheme.
705
+ */
706
+ rekey(key: string, cipher?: string): void;
658
707
  /** Close the underlying SQLite connection. */
659
708
  $disconnect(): Promise<void>;
660
709
  private assertOpen;
@@ -677,6 +726,12 @@ declare class MonliteQueryError extends MonliteError {
677
726
  cause?: unknown;
678
727
  });
679
728
  }
729
+ /** Thrown when an encrypted database can't be opened (wrong key, not encrypted). */
730
+ declare class MonliteEncryptionError extends MonliteError {
731
+ constructor(message: string, options?: {
732
+ cause?: unknown;
733
+ });
734
+ }
680
735
  /** A database constraint was violated (base class for the specific kinds). */
681
736
  declare class MonliteConstraintError extends MonliteError {
682
737
  readonly collection?: string;
@@ -717,4 +772,4 @@ declare function objectId(): string;
717
772
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
718
773
  declare function isObjectId(value: unknown): value is string;
719
774
 
720
- export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, Monlite, MonliteConstraintError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
775
+ export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
package/dist/index.js CHANGED
@@ -116,6 +116,12 @@ var MonliteQueryError = class extends MonliteError {
116
116
  this.name = "MonliteQueryError";
117
117
  }
118
118
  };
119
+ var MonliteEncryptionError = class extends MonliteError {
120
+ constructor(message, options) {
121
+ super(message, options);
122
+ this.name = "MonliteEncryptionError";
123
+ }
124
+ };
119
125
  var MonliteConstraintError = class extends MonliteError {
120
126
  collection;
121
127
  constructor(message, options) {
@@ -784,6 +790,124 @@ var Collection = class {
784
790
  }
785
791
  }
786
792
  }
793
+ foreignKeysOn() {
794
+ try {
795
+ const row = this.db.prepare(`PRAGMA foreign_keys`).get();
796
+ return !!row?.foreign_keys;
797
+ } catch {
798
+ return true;
799
+ }
800
+ }
801
+ declaredIndexDdl() {
802
+ const out = [];
803
+ for (const field of this.columnOrder) {
804
+ if (this.columnDefs[field].index) {
805
+ out.push(
806
+ `CREATE INDEX IF NOT EXISTS "idx_${this.name}_${field}" ON "${this.name}"("${field}")`
807
+ );
808
+ }
809
+ }
810
+ return out;
811
+ }
812
+ /**
813
+ * Reconcile the physical table to the declared schema, performing the changes
814
+ * the auto-additive path can't: **dropping** columns, **renaming** them, and
815
+ * **changing a column's type/constraints** — via a safe, transactional table
816
+ * rebuild that preserves data. Structured collections only.
817
+ *
818
+ * Pass `rename` to map an existing physical column to a new declared name, and
819
+ * `drop` to acknowledge columns that the new schema removes (an unacknowledged
820
+ * column drop throws, so data is never lost by accident).
821
+ *
822
+ * ```ts
823
+ * const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
824
+ * await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
825
+ * ```
826
+ */
827
+ async $migrate(options = {}) {
828
+ if (this.mode !== "structured") {
829
+ throw new MonliteError(
830
+ `$migrate() is only available on structured collections (declare a schema).`
831
+ );
832
+ }
833
+ this.ensureTable();
834
+ const rename = options.rename ?? {};
835
+ const drop = new Set(options.drop ?? []);
836
+ const SYSTEM = /* @__PURE__ */ new Set(["_id", "created_at", "updated_at", "data"]);
837
+ const physical = this.db.prepare(`PRAGMA table_info("${this.name}")`).all().map((r) => r.name).filter((n) => !SYSTEM.has(n));
838
+ const physicalSet = new Set(physical);
839
+ const targetSet = new Set(this.columnOrder);
840
+ const renamedFrom = {};
841
+ for (const [from, to] of Object.entries(rename)) {
842
+ if (!physicalSet.has(from)) {
843
+ throw new MonliteError(
844
+ `Cannot rename "${from}": no such column in "${this.name}".`
845
+ );
846
+ }
847
+ if (!targetSet.has(to)) {
848
+ throw new MonliteError(
849
+ `Cannot rename "${from}" to "${to}": "${to}" is not in the schema.`
850
+ );
851
+ }
852
+ renamedFrom[to] = from;
853
+ }
854
+ const renameSources = new Set(Object.keys(rename));
855
+ for (const col of physical) {
856
+ if (targetSet.has(col) || renameSources.has(col) || drop.has(col))
857
+ continue;
858
+ throw new MonliteError(
859
+ `Column "${col}" exists in "${this.name}" but isn't in the schema. Add it to the schema, or list it in \`drop\` to remove it.`
860
+ );
861
+ }
862
+ const tmp = `__mon_migrate_${this.name}`;
863
+ const newCols = [
864
+ `_id TEXT PRIMARY KEY`,
865
+ `created_at INTEGER NOT NULL`,
866
+ `updated_at INTEGER NOT NULL`,
867
+ `data TEXT NOT NULL DEFAULT '{}'`,
868
+ ...this.columnOrder.map((f) => this.columnDdl(f, false))
869
+ ];
870
+ const destCols = [
871
+ "_id",
872
+ "created_at",
873
+ "updated_at",
874
+ "data",
875
+ ...this.columnOrder.map((f) => `"${f}"`)
876
+ ].join(", ");
877
+ const srcCols = [
878
+ "_id",
879
+ "created_at",
880
+ "updated_at",
881
+ "data",
882
+ ...this.columnOrder.map((t) => {
883
+ const src = renamedFrom[t] ?? (physicalSet.has(t) ? t : null);
884
+ return src ? `"${src}"` : "NULL";
885
+ })
886
+ ].join(", ");
887
+ const fkOn = this.foreignKeysOn();
888
+ if (fkOn) this.db.exec(`PRAGMA foreign_keys = OFF`);
889
+ try {
890
+ this.guard(
891
+ () => this.db.transaction(() => {
892
+ this.db.exec(`DROP TABLE IF EXISTS "${tmp}"`);
893
+ this.db.exec(
894
+ `CREATE TABLE "${tmp}" (
895
+ ${newCols.join(",\n ")}
896
+ )`
897
+ );
898
+ this.db.exec(
899
+ `INSERT INTO "${tmp}" (${destCols}) SELECT ${srcCols} FROM "${this.name}"`
900
+ );
901
+ this.db.exec(`DROP TABLE "${this.name}"`);
902
+ this.db.exec(`ALTER TABLE "${tmp}" RENAME TO "${this.name}"`);
903
+ for (const ddl of this.declaredIndexDdl()) this.db.exec(ddl);
904
+ })
905
+ );
906
+ } finally {
907
+ if (fkOn) this.db.exec(`PRAGMA foreign_keys = ON`);
908
+ }
909
+ this.insertSqlCache = void 0;
910
+ }
787
911
  /* --------------------------- row <-> doc -------------------------- */
788
912
  rowToDoc(row) {
789
913
  const doc = this.mode === "document" ? JSON.parse(row.data) : JSON.parse(row.data ?? "{}");
@@ -1314,6 +1438,7 @@ var AutoIndexer = class {
1314
1438
 
1315
1439
  // src/driver/better-sqlite3.ts
1316
1440
  var STMT_CACHE_MAX = 256;
1441
+ var quote = (s) => s.replace(/'/g, "''");
1317
1442
  var BetterSqlite3Driver = class {
1318
1443
  name = "better-sqlite3";
1319
1444
  raw;
@@ -1324,6 +1449,9 @@ var BetterSqlite3Driver = class {
1324
1449
  this.raw = new BetterSqlite3(filename, {
1325
1450
  readonly: options.readonly ?? false
1326
1451
  });
1452
+ if (options.encryption) {
1453
+ this.applyKey(options.encryption.key, options.encryption.cipher);
1454
+ }
1327
1455
  this.raw.pragma("foreign_keys = ON");
1328
1456
  this.raw.pragma(`busy_timeout = ${options.busyTimeout ?? 5e3}`);
1329
1457
  if (!options.readonly && (options.wal ?? true)) {
@@ -1352,6 +1480,28 @@ var BetterSqlite3Driver = class {
1352
1480
  transaction(fn) {
1353
1481
  return this.raw.transaction(fn)();
1354
1482
  }
1483
+ /** Apply the encryption key and verify it by reading the schema. */
1484
+ applyKey(key, cipher) {
1485
+ if (cipher) this.raw.pragma(`cipher='${quote(cipher)}'`);
1486
+ this.raw.pragma(`key='${quote(key)}'`);
1487
+ try {
1488
+ this.raw.exec("SELECT count(*) FROM sqlite_master");
1489
+ } catch (err) {
1490
+ this.raw.close();
1491
+ throw new MonliteEncryptionError(
1492
+ "Failed to open the encrypted database: the key is incorrect, or the file is not encrypted.",
1493
+ { cause: err }
1494
+ );
1495
+ }
1496
+ }
1497
+ rekey(key, cipher) {
1498
+ if (cipher) this.raw.pragma(`cipher='${quote(cipher)}'`);
1499
+ const mode = String(this.raw.pragma("journal_mode", { simple: true }));
1500
+ const wasWal = mode.toLowerCase() === "wal";
1501
+ if (wasWal) this.raw.pragma("journal_mode = DELETE");
1502
+ this.raw.pragma(`rekey='${quote(key)}'`);
1503
+ if (wasWal) this.raw.pragma("journal_mode = WAL");
1504
+ }
1355
1505
  close() {
1356
1506
  this.cache.clear();
1357
1507
  this.raw.close();
@@ -1367,6 +1517,11 @@ var NodeSqliteDriver = class {
1367
1517
  cache = /* @__PURE__ */ new Map();
1368
1518
  depth = 0;
1369
1519
  constructor(nodeSqlite, filename, options) {
1520
+ if (options.encryption) {
1521
+ throw new MonliteError(
1522
+ "Encryption is not supported on the node:sqlite backend. Use better-sqlite3 with the better-sqlite3-multiple-ciphers package."
1523
+ );
1524
+ }
1370
1525
  this.verbose = options.verbose;
1371
1526
  const { DatabaseSync } = nodeSqlite;
1372
1527
  this.raw = new DatabaseSync(filename, {
@@ -1442,6 +1597,14 @@ function loadBetterSqlite3() {
1442
1597
  return null;
1443
1598
  }
1444
1599
  }
1600
+ function loadCipherSqlite3() {
1601
+ try {
1602
+ const mod = req("better-sqlite3-multiple-ciphers");
1603
+ return mod?.default ?? mod;
1604
+ } catch {
1605
+ return null;
1606
+ }
1607
+ }
1445
1608
  function loadNodeSqlite() {
1446
1609
  try {
1447
1610
  return req("node:sqlite");
@@ -1451,6 +1614,20 @@ function loadNodeSqlite() {
1451
1614
  }
1452
1615
  function createDriver(filename, options = {}) {
1453
1616
  const choice = options.driver ?? "auto";
1617
+ if (options.encryption) {
1618
+ if (choice === "node:sqlite") {
1619
+ throw new MonliteError(
1620
+ `Encryption is not supported on the node:sqlite backend. Use better-sqlite3 with the better-sqlite3-multiple-ciphers package.`
1621
+ );
1622
+ }
1623
+ const cipher = loadCipherSqlite3();
1624
+ if (!cipher) {
1625
+ throw new MonliteError(
1626
+ `Encryption requires the "better-sqlite3-multiple-ciphers" package (a drop-in for better-sqlite3). Run \`npm install better-sqlite3-multiple-ciphers\`.`
1627
+ );
1628
+ }
1629
+ return new BetterSqlite3Driver(cipher, filename, options);
1630
+ }
1454
1631
  if (choice === "better-sqlite3") {
1455
1632
  const mod = loadBetterSqlite3();
1456
1633
  if (!mod) {
@@ -1890,6 +2067,7 @@ var Monlite = class {
1890
2067
  $sync;
1891
2068
  collections = /* @__PURE__ */ new Map();
1892
2069
  plugins;
2070
+ encrypted;
1893
2071
  closed = false;
1894
2072
  constructor(filename, options = {}) {
1895
2073
  this.driver = createDriver(filename, {
@@ -1898,8 +2076,10 @@ var Monlite = class {
1898
2076
  wal: options.wal,
1899
2077
  busyTimeout: options.busyTimeout,
1900
2078
  allowExtensions: options.allowExtensions,
2079
+ encryption: options.encryption,
1901
2080
  verbose: options.verbose
1902
2081
  });
2082
+ this.encrypted = options.encryption !== void 0;
1903
2083
  this.autoIndexer = new AutoIndexer(
1904
2084
  this.driver,
1905
2085
  options.autoIndex ?? true,
@@ -2047,6 +2227,19 @@ var Monlite = class {
2047
2227
  this.driver.exec(`VACUUM INTO '${path.replace(/'/g, "''")}'`);
2048
2228
  return Promise.resolve();
2049
2229
  }
2230
+ /**
2231
+ * Rotate the encryption key. Only valid for a database opened with the
2232
+ * `encryption` option; throws otherwise. Pass `cipher` to also change scheme.
2233
+ */
2234
+ rekey(key, cipher) {
2235
+ this.assertOpen();
2236
+ if (!this.encrypted || !this.driver.rekey) {
2237
+ throw new MonliteError(
2238
+ "rekey() requires a database opened with the `encryption` option."
2239
+ );
2240
+ }
2241
+ this.driver.rekey(key, cipher);
2242
+ }
2050
2243
  /** Close the underlying SQLite connection. */
2051
2244
  $disconnect() {
2052
2245
  if (!this.closed) {
@@ -2063,6 +2256,6 @@ function createDb(filename, options) {
2063
2256
  return new Monlite(filename, options);
2064
2257
  }
2065
2258
 
2066
- export { Collection, Monlite, MonliteConstraintError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, MonliteQueryError, MonliteUniqueConstraintError, SyncStore, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
2259
+ export { Collection, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, MonliteQueryError, MonliteUniqueConstraintError, SyncStore, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
2067
2260
  //# sourceMappingURL=index.js.map
2068
2261
  //# sourceMappingURL=index.js.map