@delali/sirannon-db 0.3.6-next.77 → 0.3.6-next.79

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.
Files changed (73) hide show
  1. package/README.md +31 -3
  2. package/dist/backup/index.d.ts +5 -5
  3. package/dist/backup/index.mjs +5 -3
  4. package/dist/backup-scheduler/index.d.ts +2 -2
  5. package/dist/backup-scheduler/index.mjs +2 -2
  6. package/dist/chunk-22TYGH7J.mjs +55 -0
  7. package/dist/{chunk-SB3MIFLG.mjs → chunk-2ZQOZFX7.mjs} +29 -6
  8. package/dist/{chunk-UIDQGPJY.mjs → chunk-4O7WYZHN.mjs} +38 -3
  9. package/dist/{chunk-ZFFUVYYE.mjs → chunk-5DKWHT6K.mjs} +109 -1
  10. package/dist/chunk-5O4J6LQQ.mjs +35 -0
  11. package/dist/{chunk-CZL4D3BM.mjs → chunk-6CABU54O.mjs} +2 -2
  12. package/dist/{chunk-JAQ2I4OT.mjs → chunk-A5ULJVK7.mjs} +11 -11
  13. package/dist/{chunk-RLBKZZ24.mjs → chunk-AC7TKWNH.mjs} +1 -0
  14. package/dist/chunk-BKO4SCEC.mjs +99 -0
  15. package/dist/{chunk-KOTBP4AW.mjs → chunk-C7E4H6UN.mjs} +24 -4
  16. package/dist/{chunk-G4AG3ESO.mjs → chunk-FBA7P4ZA.mjs} +114 -149
  17. package/dist/{chunk-DKML5425.mjs → chunk-GBVB6WSC.mjs} +2 -2
  18. package/dist/chunk-I4JY5QPA.mjs +51 -0
  19. package/dist/{chunk-L6EX6K27.mjs → chunk-MO4APBYG.mjs} +54 -13
  20. package/dist/{chunk-RPWBYASV.mjs → chunk-MTOQ6QIS.mjs} +1 -1
  21. package/dist/chunk-MX77PGEN.mjs +60 -0
  22. package/dist/{chunk-BTUD6XQO.mjs → chunk-N3MHZV3B.mjs} +11 -29
  23. package/dist/{chunk-UHUDPQFI.mjs → chunk-OJXV4L3N.mjs} +1 -3
  24. package/dist/chunk-QF7WF7RJ.mjs +34 -0
  25. package/dist/chunk-RLBV4YTB.mjs +22 -0
  26. package/dist/{chunk-ISDT4RBD.mjs → chunk-ULUZUBON.mjs} +1 -1
  27. package/dist/{chunk-AP4367HK.mjs → chunk-VGRAAQX3.mjs} +4 -4
  28. package/dist/chunk-W74HFQFD.mjs +153 -0
  29. package/dist/chunk-Y4SP7CRF.mjs +9 -0
  30. package/dist/{chunk-JFVEGN7J.mjs → chunk-ZAUHAJ4G.mjs} +2 -1
  31. package/dist/{chunk-DDIHBUEN.mjs → chunk-ZWW53O5P.mjs} +19 -7
  32. package/dist/client/index.d.ts +8 -8
  33. package/dist/client/topology.d.ts +4 -4
  34. package/dist/{client-base-Doaqwrx3.d.ts → client-base-DJZbSY55.d.ts} +2 -2
  35. package/dist/{client-streams-MKPE4MXW.mjs → client-streams-F7IG3PTU.mjs} +13 -16
  36. package/dist/core/index.d.ts +48 -12
  37. package/dist/core/index.mjs +1812 -107
  38. package/dist/core/writer-worker.mjs +28 -4
  39. package/dist/{database-fz-8CJBP.d.ts → database-BZ96ZzAx.d.ts} +411 -184
  40. package/dist/{database-backup-BwTNiBm2.d.ts → database-backup-BhM8yJ1A.d.ts} +32 -2
  41. package/dist/driver/better-sqlite3.d.ts +11 -1
  42. package/dist/driver/better-sqlite3.mjs +212 -23
  43. package/dist/driver/bun.d.ts +1 -1
  44. package/dist/driver/bun.mjs +15 -22
  45. package/dist/driver/expo.d.ts +1 -1
  46. package/dist/driver/expo.mjs +185 -16
  47. package/dist/driver/node.d.ts +1 -1
  48. package/dist/driver/node.mjs +15 -62
  49. package/dist/driver/wa-sqlite.d.ts +1 -1
  50. package/dist/driver/wa-sqlite.mjs +14 -153
  51. package/dist/{change-tracker-Dpo99EI-.d.ts → errors-DLMAGQFb.d.ts} +200 -2
  52. package/dist/file-migrations/index.d.ts +1 -1
  53. package/dist/peer-refusal-RR37UOLV.mjs +1 -0
  54. package/dist/{protocol-DZSYSv0S.d.ts → protocol-DE10taas.d.ts} +1 -1
  55. package/dist/replication/coordinator/etcd.mjs +1 -1
  56. package/dist/replication/index.d.ts +7 -7
  57. package/dist/replication/index.mjs +66 -9
  58. package/dist/{restore-options-CcMukOLO.d.ts → restore-options-B6HV_5XT.d.ts} +6 -1
  59. package/dist/server/index.d.ts +10 -10
  60. package/dist/server/index.mjs +506 -350
  61. package/dist/{server-options-B9CsAZp5.d.ts → server-options-Bhdlvehe.d.ts} +14 -1
  62. package/dist/{server-streams-SBYCHWWM.mjs → server-streams-Q2AZAW3N.mjs} +6 -30
  63. package/dist/{sirannon-D0CjDULa.d.ts → sirannon-Ba1o2HUQ.d.ts} +5 -5
  64. package/dist/transport/grpc.d.ts +15 -4
  65. package/dist/transport/grpc.mjs +34 -37
  66. package/dist/transport/memory.d.ts +4 -3
  67. package/dist/transport/memory.mjs +8 -26
  68. package/dist/{types-DTmi7pXp.d.ts → types-BWyv_N18.d.ts} +189 -1
  69. package/dist/{types-BlZ9GG1a.d.ts → types-CDnlkVRK.d.ts} +8 -2
  70. package/package.json +17 -9
  71. package/dist/chunk-DBN57KVR.mjs +0 -171
  72. package/dist/chunk-W67A4H2E.mjs +0 -23
  73. package/dist/chunk-YW7IKQ7T.mjs +0 -62
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![types](https://img.shields.io/badge/types-TypeScript-blue)](https://www.npmjs.com/package/@delali/sirannon-db)
7
7
  [![license](https://img.shields.io/npm/l/@delali/sirannon-db)](https://github.com/assetcorp/sirannon-db/blob/main/LICENSE)
8
8
 
9
- Build a networked SQLite service with connection pooling, change data capture, live queries, migrations, backups, device sync, and a client SDK. Applications reach Sirannon over HTTP or WebSocket, while a primary replicates its changes to other Sirannon nodes over gRPC.
9
+ Build an encrypted, networked SQLite service with connection pooling, change data capture, live queries, migrations, backups, device sync, and a client SDK. Applications reach Sirannon over HTTP or WebSocket, while a primary replicates its changes to other Sirannon nodes over gRPC.
10
10
 
11
11
  **Read the full documentation at [sirannon.sondelali.com/docs](https://sirannon.sondelali.com/docs).** Use this page to get started, and turn to the [guides](#documentation) for the detail. The benchmark harness that measures Sirannon against Postgres 17 is under [`benchmarks/server`](https://github.com/assetcorp/sirannon-db/tree/main/benchmarks/server), where a Node load generator drives both engines and a Python step joins their results. The write-up generator rewrites [BENCHMARKS.md](https://github.com/assetcorp/sirannon-db/blob/main/BENCHMARKS.md) from the latest committed run.
12
12
 
@@ -61,7 +61,9 @@ import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
61
61
  const sirannon = new Sirannon({ driver: betterSqlite3() })
62
62
  const db = await sirannon.open('app', './data/app.db')
63
63
 
64
- await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
64
+ await db.execute(
65
+ 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT, active INTEGER NOT NULL DEFAULT 1)',
66
+ )
65
67
  await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
66
68
 
67
69
  const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
@@ -79,11 +81,36 @@ const db = await sirannon.open('app', '/app.db', { walMode: false })
79
81
 
80
82
  On React Native, open the database the same way with `expoSqlite()`.
81
83
 
84
+ ### Encrypt a database
85
+
86
+ On Node.js, install Sirannon's encrypting SQLite build beside the library:
87
+
88
+ ```bash
89
+ pnpm add -E @delali/sirannon-db @delali/sirannon-sqlite
90
+ ```
91
+
92
+ Create the better-sqlite3 driver with encryption enabled. Return 32 raw key bytes when Sirannon opens the database:
93
+
94
+ ```ts
95
+ import { readFile } from 'node:fs/promises'
96
+ import { Sirannon } from '@delali/sirannon-db'
97
+ import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
98
+
99
+ const loadLedgerKey = async () => new Uint8Array(await readFile('/run/secrets/ledger-key'))
100
+
101
+ const sirannon = new Sirannon({ driver: betterSqlite3({ encryption: true }) })
102
+ const db = await sirannon.open('ledger', './data/ledger.db', {
103
+ encryption: { key: async () => ({ key: await loadLedgerKey(), id: 'ledger-2026-11' }) },
104
+ })
105
+ ```
106
+
107
+ Store the key in a key service, a system keychain, or a secret file whose permissions restrict access to the application's operating-system account. Retain both keys until a key change finishes. Retain each backup chain's key for as long as you keep that chain. The [encryption guide](https://github.com/assetcorp/sirannon-db/blob/main/docs/encryption.md) covers key changes, encrypted backups, server routes, and Expo's SQLCipher setup.
108
+
82
109
  ## Package exports
83
110
 
84
111
  | Import | What you get |
85
112
  | --- | --- |
86
- | `@delali/sirannon-db` | Core library: queries, transactions, CDC, live queries, migrations, backups, hooks, metrics, and lifecycle |
113
+ | `@delali/sirannon-db` | Core library: queries, transactions, encryption, CDC, live queries, migrations, backups, hooks, metrics, and lifecycle |
87
114
  | `@delali/sirannon-db/driver/*` | SQLite driver adapters, listed in the table above |
88
115
  | `@delali/sirannon-db/file-migrations` | A loader for `.up.sql` and `.down.sql` files in a directory |
89
116
  | `@delali/sirannon-db/backup` | Backup destination types, chain records, and `restoreBackup` |
@@ -252,6 +279,7 @@ Read the [security guide](https://sirannon.sondelali.com/docs/security) for each
252
279
  | [Bulk load](https://sirannon.sondelali.com/docs/bulk-load) | One-transaction imports under relaxed durability, over the server, and through the client SDK |
253
280
  | [Migrations](https://sirannon.sondelali.com/docs/migrations) | File-based, programmatic, and bundled migrations, rollback, checksums, baselines, concurrency, and registry migrations |
254
281
  | [Hooks, metrics, and lifecycle](https://sirannon.sondelali.com/docs/hooks-metrics-and-lifecycle) | Before and after hooks, metrics callbacks, and the multi-tenant lifecycle |
282
+ | [Encryption](https://github.com/assetcorp/sirannon-db/blob/main/docs/encryption.md) | Encrypted Node and Expo databases, key changes, encrypted backups, and operator routes |
255
283
  | [Backups](https://sirannon.sondelali.com/docs/backups) | Copies of an open database to a file and on a schedule, followed by the guides on [destinations](https://sirannon.sondelali.com/docs/backup-destinations), [continuous backups](https://sirannon.sondelali.com/docs/backup-chains), and [restores](https://sirannon.sondelali.com/docs/backup-restore) |
256
284
  | [Server](https://sirannon.sondelali.com/docs/server) | Registered operations on the server, caller identity, size limits, write shapes, `acceptSql`, the backup routes, HTTP routes, and the WebSocket protocol |
257
285
  | [Registered operations](https://sirannon.sondelali.com/docs/registered-operations) | Named reads and writes, identity-filled arguments, calls over HTTP and WebSocket, capabilities, and refusals |
@@ -1,8 +1,8 @@
1
- import { B as BackupDestination, g as BackupRunReport } from '../types-DTmi7pXp.js';
2
- export { h as BackupChain, i as BackupChainBase, j as BackupChainChange, k as BackupChainPosition, l as BackupChainRecord, m as BackupCycle, n as BackupCycleError, o as BackupCycleOptions, p as BackupCycleStatus, q as BackupFileReport, r as BackupGroupMembership, s as BackupGroupSource, t as BackupLogPosition, u as BackupNodePreference, v as BackupPiece, w as BackupProgress, x as BackupSkip, y as BackupSkipReason, z as BackupToDestinationOptions, C as BackupVerifyResult, E as DEFAULT_CHAIN_NAME, F as chainLogName, G as readBackupChains, H as verifyBackupRecord } from '../types-DTmi7pXp.js';
3
- export { B as BackupCapabilities, a as BackupChainLocation, b as BackupRestorePlan, c as BackupSafeToDeleteOptions, d as backupPiecesSafeToDelete, p as planBackupRestore } from '../database-backup-BwTNiBm2.js';
4
- import { B as BackupRestoreOptions, a as BackupRestoreReport } from '../restore-options-CcMukOLO.js';
5
- export { b as BackupRestoreProgress } from '../restore-options-CcMukOLO.js';
1
+ import { B as BackupDestination, g as BackupRunReport } from '../types-BWyv_N18.js';
2
+ export { h as BackupChain, i as BackupChainBase, j as BackupChainChange, k as BackupChainPosition, l as BackupChainRecord, m as BackupCycle, n as BackupCycleError, o as BackupCycleOptions, p as BackupCycleStatus, q as BackupFileReport, r as BackupGroupMembership, s as BackupGroupSource, t as BackupLogPosition, u as BackupNodePreference, v as BackupPiece, w as BackupProgress, x as BackupSkip, y as BackupSkipReason, z as BackupToDestinationOptions, C as BackupVerifyResult, F as DEFAULT_CHAIN_NAME, G as chainLogName, H as readBackupChains, I as verifyBackupRecord } from '../types-BWyv_N18.js';
3
+ export { B as BackupCapabilities, a as BackupChainLocation, b as BackupRestorePlan, c as BackupSafeToDeleteOptions, d as backupPiecesSafeToDelete, p as planBackupRestore } from '../database-backup-BhM8yJ1A.js';
4
+ import { B as BackupRestoreOptions, a as BackupRestoreReport } from '../restore-options-B6HV_5XT.js';
5
+ export { b as BackupRestoreProgress } from '../restore-options-B6HV_5XT.js';
6
6
  import '../query-types-5B02ixH0.js';
7
7
 
8
8
  /** The byte and piece counts for one file that Sirannon assembles from a destination.
@@ -1,9 +1,11 @@
1
- export { assembleFromDestination, restoreBackup } from '../chunk-L6EX6K27.mjs';
2
- export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-YW7IKQ7T.mjs';
1
+ export { assembleFromDestination, restoreBackup } from '../chunk-MO4APBYG.mjs';
2
+ export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-W74HFQFD.mjs';
3
+ import '../chunk-5DKWHT6K.mjs';
3
4
  export { verifyBackupRecord } from '../chunk-DX4WGJL7.mjs';
4
5
  import '../chunk-QGVRRKB2.mjs';
5
6
  import '../chunk-BUWJH3D3.mjs';
6
- export { DEFAULT_CHAIN_NAME, chainLogName, readBackupChains } from '../chunk-CZL4D3BM.mjs';
7
+ export { DEFAULT_CHAIN_NAME, chainLogName, readBackupChains } from '../chunk-6CABU54O.mjs';
7
8
  import '../chunk-BGJ5JVYL.mjs';
8
9
  import '../chunk-HO2XTXVX.mjs';
10
+ import '../chunk-5O4J6LQQ.mjs';
9
11
  import '../chunk-KINDFBQP.mjs';
@@ -1,5 +1,5 @@
1
- import { J as SQLiteConnection, K as BackupFileCopy, L as BackupRunRequest, g as BackupRunReport, O as BackupScheduleRequest } from '../types-DTmi7pXp.js';
2
- export { q as BackupFileReport, P as BackupScheduleOptions, R as RunExclusive } from '../types-DTmi7pXp.js';
1
+ import { L as SQLiteConnection, O as BackupFileCopy, P as BackupRunRequest, g as BackupRunReport, Q as BackupScheduleRequest } from '../types-BWyv_N18.js';
2
+ export { q as BackupFileReport, R as BackupScheduleOptions, U as RunExclusive } from '../types-BWyv_N18.js';
3
3
  import '../query-types-5B02ixH0.js';
4
4
 
5
5
  /** What a runtime supplies so that Sirannon can send a copy to a destination without writing a local file.
@@ -1,5 +1,5 @@
1
- export { BackupScheduler } from '../chunk-BTUD6XQO.mjs';
2
- import '../chunk-W67A4H2E.mjs';
1
+ export { BackupScheduler } from '../chunk-N3MHZV3B.mjs';
2
+ import '../chunk-I4JY5QPA.mjs';
3
3
  import '../chunk-AHCMO4YL.mjs';
4
4
  import '../chunk-BUWJH3D3.mjs';
5
5
  import '../chunk-BGJ5JVYL.mjs';
@@ -0,0 +1,55 @@
1
+ // src/core/driver/statement-text.ts
2
+ var SPACE = 32;
3
+ var TAB = 9;
4
+ var LINE_FEED = 10;
5
+ var VERTICAL_TAB = 11;
6
+ var FORM_FEED = 12;
7
+ var CARRIAGE_RETURN = 13;
8
+ var SEMICOLON = 59;
9
+ var HYPHEN = 45;
10
+ var SLASH = 47;
11
+ var ASTERISK = 42;
12
+ var NO_STATEMENT_MESSAGE = "The supplied SQL string contains no statements";
13
+ function startsSqliteSpace(code) {
14
+ return code === SPACE || code === TAB || code === LINE_FEED || code === FORM_FEED || code === CARRIAGE_RETURN;
15
+ }
16
+ function continuesSqliteSpace(code) {
17
+ return startsSqliteSpace(code) || code === VERTICAL_TAB;
18
+ }
19
+ function holdsNoStatement(sql) {
20
+ const firstNul = sql.indexOf("\0");
21
+ const text = firstNul === -1 ? sql : sql.slice(0, firstNul);
22
+ let index = 0;
23
+ while (index < text.length) {
24
+ const code = text.charCodeAt(index);
25
+ if (startsSqliteSpace(code)) {
26
+ index++;
27
+ while (index < text.length && continuesSqliteSpace(text.charCodeAt(index))) index++;
28
+ continue;
29
+ }
30
+ if (code === SEMICOLON) {
31
+ index++;
32
+ continue;
33
+ }
34
+ const next = text.charCodeAt(index + 1);
35
+ if (code === HYPHEN && next === HYPHEN) {
36
+ const lineEnd = text.indexOf("\n", index + 2);
37
+ if (lineEnd === -1) return true;
38
+ index = lineEnd;
39
+ continue;
40
+ }
41
+ if (code === SLASH && next === ASTERISK && index + 2 < text.length) {
42
+ const commentEnd = text.indexOf("*/", index + 2);
43
+ if (commentEnd === -1) return true;
44
+ index = commentEnd + 2;
45
+ continue;
46
+ }
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ function assertHoldsStatement(sql) {
52
+ if (holdsNoStatement(sql)) throw new RangeError(NO_STATEMENT_MESSAGE);
53
+ }
54
+
55
+ export { assertHoldsStatement };
@@ -1,9 +1,9 @@
1
- import { selectOrdinaryTableName, deleteReplacedRowsOfTable, selectMinChangeSeqSql, selectMaxChangeSeq, deleteChangesBeforeUpToSeqSql, deleteAllReplacedRows, selectTableExists, ensureChangesTable, ensureReplacedRowsTable, ensureMetaTable, insertMetaValueIfAbsent, selectMetaValue, selectTriggerSql, selectChangesAfterSeqSql, selectTablesChangesInRangeSql, tableInfoRows, primaryKeyColumns, tableColumnsWithGenerated, tableIndexList, indexColumns, selectTableSql, TABLE_XINFO_HIDDEN, deleteExpiredDeviceCursors, selectMinDeviceCursorBoundary, SEQ_STRING_RE, selectIndexSql } from './chunk-RPWBYASV.mjs';
2
- import { CDC_TRIGGER_PREFIX, CHANGES_TABLE, isReservedIdentifier, META_TABLE, encodeTaggedValues, REPLACED_ROWS_TABLE, changedAtToEventTimestamp, decodeTaggedValues, DEVICE_CURSORS_TABLE, SAFE_INT_BOUND_TEXT } from './chunk-JFVEGN7J.mjs';
1
+ import { selectOrdinaryTableName, deleteReplacedRowsOfTable, selectMinChangeSeqSql, selectMaxChangeSeq, deleteChangesBeforeUpToSeqSql, deleteAllReplacedRows, selectTableExists, ensureChangesTable, ensureReplacedRowsTable, ensureMetaTable, insertMetaValueIfAbsent, selectMetaValue, selectTriggerSql, selectChangesAfterSeqSql, selectTablesChangesInRangeSql, tableInfoRows, primaryKeyColumns, tableColumnsWithGenerated, tableIndexList, indexColumns, selectTableSql, TABLE_XINFO_HIDDEN, deleteExpiredDeviceCursors, selectMinDeviceCursorBoundary, SEQ_STRING_RE, selectIndexSql } from './chunk-MTOQ6QIS.mjs';
2
+ import { CDC_TRIGGER_PREFIX, CHANGES_TABLE, isReservedIdentifier, META_TABLE, encodeTaggedValues, REPLACED_ROWS_TABLE, changedAtToEventTimestamp, decodeTaggedValues, DEVICE_CURSORS_TABLE, SAFE_INT_BOUND_TEXT } from './chunk-ZAUHAJ4G.mjs';
3
3
  import { tokenizeSql, isKeyword } from './chunk-SEL4A4WT.mjs';
4
- import { synchronousPragmaValue } from './chunk-YCMB6GR4.mjs';
5
4
  import { randomHex } from './chunk-BGJ5JVYL.mjs';
6
5
  import { invokeCallerCallback, reportCallerFailure, toError } from './chunk-HO2XTXVX.mjs';
6
+ import { synchronousPragmaValue } from './chunk-YCMB6GR4.mjs';
7
7
  import { SirannonError, ForbiddenSqlError, CDCError, MigrationError } from './chunk-KINDFBQP.mjs';
8
8
 
9
9
  // src/core/bulk-load.ts
@@ -666,6 +666,28 @@ function buildJsonObject(columns, ref) {
666
666
  function escStr(name) {
667
667
  return name.replace(/'/g, "''");
668
668
  }
669
+ async function installKeyChangeCaptureTriggers(conn, triggers, table, log, keys) {
670
+ const logOf = (row) => `INSERT INTO "${escId(log)}" VALUES (${keys.map((key) => `${row}.${key}`).join(", ")});`;
671
+ const on = `main."${escId(table)}"`;
672
+ await conn.exec(`CREATE TEMP TRIGGER "${escId(triggers.insert)}" AFTER INSERT ON ${on} BEGIN ${logOf("NEW")} END`);
673
+ await conn.exec(
674
+ `CREATE TEMP TRIGGER "${escId(triggers.update)}" AFTER UPDATE ON ${on} BEGIN ${logOf("OLD")} ${logOf("NEW")} END`
675
+ );
676
+ await conn.exec(`CREATE TEMP TRIGGER "${escId(triggers.delete)}" AFTER DELETE ON ${on} BEGIN ${logOf("OLD")} END`);
677
+ }
678
+ async function dropKeyChangeCaptureTriggers(conn, triggers) {
679
+ for (const name of [triggers.insert, triggers.update, triggers.delete]) {
680
+ await conn.exec(`DROP TRIGGER IF EXISTS temp."${escId(name)}"`);
681
+ }
682
+ }
683
+ async function dropMainSchemaTriggers(conn) {
684
+ const triggers = await (await conn.prepare("SELECT name, sql FROM main.sqlite_schema WHERE type = 'trigger' AND sql IS NOT NULL")).all();
685
+ for (const trigger of triggers) await conn.exec(`DROP TRIGGER main."${escId(trigger.name)}"`);
686
+ return triggers.map((trigger) => trigger.sql);
687
+ }
688
+ async function createMainSchemaTriggers(conn, statements) {
689
+ for (const statement of statements) await conn.exec(statement);
690
+ }
669
691
 
670
692
  // src/core/cdc/change-tracker.ts
671
693
  var DEFAULT_RETENTION_MS = 36e5;
@@ -1155,7 +1177,7 @@ var SubscriptionBuilderImpl = class {
1155
1177
  return this.manager.subscribe(this.table, this.conditions, callback, options);
1156
1178
  }
1157
1179
  };
1158
- function startPolling(conn, tracker, manager, intervalMs, onError, runExclusive, deviceRetention = {
1180
+ function startPolling(connection, tracker, manager, intervalMs, onError, runExclusive, deviceRetention = {
1159
1181
  cursorRetentionMs: DEFAULT_DEVICE_CURSOR_RETENTION_MS,
1160
1182
  maxChangesHeldForDevice: 0
1161
1183
  }) {
@@ -1170,7 +1192,7 @@ function startPolling(conn, tracker, manager, intervalMs, onError, runExclusive,
1170
1192
  if (polling) return;
1171
1193
  polling = true;
1172
1194
  try {
1173
- const events = await exclusive(() => tracker.poll(conn));
1195
+ const events = await exclusive(() => tracker.poll(connection()));
1174
1196
  if (events.length > 0) {
1175
1197
  manager.dispatch(events);
1176
1198
  }
@@ -1180,6 +1202,7 @@ function startPolling(conn, tracker, manager, intervalMs, onError, runExclusive,
1180
1202
  if (tickCount >= CLEANUP_INTERVAL_TICKS) {
1181
1203
  tickCount = 0;
1182
1204
  await exclusive(async () => {
1205
+ const conn = connection();
1183
1206
  await applyDeviceCursorBoundary(conn, tracker, deviceRetention);
1184
1207
  await tracker.cleanup(conn);
1185
1208
  });
@@ -1371,4 +1394,4 @@ var TransactionGrouper = class {
1371
1394
  }
1372
1395
  };
1373
1396
 
1374
- export { ChangeTracker, DEVICE_SYNC_PUSHED_SEQ_META_KEY, PrimedSubscription, ROWID_ALIASES, SubscriptionBuilderImpl, SubscriptionManager, TransactionGrouper, applyDeviceCursorBoundary, dropCdcTriggers, ensureCdcEpoch, filteredChange, isBulkLoadDurability, migrationChecksum, migrationContentChecksum, needsResync, parseColumnDefinitions, reconcileMigrationChecksums, resolveDeviceRetentionPolicy, runBulkLoad, startPolling };
1397
+ export { ChangeTracker, DEVICE_SYNC_PUSHED_SEQ_META_KEY, PrimedSubscription, ROWID_ALIASES, SubscriptionBuilderImpl, SubscriptionManager, TransactionGrouper, applyDeviceCursorBoundary, createMainSchemaTriggers, dropCdcTriggers, dropKeyChangeCaptureTriggers, dropMainSchemaTriggers, ensureCdcEpoch, escId, filteredChange, installKeyChangeCaptureTriggers, isBulkLoadDurability, migrationChecksum, migrationContentChecksum, needsResync, parseColumnDefinitions, reconcileMigrationChecksums, resolveDeviceRetentionPolicy, runBulkLoad, startPolling };
@@ -883,7 +883,7 @@ var AckPayload = {
883
883
  }
884
884
  };
885
885
  function createBaseHello() {
886
- return { nodeId: "", role: "", groupId: "", primaryTerm: 0n, protocolVersion: "" };
886
+ return { nodeId: "", role: "", groupId: "", primaryTerm: 0n, protocolVersion: "", encrypted: false };
887
887
  }
888
888
  var Hello = {
889
889
  encode(message, writer = new BinaryWriter()) {
@@ -905,6 +905,9 @@ var Hello = {
905
905
  if (message.protocolVersion !== "") {
906
906
  writer.uint32(42).string(message.protocolVersion);
907
907
  }
908
+ if (message.encrypted !== false) {
909
+ writer.uint32(48).bool(message.encrypted);
910
+ }
908
911
  return writer;
909
912
  },
910
913
  decode(input, length) {
@@ -949,6 +952,13 @@ var Hello = {
949
952
  message.protocolVersion = reader.string();
950
953
  continue;
951
954
  }
955
+ case 6: {
956
+ if (tag !== 48) {
957
+ break;
958
+ }
959
+ message.encrypted = reader.bool();
960
+ continue;
961
+ }
952
962
  }
953
963
  if ((tag & 7) === 4 || tag === 0) {
954
964
  break;
@@ -963,7 +973,8 @@ var Hello = {
963
973
  role: isSet(object.role) ? globalThis.String(object.role) : "",
964
974
  groupId: isSet(object.groupId) ? globalThis.String(object.groupId) : isSet(object.group_id) ? globalThis.String(object.group_id) : "",
965
975
  primaryTerm: isSet(object.primaryTerm) ? BigInt(object.primaryTerm) : isSet(object.primary_term) ? BigInt(object.primary_term) : 0n,
966
- protocolVersion: isSet(object.protocolVersion) ? globalThis.String(object.protocolVersion) : isSet(object.protocol_version) ? globalThis.String(object.protocol_version) : ""
976
+ protocolVersion: isSet(object.protocolVersion) ? globalThis.String(object.protocolVersion) : isSet(object.protocol_version) ? globalThis.String(object.protocol_version) : "",
977
+ encrypted: isSet(object.encrypted) ? globalThis.Boolean(object.encrypted) : false
967
978
  };
968
979
  },
969
980
  toJSON(message) {
@@ -983,6 +994,9 @@ var Hello = {
983
994
  if (message.protocolVersion !== "") {
984
995
  obj.protocolVersion = message.protocolVersion;
985
996
  }
997
+ if (message.encrypted !== false) {
998
+ obj.encrypted = message.encrypted;
999
+ }
986
1000
  return obj;
987
1001
  },
988
1002
  create(base) {
@@ -995,6 +1009,7 @@ var Hello = {
995
1009
  message.groupId = object.groupId ?? "";
996
1010
  message.primaryTerm = object.primaryTerm ?? 0n;
997
1011
  message.protocolVersion = object.protocolVersion ?? "";
1012
+ message.encrypted = object.encrypted ?? false;
998
1013
  return message;
999
1014
  }
1000
1015
  };
@@ -2639,4 +2654,24 @@ function buildChannelCreds(options) {
2639
2654
  return credentials.createSsl(ca, key, cert);
2640
2655
  }
2641
2656
 
2642
- export { ReplicationClient, ReplicationService, buildChannelCreds, buildServerCreds };
2657
+ // src/transport/grpc/hello.ts
2658
+ function localHello(t) {
2659
+ return {
2660
+ nodeId: t.localNodeId,
2661
+ role: t.localRole,
2662
+ groupId: t.localGroupId ?? "",
2663
+ primaryTerm: t.localPrimaryTerm ?? 0n,
2664
+ protocolVersion: t.localProtocolVersion ?? "",
2665
+ encrypted: t.localEncrypted
2666
+ };
2667
+ }
2668
+ function peerHelloFields(hello) {
2669
+ return {
2670
+ groupId: hello.groupId || void 0,
2671
+ primaryTerm: hello.primaryTerm === 0n ? void 0 : hello.primaryTerm,
2672
+ protocolVersion: hello.protocolVersion || void 0,
2673
+ encrypted: hello.encrypted === true
2674
+ };
2675
+ }
2676
+
2677
+ export { ReplicationClient, ReplicationService, buildChannelCreds, buildServerCreds, localHello, peerHelloFields };
@@ -94,6 +94,54 @@ function getDotPath(issue) {
94
94
  return null;
95
95
  }
96
96
  // @__NO_SIDE_EFFECTS__
97
+ function check(requirement, message$1) {
98
+ return {
99
+ kind: "validation",
100
+ type: "check",
101
+ reference: check,
102
+ async: false,
103
+ expects: null,
104
+ requirement,
105
+ message: message$1,
106
+ "~run"(dataset, config$1) {
107
+ if (dataset.typed && !this.requirement(dataset.value)) _addIssue(this, "input", dataset, config$1);
108
+ return dataset;
109
+ }
110
+ };
111
+ }
112
+ // @__NO_SIDE_EFFECTS__
113
+ function maxLength(requirement, message$1) {
114
+ return {
115
+ kind: "validation",
116
+ type: "max_length",
117
+ reference: maxLength,
118
+ async: false,
119
+ expects: `<=${requirement}`,
120
+ requirement,
121
+ message: message$1,
122
+ "~run"(dataset, config$1) {
123
+ if (dataset.typed && dataset.value.length > this.requirement) _addIssue(this, "length", dataset, config$1, { received: `${dataset.value.length}` });
124
+ return dataset;
125
+ }
126
+ };
127
+ }
128
+ // @__NO_SIDE_EFFECTS__
129
+ function minLength(requirement, message$1) {
130
+ return {
131
+ kind: "validation",
132
+ type: "min_length",
133
+ reference: minLength,
134
+ async: false,
135
+ expects: `>=${requirement}`,
136
+ requirement,
137
+ message: message$1,
138
+ "~run"(dataset, config$1) {
139
+ if (dataset.typed && dataset.value.length < this.requirement) _addIssue(this, "length", dataset, config$1, { received: `${dataset.value.length}` });
140
+ return dataset;
141
+ }
142
+ };
143
+ }
144
+ // @__NO_SIDE_EFFECTS__
97
145
  function regex(requirement, message$1) {
98
146
  return {
99
147
  kind: "validation",
@@ -163,6 +211,23 @@ function array(item, message$1) {
163
211
  });
164
212
  }
165
213
  // @__NO_SIDE_EFFECTS__
214
+ function instance(class_, message$1) {
215
+ return _standardSchema({
216
+ kind: "schema",
217
+ type: "instance",
218
+ reference: instance,
219
+ expects: class_.name,
220
+ async: false,
221
+ class: class_,
222
+ message: message$1,
223
+ "~run"(dataset, config$1) {
224
+ if (dataset.value instanceof this.class) dataset.typed = true;
225
+ else _addIssue(this, "type", dataset, config$1);
226
+ return dataset;
227
+ }
228
+ });
229
+ }
230
+ // @__NO_SIDE_EFFECTS__
166
231
  function nullable(wrapped, default_) {
167
232
  return _standardSchema({
168
233
  kind: "schema",
@@ -383,6 +448,49 @@ function string(message$1) {
383
448
  });
384
449
  }
385
450
  // @__NO_SIDE_EFFECTS__
451
+ function _subIssues(datasets) {
452
+ let issues;
453
+ if (datasets) for (const dataset of datasets) if (issues) for (const issue of dataset.issues) issues.push(issue);
454
+ else issues = dataset.issues;
455
+ return issues;
456
+ }
457
+ // @__NO_SIDE_EFFECTS__
458
+ function union(options, message$1) {
459
+ return _standardSchema({
460
+ kind: "schema",
461
+ type: "union",
462
+ reference: union,
463
+ expects: /* @__PURE__ */ _joinExpects(options.map((option) => option.expects), "|"),
464
+ async: false,
465
+ options,
466
+ message: message$1,
467
+ "~run"(dataset, config$1) {
468
+ let validDataset;
469
+ let typedDatasets;
470
+ let untypedDatasets;
471
+ for (const schema of this.options) {
472
+ const optionDataset = schema["~run"]({ value: dataset.value }, config$1);
473
+ if (optionDataset.typed) if (optionDataset.issues) if (typedDatasets) typedDatasets.push(optionDataset);
474
+ else typedDatasets = [optionDataset];
475
+ else {
476
+ validDataset = optionDataset;
477
+ break;
478
+ }
479
+ else if (untypedDatasets) untypedDatasets.push(optionDataset);
480
+ else untypedDatasets = [optionDataset];
481
+ }
482
+ if (validDataset) return validDataset;
483
+ if (typedDatasets) {
484
+ if (typedDatasets.length === 1) return typedDatasets[0];
485
+ _addIssue(this, "type", dataset, config$1, { issues: /* @__PURE__ */ _subIssues(typedDatasets) });
486
+ dataset.typed = true;
487
+ } else if (untypedDatasets?.length === 1) return untypedDatasets[0];
488
+ else _addIssue(this, "type", dataset, config$1, { issues: /* @__PURE__ */ _subIssues(untypedDatasets) });
489
+ return dataset;
490
+ }
491
+ });
492
+ }
493
+ // @__NO_SIDE_EFFECTS__
386
494
  function unknown() {
387
495
  return _standardSchema({
388
496
  kind: "schema",
@@ -436,4 +544,4 @@ function summarize(issues) {
436
544
  return summary;
437
545
  }
438
546
 
439
- export { array, getDotPath, nullable, number, object, optional, picklist, pipe, record, regex, safeParse, string, summarize, unknown };
547
+ export { array, check, getDotPath, instance, maxLength, minLength, nullable, number, object, optional, picklist, pipe, record, regex, safeParse, string, summarize, union, unknown };
@@ -0,0 +1,35 @@
1
+ import { SirannonError } from './chunk-KINDFBQP.mjs';
2
+
3
+ // src/core/encryption/errors.ts
4
+ var EncryptionError = class extends SirannonError {
5
+ /**
6
+ * Returns an error with an encryption code, and the failure that caused it when there is one.
7
+ *
8
+ * @param message - What failed, written without the key.
9
+ * @param code - The machine-readable code.
10
+ * @param cause - The failure that caused this error, such as the error that a key function threw.
11
+ */
12
+ constructor(message, code, cause) {
13
+ super(message, code);
14
+ this.name = "EncryptionError";
15
+ if (cause !== void 0) {
16
+ Object.defineProperty(this, "cause", { value: cause, enumerable: false, writable: true, configurable: true });
17
+ }
18
+ }
19
+ };
20
+ function keyRejected(path, cause) {
21
+ return new EncryptionError(
22
+ `The key failed the authentication check of the first page of the database ${path}. Either the key is wrong, or the file comes from another encryption scheme: the better-sqlite3 driver encrypts with AEGIS-256 and the Expo driver encrypts with SQLCipher 4, and neither opens the other's files`,
23
+ "ENCRYPTION_KEY_REJECTED",
24
+ cause
25
+ );
26
+ }
27
+ function refuseEncryptedOpen(driverName, options) {
28
+ if (options?.encryption === void 0) return;
29
+ throw new EncryptionError(
30
+ `The ${driverName} driver opens SQLite through a library that Sirannon cannot replace with an encrypting build, so it cannot open an encrypted database`,
31
+ "ENCRYPTION_UNSUPPORTED"
32
+ );
33
+ }
34
+
35
+ export { EncryptionError, keyRejected, refuseEncryptedOpen };
@@ -20,11 +20,11 @@ function isBackupChainHead(value) {
20
20
  }
21
21
  function isBackupChainBase(value) {
22
22
  const base = value;
23
- return base?.kind === "full" && typeof base.chainId === "string" && typeof base.name === "string" && typeof base.runId === "string" && isWholeNumber(base.finishedAt) && isWholeNumber(base.pieceCount) && isAtLeastOne(base.pieceBytes) && isWholeNumber(base.bytesWritten) && isTextOrAbsent(base.fingerprint);
23
+ return base?.kind === "full" && typeof base.chainId === "string" && typeof base.name === "string" && typeof base.runId === "string" && isWholeNumber(base.finishedAt) && isWholeNumber(base.pieceCount) && isAtLeastOne(base.pieceBytes) && isWholeNumber(base.bytesWritten) && isTextOrAbsent(base.fingerprint) && isTextOrAbsent(base.keyId);
24
24
  }
25
25
  function isBackupChainChange(value) {
26
26
  const change = value;
27
- return change?.kind === "change" && typeof change.chainId === "string" && typeof change.name === "string" && typeof change.runId === "string" && isAtLeastOne(change.sequence) && isBackupChainPosition(change.position) && isWholeNumber(change.capturedAt) && isAtLeastOne(change.frameCount) && isWholeNumber(change.pieceCount) && isAtLeastOne(change.pieceBytes) && isWholeNumber(change.bytesWritten) && typeof change.checkpointed === "boolean" && isTextOrAbsent(change.fingerprint);
27
+ return change?.kind === "change" && typeof change.chainId === "string" && typeof change.name === "string" && typeof change.runId === "string" && isAtLeastOne(change.sequence) && isBackupChainPosition(change.position) && isWholeNumber(change.capturedAt) && isAtLeastOne(change.frameCount) && isWholeNumber(change.pieceCount) && isAtLeastOne(change.pieceBytes) && isWholeNumber(change.bytesWritten) && typeof change.checkpointed === "boolean" && isTextOrAbsent(change.fingerprint) && isTextOrAbsent(change.keyId);
28
28
  }
29
29
 
30
30
  // src/core/backup/chain.ts
@@ -1,6 +1,13 @@
1
1
  import { SirannonError } from './chunk-KINDFBQP.mjs';
2
2
 
3
3
  // src/core/driver/define.ts
4
+ function boundFileSystem(files) {
5
+ return Object.freeze({
6
+ readStart: files.readStart.bind(files),
7
+ remove: files.remove.bind(files),
8
+ rename: files.rename.bind(files)
9
+ });
10
+ }
4
11
  function defineDriver(config) {
5
12
  if (!config.capabilities || typeof config.open !== "function") {
6
13
  throw new SirannonError("Driver must define capabilities and open()", "INVALID_DRIVER");
@@ -9,22 +16,15 @@ function defineDriver(config) {
9
16
  throw new SirannonError("Driver worker entry must define a string specifier", "INVALID_DRIVER");
10
17
  }
11
18
  return Object.freeze({
12
- capabilities: Object.freeze({ ...config.capabilities }),
19
+ capabilities: Object.freeze({ ...config.capabilities, encryption: config.capabilities.encryption === true }),
13
20
  open: config.open.bind(config),
14
21
  ...config.worker ? { worker: Object.freeze({ ...config.worker }) } : {},
15
22
  ...config.startWriterHost ? { startWriterHost: config.startWriterHost.bind(config) } : {},
16
23
  ...config.createWriterContext ? { createWriterContext: config.createWriterContext.bind(config) } : {},
17
24
  ...config.createBackupEngine ? { createBackupEngine: config.createBackupEngine.bind(config) } : {},
18
- ...config.resolveExtensionPath ? { resolveExtensionPath: config.resolveExtensionPath.bind(config) } : {}
25
+ ...config.resolveExtensionPath ? { resolveExtensionPath: config.resolveExtensionPath.bind(config) } : {},
26
+ ...config.files ? { files: boundFileSystem(config.files) } : {}
19
27
  });
20
28
  }
21
29
 
22
- // src/core/backup/cycle-options.ts
23
- var DEFAULT_CAPTURE_INTERVAL_MS = 6e4;
24
- var DEFAULT_FULL_COPY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
25
- var DEFAULT_BACKUP_NAME_PREFIX = "sirannon-backup";
26
- function defaultStagingDir(sourcePath) {
27
- return `${sourcePath}-backup`;
28
- }
29
-
30
- export { DEFAULT_BACKUP_NAME_PREFIX, DEFAULT_CAPTURE_INTERVAL_MS, DEFAULT_FULL_COPY_INTERVAL_MS, defaultStagingDir, defineDriver };
30
+ export { defineDriver };
@@ -19,6 +19,7 @@ function registerPeer(connectedPeers, peerConnectedHandler, nodeId, role, metada
19
19
  role,
20
20
  primaryTerm: metadata.primaryTerm,
21
21
  protocolVersion: metadata.protocolVersion,
22
+ encrypted: metadata.encrypted === true,
22
23
  joinedAt: Date.now(),
23
24
  lastSeenAt: Date.now(),
24
25
  lastAckedSeq: 0n