quilltap 4.12.0-dev.9 → 4.12.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
@@ -65,7 +65,7 @@ quilltap --update
65
65
 
66
66
  ## How It Works
67
67
 
68
- The `quilltap` npm package is a lightweight CLI launcher (~10 KB). On first run, it downloads the pre-built application from [GitHub Releases](https://github.com/foundry-9/quilltap-server/releases) and caches it in a platform-specific directory. Native modules (`better-sqlite3`, `sharp`) ship as prebuilt binaries for your platform — nothing is compiled on install. On Linux, the SQLCipher driver needs glibc 2.35 or newer (Ubuntu 22.04, Debian 12, RHEL 9 or newer); older hosts should run the Docker image.
68
+ The `quilltap` npm package is a lightweight CLI launcher (~10 KB). On first run, it downloads the pre-built application from [GitHub Releases](https://github.com/foundry-9/quilltap-server/releases) and caches it in a platform-specific directory. The SQLCipher database driver (`better-sqlite3`) and `sharp` ship as prebuilt binaries for your platform — the driver is never compiled or rebuilt. `node-pty` (terminals) uses its prebuilds where they exist and compiles on Linux; if a Node.js upgrade leaves `sharp` or `node-pty` built for the old version, the next run rebuilds them automatically. On Linux, the SQLCipher driver needs glibc 2.35 or newer (Ubuntu 22.04, Debian 12, RHEL 9 or newer); older hosts should run the Docker image.
69
69
 
70
70
  ### Cache Locations
71
71
 
@@ -112,7 +112,7 @@ Every subcommand then accepts `--instance <name>` in place of `--data-dir`. The
112
112
 
113
113
  ## Database Tool
114
114
 
115
- The encrypted SQLite databases (main, LLM logs, mount index) can be queried directly via `quilltap db`. They are SQLCipher-encrypted, so the stock `sqlite3` binary **cannot** open them — this is the way in. There are two modes: high-level subcommands that auto-pick the right database and resolve characters/chats/projects by name, and a low-level path for arbitrary SQL.
115
+ The encrypted SQLite databases (main, LLM logs, mount index) can be queried directly via `quilltap db`. They are encrypted with ChaCha20-Poly1305 (SQLite3 Multiple Ciphers), so neither the stock `sqlite3` binary nor `sqlcipher` can open them — this is the way in. There are two modes: high-level subcommands that auto-pick the right database and resolve characters/chats/projects by name, and a low-level path for arbitrary SQL.
116
116
 
117
117
  ### Subcommands
118
118
 
@@ -170,7 +170,7 @@ quilltap db backup # online snapshot of all three DBs
170
170
  quilltap db backup main --out /tmp/snap # one DB to a chosen directory
171
171
  quilltap db backup --json # parseable per-target sizes + durations
172
172
 
173
- quilltap db integrity # cipher_integrity_check + integrity_check
173
+ quilltap db integrity # integrity_check (decrypts + verifies every page it reads)
174
174
  quilltap db integrity llm-logs # one DB; exit 0 ok, 1 issues, 2 open failure
175
175
  ```
176
176
 
package/bin/quilltap.js CHANGED
@@ -803,9 +803,10 @@ Subcommands (high-level shortcuts; auto-pick the right database):
803
803
  Default destination: <dataDir>/backups/<timestamp>/.
804
804
  Each snapshot is re-opened with the same key and
805
805
  verified with PRAGMA quick_check.
806
- integrity [target...] Run PRAGMA cipher_integrity_check and
807
- PRAGMA integrity_check on the encrypted
808
- databases. Read-only; safe alongside a live
806
+ integrity [target...] Run PRAGMA integrity_check on the encrypted
807
+ databases; every page read is decrypted and
808
+ its Poly1305 tag verified, so tampered pages
809
+ count as issues. Read-only; safe alongside a live
809
810
  instance. Exit code: 0 ok, 1 issues, 2 open
810
811
  failure.
811
812
  Most subcommands also accept --json and --limit N.
@@ -851,7 +852,7 @@ Examples:
851
852
  quilltap db optimize llm-logs # only the LLM logs DB
852
853
  quilltap db backup # snapshot all DBs to <dataDir>/backups/<ts>/
853
854
  quilltap db backup main --out /tmp/qtap-snap # snapshot only the main DB
854
- quilltap db integrity # cipher_integrity_check + integrity_check
855
+ quilltap db integrity # integrity_check on every DB
855
856
  quilltap db --tables
856
857
  quilltap db "SELECT count(*) FROM characters"
857
858
  quilltap db --count messages
@@ -1604,7 +1604,7 @@ async function backupOneDb(key, sourcePath, destDir, ctx) {
1604
1604
  return { target: target.label, source: sourcePath, sourceSize, dest: destPath, ok: false, error: err.message };
1605
1605
  }
1606
1606
 
1607
- // The SQLCipher build in this driver does not expose `sqlcipher_export`
1607
+ // The SQLite3 Multiple Ciphers build in this driver does not expose `sqlcipher_export`
1608
1608
  // and the SQLite online-backup API refuses cross-cipher copies. We
1609
1609
  // instead take a brief exclusive lock, force a WAL checkpoint, and copy
1610
1610
  // the encrypted .db file at the byte level — the pages are already
@@ -1742,44 +1742,30 @@ function integrityOneDb(key, dbPath, ctx) {
1742
1742
  return { target: target.label, ok: false, openable: false, issues: [], reason: err.message };
1743
1743
  }
1744
1744
 
1745
+ // SQLite3 Multiple Ciphers (ChaCha20-Poly1305) has no SQLCipher-style
1746
+ // `cipher_integrity_check` pragma — an unknown pragma returns no rows, so
1747
+ // asking for it reported "ok" without checking anything. The page-level
1748
+ // check is integrity_check itself: every page it reads must decrypt and
1749
+ // pass its Poly1305 tag, and a page that fails throws (SQLITE_CORRUPT /
1750
+ // SQLITE_NOTADB) rather than returning a row, so that is caught as an issue.
1745
1751
  const issues = [];
1746
- let cipherCheck = null;
1747
1752
  let integrityCheck = null;
1748
1753
  const t0 = Date.now();
1749
1754
  try {
1755
+ let lines;
1750
1756
  try {
1751
- const rows = db.pragma('cipher_integrity_check');
1752
- if (rows && rows.length === 0) {
1753
- cipherCheck = 'ok';
1754
- console.log(' cipher_integrity_check: ok');
1755
- } else {
1756
- const lines = rows.map(r => (r.cipher_integrity_check || Object.values(r)[0] || '')).filter(Boolean);
1757
- if (lines.length === 1 && lines[0] === 'ok') {
1758
- cipherCheck = 'ok';
1759
- console.log(' cipher_integrity_check: ok');
1760
- } else {
1761
- cipherCheck = lines.join('; ');
1762
- for (const line of lines) {
1763
- console.log(` cipher_integrity_check: ${line}`);
1764
- issues.push({ pragma: 'cipher_integrity_check', message: line });
1765
- }
1766
- }
1767
- }
1757
+ const integRows = db.pragma('integrity_check');
1758
+ lines = integRows.map(r => (r.integrity_check || Object.values(r)[0] || '')).filter(Boolean);
1768
1759
  } catch (err) {
1769
- // Plain SQLite has no cipher_integrity_check; treat as N/A and continue.
1770
- cipherCheck = `n/a (${err.message})`;
1771
- console.log(` cipher_integrity_check: n/a (${err.message})`);
1760
+ lines = [`page read failed (${err.code || 'error'}): ${err.message}`];
1772
1761
  }
1773
-
1774
- const integRows = db.pragma('integrity_check');
1775
- const lines = integRows.map(r => (r.integrity_check || Object.values(r)[0] || '')).filter(Boolean);
1776
1762
  if (lines.length === 1 && lines[0] === 'ok') {
1777
1763
  integrityCheck = 'ok';
1778
- console.log(' integrity_check: ok');
1764
+ console.log(' integrity_check: ok');
1779
1765
  } else {
1780
1766
  integrityCheck = lines.join('; ');
1781
1767
  for (const line of lines) {
1782
- console.log(` integrity_check: ${line}`);
1768
+ console.log(` integrity_check: ${line}`);
1783
1769
  issues.push({ pragma: 'integrity_check', message: line });
1784
1770
  }
1785
1771
  }
@@ -1789,13 +1775,10 @@ function integrityOneDb(key, dbPath, ctx) {
1789
1775
  const durationMs = Date.now() - t0;
1790
1776
  console.log(` duration: ${formatDuration(durationMs)}`);
1791
1777
 
1792
- const ok = (cipherCheck === 'ok' || cipherCheck === null || (typeof cipherCheck === 'string' && cipherCheck.startsWith('n/a')))
1793
- && integrityCheck === 'ok';
1794
1778
  return {
1795
1779
  target: target.label,
1796
- ok,
1780
+ ok: integrityCheck === 'ok',
1797
1781
  openable: true,
1798
- cipherIntegrityCheck: cipherCheck,
1799
1782
  integrityCheck,
1800
1783
  issues,
1801
1784
  durationMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.12.0-dev.9",
3
+ "version": "4.12.0",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",
@@ -36,7 +36,7 @@
36
36
  "README.md"
37
37
  ],
38
38
  "dependencies": {
39
- "@napi-rs/canvas": "^1.0.13",
39
+ "@napi-rs/canvas": "^1.1.0",
40
40
  "better-sqlite3-multiple-ciphers": "^13.0.3",
41
41
  "node-pty": "^1.1.0",
42
42
  "sharp": "^0.35.5",