@graphorin/store-sqlite-encrypted 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @graphorin/store-sqlite-encrypted
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#135](https://github.com/o-stepper/graphorin/pull/135) [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3) Thanks [@o-stepper](https://github.com/o-stepper)! - Store durability and data lifecycle (audit 2026-07-04 Wave B, cluster B7).
8
+
9
+ - store-02: new `graphorin storage backup <dest>` - online, consistent copy via the driver's page-level `backup()` API (safe under a live writer, preserves rowids so FTS5 mappings survive, encrypted stores produce an equally-encrypted copy). `deployment.md` stops recommending the non-existent `BACKUP TO` and explicitly warns against `VACUUM INTO`.
10
+ - store-03: episode vector KNN gains the MRET-9 over-fetch loop facts already had (shared `widenKnn` helper) - a minority user's episodic recall is no longer starved to zero by a dominant user's vectors.
11
+ - store-04: GDPR `purge()` scrubs the fact's text out of `memory_history` (both rows keyed to the id and value-matching rows - a SUPERSEDE row carries the new fact's text on the OLD id) inside the same transaction, keeping the event skeleton; new `SqliteMemoryStore.pruneHistory(olderThanMs)` retention API bounds the otherwise-unbounded table.
12
+ - store-05: `encryptDatabase` copies via the driver's online backup API instead of checkpoint-close-then-copyFileSync - a concurrent writer can no longer commit WAL frames that silently miss the encrypted copy.
13
+ - store-06: every write transaction now BEGINs IMMEDIATE, eliminating the SQLITE_BUSY_SNAPSHOT class on read-then-write transactions under server+CLI concurrency (busy_timeout waits instead).
14
+ - store-07: `upsertState` builds its UPDATE from the supplied patch keys only (insert-if-absent + patch, in one immediate transaction) and `releaseLock` is a single conditional UPDATE - a concurrently acquired consolidator lock can no longer be silently reverted.
15
+ - store-13: migration 025 adds partial indexes on `facts.supersedes` / `facts.superseded_by`, so `historyOf` chain walks stop scanning the user's facts per node; registry owner map backfilled for 024/025.
16
+ - store F13: `distanceMetric: 'dot'` is coerced to `'cosine'` at registration with a loud warning (the vec0 table never computed dot; the meta now matches reality).
17
+
18
+ - [#138](https://github.com/o-stepper/graphorin/pull/138) [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a) Thanks [@o-stepper](https://github.com/o-stepper)! - Documentation-truth sweep (audit 2026-07-04 Wave E, cluster E10): stale npm package descriptions rewritten (cli "Phase 14a three commands", mcp "upcoming auth CLI", store-sqlite's nonexistent WorkerPool), the store-sqlite WorkerPool TSDoc and the cipher-pragma ordering comment corrected, the executor timeout-precedence JSDoc fixed to the real `inlineToolTimeoutMs > tier timeoutMs > default` order, the `rrf.<label>` explain signals documented, and the skills spec-snapshot wording no longer claims a CI cron refreshes it (the diff is a manual `--upstream` pass; the release gate only parses the bundled snapshot).
19
+
20
+ - Updated dependencies [[`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a)]:
21
+ - @graphorin/store-sqlite@0.6.0
22
+
23
+ ## 0.5.0
24
+
25
+ First version published to the npm registry (with Sigstore build
26
+ provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
27
+ milestones and were never published. All `@graphorin/*` packages release
28
+ lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
29
+ in the repository-level
30
+ [CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
31
+
3
32
  ## 0.1.0
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -7,14 +7,14 @@
7
7
  > encrypt / rekey / integrity-check runners that back the `graphorin storage`
8
8
  > CLI subcommand group.
9
9
  >
10
- > Project Graphorin · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko ·
10
+ > Project Graphorin · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko ·
11
11
  > <https://github.com/o-stepper/graphorin>
12
12
 
13
13
  ---
14
14
 
15
15
  ## Status
16
16
 
17
- - **Published:** v0.5.0 (optional sub-pack)
17
+ - **Published:** v0.6.0 (optional sub-pack)
18
18
  - **Default cipher:** `sqlcipher` (SQLCipher v4 compatible, `legacy=4`)
19
19
  - **Defaults:** encryption-at-rest is **OFF by default**. Opt in through
20
20
  `graphorin init --encrypted`.
@@ -111,8 +111,8 @@ await rekeyDatabase({
111
111
 
112
112
  ## Cipher selection
113
113
 
114
- The default cipher is `'sqlcipher'` with the `legacy=4` parameter set
115
- SQLCipher v4 compatible chosen for ecosystem tooling compatibility
114
+ The default cipher is `'sqlcipher'` with the `legacy=4` parameter set -
115
+ SQLCipher v4 compatible - chosen for ecosystem tooling compatibility
116
116
  (DB Browser for SQLCipher, `sqlcipher` CLI, GUI inspectors). Other
117
117
  cipher modes shipped by the cipher peer are accepted; pass them via the
118
118
  `cipher` option:
@@ -136,7 +136,7 @@ cipher modes shipped by the cipher peer are accepted; pass them via the
136
136
  - **WAL housekeeping bytes** are visible to an attacker on file leak
137
137
  (page numbers, lengths). Row contents are not. See ADR-030 § 5
138
138
  for the threat-model nuance.
139
- - **Performance overhead** is typically 515 % on OLTP workloads (read
139
+ - **Performance overhead** is typically 5-15 % on OLTP workloads (read
140
140
  / write of small rows). The triggers cron that runs the daily
141
141
  `cipher_integrity_check` is a read-only pragma so it does not
142
142
  block writers.
@@ -148,8 +148,8 @@ cipher modes shipped by the cipher peer are accepted; pass them via the
148
148
 
149
149
  ## Related decisions
150
150
 
151
- - ADR-030 SQLite encryption at rest (SQLCipher v4 baseline + KDF parameters).
152
- - ADR-008 Storage default `better-sqlite3` (synchronous embedded SQLite + WAL hardening).
151
+ - ADR-030 - SQLite encryption at rest (SQLCipher v4 baseline + KDF parameters).
152
+ - ADR-008 - Storage default `better-sqlite3` (synchronous embedded SQLite + WAL hardening).
153
153
 
154
154
  ---
155
155
 
@@ -159,4 +159,4 @@ MIT © 2026 Oleksiy Stepurenko
159
159
 
160
160
  ---
161
161
 
162
- **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
162
+ **Project Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
@@ -3,7 +3,7 @@ import { EncryptionCipher } from "@graphorin/store-sqlite/encryption";
3
3
  //#region src/cipher-config.d.ts
4
4
 
5
5
  /**
6
- * Default cipher. Matches ADR-030 § 2 SQLCipher v4 compatible
6
+ * Default cipher. Matches ADR-030 § 2 - SQLCipher v4 compatible
7
7
  * (AES-256-CBC + HMAC-SHA1, `legacy=4` parameter set).
8
8
  *
9
9
  * @stable
@@ -1,13 +1,13 @@
1
1
  //#region src/cipher-config.ts
2
2
  /**
3
- * Default cipher. Matches ADR-030 § 2 SQLCipher v4 compatible
3
+ * Default cipher. Matches ADR-030 § 2 - SQLCipher v4 compatible
4
4
  * (AES-256-CBC + HMAC-SHA1, `legacy=4` parameter set).
5
5
  *
6
6
  * @stable
7
7
  */
8
8
  const DEFAULT_CIPHER = "sqlcipher";
9
9
  /**
10
- * Cipher-specific PRAGMAs that must be applied **immediately after**
10
+ * Cipher-specific PRAGMAs that must be applied **before**
11
11
  * `PRAGMA key = ...` for SQLite3MultipleCiphers to interpret the key
12
12
  * correctly. The list mirrors the upstream documentation.
13
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cipher-config.js","names":["DEFAULT_CIPHER: EncryptionCipher","CIPHER_PRAGMAS: Readonly<Record<EncryptionCipher, ReadonlyArray<string>>>"],"sources":["../src/cipher-config.ts"],"sourcesContent":["/**\n * Cipher selection + passphrase encoding helpers shared by the\n * `createEncryptedConnection`, `encryptDatabase`, and `rekeyDatabase`\n * runners. The cipher peer reads the passphrase via `PRAGMA key`; this\n * module owns the SQL-literal escaping path so all three runners agree.\n *\n * @packageDocumentation\n */\n\nimport type { EncryptionCipher } from '@graphorin/store-sqlite/encryption';\n\nexport type { EncryptionCipher };\n\n/**\n * Default cipher. Matches ADR-030 § 2 SQLCipher v4 compatible\n * (AES-256-CBC + HMAC-SHA1, `legacy=4` parameter set).\n *\n * @stable\n */\nexport const DEFAULT_CIPHER: EncryptionCipher = 'sqlcipher';\n\n/**\n * Cipher-specific PRAGMAs that must be applied **immediately after**\n * `PRAGMA key = ...` for SQLite3MultipleCiphers to interpret the key\n * correctly. The list mirrors the upstream documentation.\n *\n * @internal\n */\nconst CIPHER_PRAGMAS: Readonly<Record<EncryptionCipher, ReadonlyArray<string>>> = Object.freeze({\n sqlcipher: Object.freeze([\"cipher = 'sqlcipher'\", 'legacy = 4']),\n chacha20: Object.freeze([\"cipher = 'chacha20'\"]),\n aes256cbc: Object.freeze([\"cipher = 'aes256cbc'\"]),\n aes128cbc: Object.freeze([\"cipher = 'aes128cbc'\"]),\n rc4: Object.freeze([\"cipher = 'rc4'\"]),\n});\n\n/**\n * Returns the PRAGMA statements that select a cipher. The list is\n * applied **before** `PRAGMA key = ...` so the cipher peer knows which\n * KDF / mode to use when interpreting the key bytes.\n *\n * @stable\n */\nexport function pragmaSequenceForCipher(cipher: EncryptionCipher): ReadonlyArray<string> {\n const seq = CIPHER_PRAGMAS[cipher];\n if (seq === undefined) {\n throw new TypeError(\n `[graphorin/store-sqlite-encrypted] unknown cipher '${String(cipher)}'. ` +\n `Supported: ${Object.keys(CIPHER_PRAGMAS).join(', ')}.`,\n );\n }\n return seq;\n}\n\n/**\n * SQL-literal-encodes a passphrase for use as the right-hand side of\n * `PRAGMA key = ...`.\n *\n * - String input is wrapped in single quotes with internal `'` doubled\n * per the SQL specification.\n * - Buffer input is encoded as the cipher peer's `x'<hex>'` blob form\n * so binary keys round-trip exactly.\n *\n * Empty inputs are rejected at this layer so callers cannot\n * accidentally open an unencrypted DB with an empty key.\n *\n * @stable\n */\nexport function encodePassphraseForPragma(value: string | Buffer): string {\n if (typeof value === 'string') {\n if (value.length === 0) {\n throw new Error('[graphorin/store-sqlite-encrypted] passphrase is empty');\n }\n return `'${value.replace(/'/g, \"''\")}'`;\n }\n if (value.length === 0) {\n throw new Error('[graphorin/store-sqlite-encrypted] passphrase buffer is empty');\n }\n return `x'${value.toString('hex')}'`;\n}\n"],"mappings":";;;;;;;AAmBA,MAAaA,iBAAmC;;;;;;;;AAShD,MAAMC,iBAA4E,OAAO,OAAO;CAC9F,WAAW,OAAO,OAAO,CAAC,wBAAwB,aAAa,CAAC;CAChE,UAAU,OAAO,OAAO,CAAC,sBAAsB,CAAC;CAChD,WAAW,OAAO,OAAO,CAAC,uBAAuB,CAAC;CAClD,WAAW,OAAO,OAAO,CAAC,uBAAuB,CAAC;CAClD,KAAK,OAAO,OAAO,CAAC,iBAAiB,CAAC;CACvC,CAAC;;;;;;;;AASF,SAAgB,wBAAwB,QAAiD;CACvF,MAAM,MAAM,eAAe;AAC3B,KAAI,QAAQ,OACV,OAAM,IAAI,UACR,sDAAsD,OAAO,OAAO,CAAC,gBACrD,OAAO,KAAK,eAAe,CAAC,KAAK,KAAK,CAAC,GACxD;AAEH,QAAO;;;;;;;;;;;;;;;;AAiBT,SAAgB,0BAA0B,OAAgC;AACxE,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,MAAM,WAAW,EACnB,OAAM,IAAI,MAAM,yDAAyD;AAE3E,SAAO,IAAI,MAAM,QAAQ,MAAM,KAAK,CAAC;;AAEvC,KAAI,MAAM,WAAW,EACnB,OAAM,IAAI,MAAM,gEAAgE;AAElF,QAAO,KAAK,MAAM,SAAS,MAAM,CAAC"}
1
+ {"version":3,"file":"cipher-config.js","names":["DEFAULT_CIPHER: EncryptionCipher","CIPHER_PRAGMAS: Readonly<Record<EncryptionCipher, ReadonlyArray<string>>>"],"sources":["../src/cipher-config.ts"],"sourcesContent":["/**\n * Cipher selection + passphrase encoding helpers shared by the\n * `createEncryptedConnection`, `encryptDatabase`, and `rekeyDatabase`\n * runners. The cipher peer reads the passphrase via `PRAGMA key`; this\n * module owns the SQL-literal escaping path so all three runners agree.\n *\n * @packageDocumentation\n */\n\nimport type { EncryptionCipher } from '@graphorin/store-sqlite/encryption';\n\nexport type { EncryptionCipher };\n\n/**\n * Default cipher. Matches ADR-030 § 2 - SQLCipher v4 compatible\n * (AES-256-CBC + HMAC-SHA1, `legacy=4` parameter set).\n *\n * @stable\n */\nexport const DEFAULT_CIPHER: EncryptionCipher = 'sqlcipher';\n\n/**\n * Cipher-specific PRAGMAs that must be applied **before**\n * `PRAGMA key = ...` for SQLite3MultipleCiphers to interpret the key\n * correctly. The list mirrors the upstream documentation.\n *\n * @internal\n */\nconst CIPHER_PRAGMAS: Readonly<Record<EncryptionCipher, ReadonlyArray<string>>> = Object.freeze({\n sqlcipher: Object.freeze([\"cipher = 'sqlcipher'\", 'legacy = 4']),\n chacha20: Object.freeze([\"cipher = 'chacha20'\"]),\n aes256cbc: Object.freeze([\"cipher = 'aes256cbc'\"]),\n aes128cbc: Object.freeze([\"cipher = 'aes128cbc'\"]),\n rc4: Object.freeze([\"cipher = 'rc4'\"]),\n});\n\n/**\n * Returns the PRAGMA statements that select a cipher. The list is\n * applied **before** `PRAGMA key = ...` so the cipher peer knows which\n * KDF / mode to use when interpreting the key bytes.\n *\n * @stable\n */\nexport function pragmaSequenceForCipher(cipher: EncryptionCipher): ReadonlyArray<string> {\n const seq = CIPHER_PRAGMAS[cipher];\n if (seq === undefined) {\n throw new TypeError(\n `[graphorin/store-sqlite-encrypted] unknown cipher '${String(cipher)}'. ` +\n `Supported: ${Object.keys(CIPHER_PRAGMAS).join(', ')}.`,\n );\n }\n return seq;\n}\n\n/**\n * SQL-literal-encodes a passphrase for use as the right-hand side of\n * `PRAGMA key = ...`.\n *\n * - String input is wrapped in single quotes with internal `'` doubled\n * per the SQL specification.\n * - Buffer input is encoded as the cipher peer's `x'<hex>'` blob form\n * so binary keys round-trip exactly.\n *\n * Empty inputs are rejected at this layer so callers cannot\n * accidentally open an unencrypted DB with an empty key.\n *\n * @stable\n */\nexport function encodePassphraseForPragma(value: string | Buffer): string {\n if (typeof value === 'string') {\n if (value.length === 0) {\n throw new Error('[graphorin/store-sqlite-encrypted] passphrase is empty');\n }\n return `'${value.replace(/'/g, \"''\")}'`;\n }\n if (value.length === 0) {\n throw new Error('[graphorin/store-sqlite-encrypted] passphrase buffer is empty');\n }\n return `x'${value.toString('hex')}'`;\n}\n"],"mappings":";;;;;;;AAmBA,MAAaA,iBAAmC;;;;;;;;AAShD,MAAMC,iBAA4E,OAAO,OAAO;CAC9F,WAAW,OAAO,OAAO,CAAC,wBAAwB,aAAa,CAAC;CAChE,UAAU,OAAO,OAAO,CAAC,sBAAsB,CAAC;CAChD,WAAW,OAAO,OAAO,CAAC,uBAAuB,CAAC;CAClD,WAAW,OAAO,OAAO,CAAC,uBAAuB,CAAC;CAClD,KAAK,OAAO,OAAO,CAAC,iBAAiB,CAAC;CACvC,CAAC;;;;;;;;AASF,SAAgB,wBAAwB,QAAiD;CACvF,MAAM,MAAM,eAAe;AAC3B,KAAI,QAAQ,OACV,OAAM,IAAI,UACR,sDAAsD,OAAO,OAAO,CAAC,gBACrD,OAAO,KAAK,eAAe,CAAC,KAAK,KAAK,CAAC,GACxD;AAEH,QAAO;;;;;;;;;;;;;;;;AAiBT,SAAgB,0BAA0B,OAAgC;AACxE,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,MAAM,WAAW,EACnB,OAAM,IAAI,MAAM,yDAAyD;AAE3E,SAAO,IAAI,MAAM,QAAQ,MAAM,KAAK,CAAC;;AAEvC,KAAI,MAAM,WAAW,EACnB,OAAM,IAAI,MAAM,gEAAgE;AAElF,QAAO,KAAK,MAAM,SAAS,MAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"cipher-peer.js","names":["CACHED_CTOR: BetterSqlite3Constructor | null"],"sources":["../src/cipher-peer.ts"],"sourcesContent":["/**\n * Loads the `better-sqlite3-multiple-ciphers` cipher peer driver. The\n * peer is declared `optional: false` in this package's `package.json`\n * (the whole point of installing this sub-pack is to opt in to\n * encryption-at-rest), but we still wrap the import in a friendly\n * fail-fast helper so consumers see an actionable error rather than a\n * raw `ERR_MODULE_NOT_FOUND`.\n *\n * The peer is loaded lazily importing this package on its own does\n * NOT load the native addon, so callers that only inspect the public\n * helpers (e.g. for documentation generation) pay no native-load cost.\n *\n * @packageDocumentation\n */\n\nimport type { BetterSqlite3Constructor } from '@graphorin/store-sqlite/connection';\n\n/**\n * Raised when the cipher peer driver cannot be loaded. Distinct from\n * the matching `CipherPeerMissingError` in `@graphorin/store-sqlite/\n * encryption` so consumers can catch the two layers independently.\n *\n * @stable\n */\nexport class EncryptedStorePeerMissingError extends Error {\n override readonly name = 'EncryptedStorePeerMissingError';\n}\n\n/** @internal */\nlet CACHED_CTOR: BetterSqlite3Constructor | null = null;\n\n/**\n * Loads `better-sqlite3-multiple-ciphers`. The result is cached for\n * the lifetime of the process so repeat callers (encrypt + rekey +\n * connection-open in the same process) share one native handle.\n *\n * @stable\n */\nexport async function loadCipherPeer(): Promise<BetterSqlite3Constructor> {\n if (CACHED_CTOR !== null) return CACHED_CTOR;\n try {\n const mod = (await import('better-sqlite3-multiple-ciphers')) as unknown as {\n default: BetterSqlite3Constructor;\n };\n CACHED_CTOR = mod.default;\n return CACHED_CTOR;\n } catch (err) {\n throw new EncryptedStorePeerMissingError(\n \"the cipher peer 'better-sqlite3-multiple-ciphers' could not be loaded. \" +\n 'This sub-pack declares it as a required peer dependency; install it via ' +\n '`pnpm add better-sqlite3-multiple-ciphers@^12.9.0` (or rerun `pnpm install` ' +\n 'after adding @graphorin/store-sqlite-encrypted).',\n { cause: err },\n );\n }\n}\n\n/**\n * Test-only escape hatch. Drops the cached constructor so the next\n * {@link loadCipherPeer} call re-imports the peer.\n *\n * @internal\n */\nexport function _resetCipherPeerCacheForTesting(): void {\n CACHED_CTOR = null;\n}\n\n/**\n * Test-only escape hatch. Pre-populates the cache with a stub driver\n * so unit tests can exercise the encrypt / rekey runners without\n * touching the native cipher addon.\n *\n * @internal\n */\nexport function _setCipherPeerForTesting(ctor: BetterSqlite3Constructor): void {\n CACHED_CTOR = ctor;\n}\n"],"mappings":";;;;;;;;AAwBA,IAAa,iCAAb,cAAoD,MAAM;CACxD,AAAkB,OAAO;;;AAI3B,IAAIA,cAA+C;;;;;;;;AASnD,eAAsB,iBAAoD;AACxE,KAAI,gBAAgB,KAAM,QAAO;AACjC,KAAI;AAIF,iBAHa,MAAM,OAAO,oCAGR;AAClB,SAAO;UACA,KAAK;AACZ,QAAM,IAAI,+BACR,+QAIA,EAAE,OAAO,KAAK,CACf;;;;;;;;;AAUL,SAAgB,kCAAwC;AACtD,eAAc;;;;;;;;;AAUhB,SAAgB,yBAAyB,MAAsC;AAC7E,eAAc"}
1
+ {"version":3,"file":"cipher-peer.js","names":["CACHED_CTOR: BetterSqlite3Constructor | null"],"sources":["../src/cipher-peer.ts"],"sourcesContent":["/**\n * Loads the `better-sqlite3-multiple-ciphers` cipher peer driver. The\n * peer is declared `optional: false` in this package's `package.json`\n * (the whole point of installing this sub-pack is to opt in to\n * encryption-at-rest), but we still wrap the import in a friendly\n * fail-fast helper so consumers see an actionable error rather than a\n * raw `ERR_MODULE_NOT_FOUND`.\n *\n * The peer is loaded lazily - importing this package on its own does\n * NOT load the native addon, so callers that only inspect the public\n * helpers (e.g. for documentation generation) pay no native-load cost.\n *\n * @packageDocumentation\n */\n\nimport type { BetterSqlite3Constructor } from '@graphorin/store-sqlite/connection';\n\n/**\n * Raised when the cipher peer driver cannot be loaded. Distinct from\n * the matching `CipherPeerMissingError` in `@graphorin/store-sqlite/\n * encryption` so consumers can catch the two layers independently.\n *\n * @stable\n */\nexport class EncryptedStorePeerMissingError extends Error {\n override readonly name = 'EncryptedStorePeerMissingError';\n}\n\n/** @internal */\nlet CACHED_CTOR: BetterSqlite3Constructor | null = null;\n\n/**\n * Loads `better-sqlite3-multiple-ciphers`. The result is cached for\n * the lifetime of the process so repeat callers (encrypt + rekey +\n * connection-open in the same process) share one native handle.\n *\n * @stable\n */\nexport async function loadCipherPeer(): Promise<BetterSqlite3Constructor> {\n if (CACHED_CTOR !== null) return CACHED_CTOR;\n try {\n const mod = (await import('better-sqlite3-multiple-ciphers')) as unknown as {\n default: BetterSqlite3Constructor;\n };\n CACHED_CTOR = mod.default;\n return CACHED_CTOR;\n } catch (err) {\n throw new EncryptedStorePeerMissingError(\n \"the cipher peer 'better-sqlite3-multiple-ciphers' could not be loaded. \" +\n 'This sub-pack declares it as a required peer dependency; install it via ' +\n '`pnpm add better-sqlite3-multiple-ciphers@^12.9.0` (or rerun `pnpm install` ' +\n 'after adding @graphorin/store-sqlite-encrypted).',\n { cause: err },\n );\n }\n}\n\n/**\n * Test-only escape hatch. Drops the cached constructor so the next\n * {@link loadCipherPeer} call re-imports the peer.\n *\n * @internal\n */\nexport function _resetCipherPeerCacheForTesting(): void {\n CACHED_CTOR = null;\n}\n\n/**\n * Test-only escape hatch. Pre-populates the cache with a stub driver\n * so unit tests can exercise the encrypt / rekey runners without\n * touching the native cipher addon.\n *\n * @internal\n */\nexport function _setCipherPeerForTesting(ctor: BetterSqlite3Constructor): void {\n CACHED_CTOR = ctor;\n}\n"],"mappings":";;;;;;;;AAwBA,IAAa,iCAAb,cAAoD,MAAM;CACxD,AAAkB,OAAO;;;AAI3B,IAAIA,cAA+C;;;;;;;;AASnD,eAAsB,iBAAoD;AACxE,KAAI,gBAAgB,KAAM,QAAO;AACjC,KAAI;AAIF,iBAHa,MAAM,OAAO,oCAGR;AAClB,SAAO;UACA,KAAK;AACZ,QAAM,IAAI,+BACR,+QAIA,EAAE,OAAO,KAAK,CACf;;;;;;;;;AAUL,SAAgB,kCAAwC;AACtD,eAAc;;;;;;;;;AAUhB,SAAgB,yBAAyB,MAAsC;AAC7E,eAAc"}
@@ -4,7 +4,7 @@ import { OpenConnectionOptions, SqliteConnection } from "@graphorin/store-sqlite
4
4
 
5
5
  /**
6
6
  * Opens an encrypted SQLite connection. Differs from `openConnection`
7
- * only in that the cipher peer driver is preloaded callers that
7
+ * only in that the cipher peer driver is preloaded - callers that
8
8
  * supply an `encryption.passphraseResolver` get the same behaviour as
9
9
  * `openConnection({ encryption })` plus an explicit fail-fast on a
10
10
  * missing cipher peer.
@@ -7,7 +7,7 @@ import { openConnection } from "@graphorin/store-sqlite/connection";
7
7
  * sqlite/connection` that pre-loads the cipher peer driver and applies
8
8
  * the cipher PRAGMAs in the right order.
9
9
  *
10
- * Most callers never reach this directly the standard path is to
10
+ * Most callers never reach this directly - the standard path is to
11
11
  * pass `{ encryption: { enabled: true, passphraseResolver } }` to the
12
12
  * store-sqlite facade and let it consult the canonical
13
13
  * `loadCipherDriver`. This helper exists for the encrypt / rekey
@@ -20,7 +20,7 @@ import { openConnection } from "@graphorin/store-sqlite/connection";
20
20
  */
21
21
  /**
22
22
  * Opens an encrypted SQLite connection. Differs from `openConnection`
23
- * only in that the cipher peer driver is preloaded callers that
23
+ * only in that the cipher peer driver is preloaded - callers that
24
24
  * supply an `encryption.passphraseResolver` get the same behaviour as
25
25
  * `openConnection({ encryption })` plus an explicit fail-fast on a
26
26
  * missing cipher peer.
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","names":[],"sources":["../src/connection.ts"],"sourcesContent":["/**\n * Convenience wrapper around `openConnection` from `@graphorin/store-\n * sqlite/connection` that pre-loads the cipher peer driver and applies\n * the cipher PRAGMAs in the right order.\n *\n * Most callers never reach this directly the standard path is to\n * pass `{ encryption: { enabled: true, passphraseResolver } }` to the\n * store-sqlite facade and let it consult the canonical\n * `loadCipherDriver`. This helper exists for the encrypt / rekey\n * runners (which need a \"raw\" handle on a freshly created cipher DB\n * before the standard schema migrations run) and for advanced\n * consumers that want to build a custom store on top of an encrypted\n * connection.\n *\n * @packageDocumentation\n */\n\nimport {\n type OpenConnectionOptions,\n openConnection,\n type SqliteConnection,\n} from '@graphorin/store-sqlite/connection';\n\nimport { loadCipherPeer } from './cipher-peer.js';\n\n/**\n * Opens an encrypted SQLite connection. Differs from `openConnection`\n * only in that the cipher peer driver is preloaded callers that\n * supply an `encryption.passphraseResolver` get the same behaviour as\n * `openConnection({ encryption })` plus an explicit fail-fast on a\n * missing cipher peer.\n *\n * @stable\n */\nexport async function createEncryptedConnection(\n options: OpenConnectionOptions,\n): Promise<SqliteConnection> {\n if (options.encryption?.enabled !== true) {\n throw new Error(\n '[graphorin/store-sqlite-encrypted] createEncryptedConnection requires ' +\n '`encryption.enabled: true`. Use `openConnection` directly for unencrypted DBs.',\n );\n }\n const ctor = await loadCipherPeer();\n return openConnection({\n ...options,\n cipherLoader: async () => ctor,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,0BACpB,SAC2B;AAC3B,KAAI,QAAQ,YAAY,YAAY,KAClC,OAAM,IAAI,MACR,uJAED;CAEH,MAAM,OAAO,MAAM,gBAAgB;AACnC,QAAO,eAAe;EACpB,GAAG;EACH,cAAc,YAAY;EAC3B,CAAC"}
1
+ {"version":3,"file":"connection.js","names":[],"sources":["../src/connection.ts"],"sourcesContent":["/**\n * Convenience wrapper around `openConnection` from `@graphorin/store-\n * sqlite/connection` that pre-loads the cipher peer driver and applies\n * the cipher PRAGMAs in the right order.\n *\n * Most callers never reach this directly - the standard path is to\n * pass `{ encryption: { enabled: true, passphraseResolver } }` to the\n * store-sqlite facade and let it consult the canonical\n * `loadCipherDriver`. This helper exists for the encrypt / rekey\n * runners (which need a \"raw\" handle on a freshly created cipher DB\n * before the standard schema migrations run) and for advanced\n * consumers that want to build a custom store on top of an encrypted\n * connection.\n *\n * @packageDocumentation\n */\n\nimport {\n type OpenConnectionOptions,\n openConnection,\n type SqliteConnection,\n} from '@graphorin/store-sqlite/connection';\n\nimport { loadCipherPeer } from './cipher-peer.js';\n\n/**\n * Opens an encrypted SQLite connection. Differs from `openConnection`\n * only in that the cipher peer driver is preloaded - callers that\n * supply an `encryption.passphraseResolver` get the same behaviour as\n * `openConnection({ encryption })` plus an explicit fail-fast on a\n * missing cipher peer.\n *\n * @stable\n */\nexport async function createEncryptedConnection(\n options: OpenConnectionOptions,\n): Promise<SqliteConnection> {\n if (options.encryption?.enabled !== true) {\n throw new Error(\n '[graphorin/store-sqlite-encrypted] createEncryptedConnection requires ' +\n '`encryption.enabled: true`. Use `openConnection` directly for unencrypted DBs.',\n );\n }\n const ctor = await loadCipherPeer();\n return openConnection({\n ...options,\n cipherLoader: async () => ctor,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,0BACpB,SAC2B;AAC3B,KAAI,QAAQ,YAAY,YAAY,KAClC,OAAM,IAAI,MACR,uJAED;CAEH,MAAM,OAAO,MAAM,gBAAgB;AACnC,QAAO,eAAe;EACpB,GAAG;EACH,cAAc,YAAY;EAC3B,CAAC"}
package/dist/encrypt.d.ts CHANGED
@@ -20,7 +20,7 @@ interface EncryptDatabaseOptions {
20
20
  * If `true`, atomically rename `targetPath` -> `sourcePath` after the
21
21
  * integrity check passes. The original `sourcePath` is renamed to
22
22
  * `${sourcePath}.bak.${timestamp}` so an operator can recover.
23
- * Default `false` the CLI does the swap explicitly.
23
+ * Default `false` - the CLI does the swap explicitly.
24
24
  */
25
25
  readonly swap?: boolean;
26
26
  /**
package/dist/encrypt.js CHANGED
@@ -2,25 +2,25 @@ import { DEFAULT_CIPHER, encodePassphraseForPragma, pragmaSequenceForCipher } fr
2
2
  import { loadCipherPeer } from "./cipher-peer.js";
3
3
  import { createEncryptedConnection } from "./connection.js";
4
4
  import { cipherIntegrityCheck } from "./integrity-check.js";
5
- import { copyFileSync, existsSync, renameSync, unlinkSync } from "node:fs";
5
+ import { existsSync, renameSync, unlinkSync } from "node:fs";
6
6
  import { isAbsolute, resolve } from "node:path";
7
7
 
8
8
  //#region src/encrypt.ts
9
9
  /**
10
- * `encryptDatabase` converts an unencrypted SQLite database file
10
+ * `encryptDatabase` - converts an unencrypted SQLite database file
11
11
  * into an encrypted one. Backs `graphorin storage encrypt` per
12
12
  * ADR-030 § 8.
13
13
  *
14
14
  * Strategy (CS-7): sqlite3mc ships **no** `sqlcipher_export` function
15
- * (verified against the real peer the old ATTACH+export path threw
15
+ * (verified against the real peer - the old ATTACH+export path threw
16
16
  * "no such function" on every real run), so the export is a
17
17
  * **checkpoint → file copy → in-place `PRAGMA rekey`** sequence:
18
18
  *
19
19
  * 1. open the plaintext source, `wal_checkpoint(TRUNCATE)`, close;
20
20
  * 2. byte-copy the file to the target (this trivially preserves every
21
- * rowid, so FTS5 external-content mappings stay intact CS-10);
21
+ * rowid, so FTS5 external-content mappings stay intact - CS-10);
22
22
  * 3. open the copy through the cipher peer with NO key, apply the
23
- * cipher-selection pragmas, then `PRAGMA rekey = <key>` sqlite3mc
23
+ * cipher-selection pragmas, then `PRAGMA rekey = <key>` - sqlite3mc
24
24
  * encrypts a plaintext database in place;
25
25
  * 4. re-open with the key and verify via `PRAGMA integrity_check`.
26
26
  *
@@ -46,13 +46,12 @@ async function encryptDatabase(options) {
46
46
  }
47
47
  const Ctor = await loadCipherPeer();
48
48
  try {
49
- const source = new Ctor(sourcePath);
49
+ const source = new Ctor(sourcePath, { readonly: true });
50
50
  try {
51
- source.pragma("wal_checkpoint(TRUNCATE)");
51
+ await source.backup(targetPath);
52
52
  } finally {
53
53
  if (source.open) source.close();
54
54
  }
55
- copyFileSync(sourcePath, targetPath);
56
55
  const target = new Ctor(targetPath);
57
56
  try {
58
57
  for (const pragma of pragmaSequenceForCipher(cipher)) target.pragma(pragma);
@@ -1 +1 @@
1
- {"version":3,"file":"encrypt.js","names":["integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> }","swap: { readonly originalRenamedTo: string } | undefined"],"sources":["../src/encrypt.ts"],"sourcesContent":["/**\n * `encryptDatabase` converts an unencrypted SQLite database file\n * into an encrypted one. Backs `graphorin storage encrypt` per\n * ADR-030 § 8.\n *\n * Strategy (CS-7): sqlite3mc ships **no** `sqlcipher_export` function\n * (verified against the real peer the old ATTACH+export path threw\n * \"no such function\" on every real run), so the export is a\n * **checkpoint → file copy → in-place `PRAGMA rekey`** sequence:\n *\n * 1. open the plaintext source, `wal_checkpoint(TRUNCATE)`, close;\n * 2. byte-copy the file to the target (this trivially preserves every\n * rowid, so FTS5 external-content mappings stay intact CS-10);\n * 3. open the copy through the cipher peer with NO key, apply the\n * cipher-selection pragmas, then `PRAGMA rekey = <key>` sqlite3mc\n * encrypts a plaintext database in place;\n * 4. re-open with the key and verify via `PRAGMA integrity_check`.\n *\n * @packageDocumentation\n */\n\nimport { copyFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\nimport {\n DEFAULT_CIPHER,\n type EncryptionCipher,\n encodePassphraseForPragma,\n pragmaSequenceForCipher,\n} from './cipher-config.js';\nimport { loadCipherPeer } from './cipher-peer.js';\nimport { createEncryptedConnection } from './connection.js';\nimport { cipherIntegrityCheck } from './integrity-check.js';\n\n/**\n * Options for {@link encryptDatabase}.\n *\n * @stable\n */\nexport interface EncryptDatabaseOptions {\n /** Path to the existing unencrypted source DB. */\n readonly sourcePath: string;\n /** Path the encrypted output is written to. Must not exist. */\n readonly targetPath: string;\n /** Passphrase for the new encrypted DB. */\n readonly passphrase: string | Buffer;\n /** Cipher selection. Default `'sqlcipher'` (SQLCipher v4 compatible). */\n readonly cipher?: EncryptionCipher;\n /**\n * If `true`, atomically rename `targetPath` -> `sourcePath` after the\n * integrity check passes. The original `sourcePath` is renamed to\n * `${sourcePath}.bak.${timestamp}` so an operator can recover.\n * Default `false` the CLI does the swap explicitly.\n */\n readonly swap?: boolean;\n /**\n * If `true`, overwrite an existing `targetPath` instead of failing.\n * Default `false`.\n */\n readonly overwriteTarget?: boolean;\n}\n\n/**\n * Result of a successful {@link encryptDatabase} run.\n *\n * @stable\n */\nexport interface EncryptDatabaseResult {\n readonly sourcePath: string;\n readonly targetPath: string;\n readonly cipher: EncryptionCipher;\n readonly integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n readonly swap?: { readonly originalRenamedTo: string };\n}\n\n/**\n * Encrypts an unencrypted SQLite database. Returns once the target\n * file has been written and verified. Throws if the source is missing,\n * the target already exists (and `overwriteTarget` is unset), the\n * cipher peer is missing, or the integrity check fails.\n *\n * @stable\n */\nexport async function encryptDatabase(\n options: EncryptDatabaseOptions,\n): Promise<EncryptDatabaseResult> {\n const sourcePath = absolute(options.sourcePath);\n const targetPath = absolute(options.targetPath);\n const cipher = options.cipher ?? DEFAULT_CIPHER;\n\n if (!existsSync(sourcePath)) {\n throw new Error(`[graphorin/store-sqlite-encrypted] source DB not found: ${sourcePath}`);\n }\n if (sourcePath === targetPath) {\n throw new Error(\n '[graphorin/store-sqlite-encrypted] sourcePath and targetPath must differ. ' +\n 'Pass a temporary targetPath then enable `swap: true` to atomically replace the source.',\n );\n }\n if (existsSync(targetPath)) {\n if (options.overwriteTarget !== true) {\n throw new Error(\n `[graphorin/store-sqlite-encrypted] target DB already exists: ${targetPath}. ` +\n 'Pass `overwriteTarget: true` to replace it.',\n );\n }\n unlinkSync(targetPath);\n }\n\n const Ctor = await loadCipherPeer();\n try {\n // 1. Checkpoint the plaintext source so a byte-copy captures the\n // full state even when the DB ran in WAL mode.\n const source = new Ctor(sourcePath);\n try {\n source.pragma('wal_checkpoint(TRUNCATE)');\n } finally {\n if (source.open) source.close();\n }\n\n // 2. Byte-copy — rowids (and therefore FTS5 mappings) are preserved.\n copyFileSync(sourcePath, targetPath);\n\n // 3. In-place conversion: cipher pragmas first, then `rekey` —\n // sqlite3mc encrypts a plaintext database in place.\n const target = new Ctor(targetPath);\n try {\n for (const pragma of pragmaSequenceForCipher(cipher)) {\n target.pragma(pragma);\n }\n // sqlite3mc refuses `rekey` in WAL journal mode (real-peer\n // verified) drop to DELETE for the conversion, restore after.\n target.pragma('journal_mode = DELETE');\n const encodedKey = encodePassphraseForPragma(options.passphrase);\n target.pragma(`rekey = ${encodedKey}`);\n target.pragma('journal_mode = WAL');\n } finally {\n if (target.open) target.close();\n }\n } catch (err) {\n if (existsSync(targetPath)) {\n try {\n unlinkSync(targetPath);\n } catch {\n // best-effort cleanup\n }\n }\n throw new Error(\n `[graphorin/store-sqlite-encrypted] encryption failed: ${(err as Error).message}`,\n { cause: err },\n );\n }\n\n const verify = await createEncryptedConnection({\n path: targetPath,\n skipSqliteVec: true,\n disableWalHardening: true,\n encryption: {\n enabled: true,\n cipher,\n passphraseResolver: async () => options.passphrase,\n },\n });\n let integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n try {\n const result = cipherIntegrityCheck(verify);\n integrityCheck = { ok: result.ok, rows: result.rows };\n } finally {\n verify.close();\n }\n\n if (!integrityCheck.ok) {\n if (existsSync(targetPath)) {\n try {\n unlinkSync(targetPath);\n } catch {\n // best-effort cleanup\n }\n }\n throw new Error(\n '[graphorin/store-sqlite-encrypted] post-encrypt integrity check failed: ' +\n integrityCheck.rows.join('; '),\n );\n }\n\n let swap: { readonly originalRenamedTo: string } | undefined;\n if (options.swap === true) {\n const ts = Date.now();\n const renamedTo = `${sourcePath}.bak.${ts}`;\n renameSync(sourcePath, renamedTo);\n renameSync(targetPath, sourcePath);\n swap = Object.freeze({ originalRenamedTo: renamedTo });\n }\n\n const out: EncryptDatabaseResult = Object.freeze({\n sourcePath,\n targetPath: swap !== undefined ? sourcePath : targetPath,\n cipher,\n integrityCheck: Object.freeze(integrityCheck),\n ...(swap !== undefined ? { swap } : {}),\n });\n return out;\n}\n\nfunction absolute(p: string): string {\n return isAbsolute(p) ? p : resolve(p);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,eAAsB,gBACpB,SACgC;CAChC,MAAM,aAAa,SAAS,QAAQ,WAAW;CAC/C,MAAM,aAAa,SAAS,QAAQ,WAAW;CAC/C,MAAM,SAAS,QAAQ,UAAU;AAEjC,KAAI,CAAC,WAAW,WAAW,CACzB,OAAM,IAAI,MAAM,2DAA2D,aAAa;AAE1F,KAAI,eAAe,WACjB,OAAM,IAAI,MACR,mKAED;AAEH,KAAI,WAAW,WAAW,EAAE;AAC1B,MAAI,QAAQ,oBAAoB,KAC9B,OAAM,IAAI,MACR,gEAAgE,WAAW,iDAE5E;AAEH,aAAW,WAAW;;CAGxB,MAAM,OAAO,MAAM,gBAAgB;AACnC,KAAI;EAGF,MAAM,SAAS,IAAI,KAAK,WAAW;AACnC,MAAI;AACF,UAAO,OAAO,2BAA2B;YACjC;AACR,OAAI,OAAO,KAAM,QAAO,OAAO;;AAIjC,eAAa,YAAY,WAAW;EAIpC,MAAM,SAAS,IAAI,KAAK,WAAW;AACnC,MAAI;AACF,QAAK,MAAM,UAAU,wBAAwB,OAAO,CAClD,QAAO,OAAO,OAAO;AAIvB,UAAO,OAAO,wBAAwB;GACtC,MAAM,aAAa,0BAA0B,QAAQ,WAAW;AAChE,UAAO,OAAO,WAAW,aAAa;AACtC,UAAO,OAAO,qBAAqB;YAC3B;AACR,OAAI,OAAO,KAAM,QAAO,OAAO;;UAE1B,KAAK;AACZ,MAAI,WAAW,WAAW,CACxB,KAAI;AACF,cAAW,WAAW;UAChB;AAIV,QAAM,IAAI,MACR,yDAA0D,IAAc,WACxE,EAAE,OAAO,KAAK,CACf;;CAGH,MAAM,SAAS,MAAM,0BAA0B;EAC7C,MAAM;EACN,eAAe;EACf,qBAAqB;EACrB,YAAY;GACV,SAAS;GACT;GACA,oBAAoB,YAAY,QAAQ;GACzC;EACF,CAAC;CACF,IAAIA;AACJ,KAAI;EACF,MAAM,SAAS,qBAAqB,OAAO;AAC3C,mBAAiB;GAAE,IAAI,OAAO;GAAI,MAAM,OAAO;GAAM;WAC7C;AACR,SAAO,OAAO;;AAGhB,KAAI,CAAC,eAAe,IAAI;AACtB,MAAI,WAAW,WAAW,CACxB,KAAI;AACF,cAAW,WAAW;UAChB;AAIV,QAAM,IAAI,MACR,6EACE,eAAe,KAAK,KAAK,KAAK,CACjC;;CAGH,IAAIC;AACJ,KAAI,QAAQ,SAAS,MAAM;EAEzB,MAAM,YAAY,GAAG,WAAW,OADrB,KAAK,KAAK;AAErB,aAAW,YAAY,UAAU;AACjC,aAAW,YAAY,WAAW;AAClC,SAAO,OAAO,OAAO,EAAE,mBAAmB,WAAW,CAAC;;AAUxD,QAPmC,OAAO,OAAO;EAC/C;EACA,YAAY,SAAS,SAAY,aAAa;EAC9C;EACA,gBAAgB,OAAO,OAAO,eAAe;EAC7C,GAAI,SAAS,SAAY,EAAE,MAAM,GAAG,EAAE;EACvC,CAAC;;AAIJ,SAAS,SAAS,GAAmB;AACnC,QAAO,WAAW,EAAE,GAAG,IAAI,QAAQ,EAAE"}
1
+ {"version":3,"file":"encrypt.js","names":["integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> }","swap: { readonly originalRenamedTo: string } | undefined"],"sources":["../src/encrypt.ts"],"sourcesContent":["/**\n * `encryptDatabase` - converts an unencrypted SQLite database file\n * into an encrypted one. Backs `graphorin storage encrypt` per\n * ADR-030 § 8.\n *\n * Strategy (CS-7): sqlite3mc ships **no** `sqlcipher_export` function\n * (verified against the real peer - the old ATTACH+export path threw\n * \"no such function\" on every real run), so the export is a\n * **checkpoint → file copy → in-place `PRAGMA rekey`** sequence:\n *\n * 1. open the plaintext source, `wal_checkpoint(TRUNCATE)`, close;\n * 2. byte-copy the file to the target (this trivially preserves every\n * rowid, so FTS5 external-content mappings stay intact - CS-10);\n * 3. open the copy through the cipher peer with NO key, apply the\n * cipher-selection pragmas, then `PRAGMA rekey = <key>` - sqlite3mc\n * encrypts a plaintext database in place;\n * 4. re-open with the key and verify via `PRAGMA integrity_check`.\n *\n * @packageDocumentation\n */\n\nimport { existsSync, renameSync, unlinkSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\nimport {\n DEFAULT_CIPHER,\n type EncryptionCipher,\n encodePassphraseForPragma,\n pragmaSequenceForCipher,\n} from './cipher-config.js';\nimport { loadCipherPeer } from './cipher-peer.js';\nimport { createEncryptedConnection } from './connection.js';\nimport { cipherIntegrityCheck } from './integrity-check.js';\n\n/**\n * Options for {@link encryptDatabase}.\n *\n * @stable\n */\nexport interface EncryptDatabaseOptions {\n /** Path to the existing unencrypted source DB. */\n readonly sourcePath: string;\n /** Path the encrypted output is written to. Must not exist. */\n readonly targetPath: string;\n /** Passphrase for the new encrypted DB. */\n readonly passphrase: string | Buffer;\n /** Cipher selection. Default `'sqlcipher'` (SQLCipher v4 compatible). */\n readonly cipher?: EncryptionCipher;\n /**\n * If `true`, atomically rename `targetPath` -> `sourcePath` after the\n * integrity check passes. The original `sourcePath` is renamed to\n * `${sourcePath}.bak.${timestamp}` so an operator can recover.\n * Default `false` - the CLI does the swap explicitly.\n */\n readonly swap?: boolean;\n /**\n * If `true`, overwrite an existing `targetPath` instead of failing.\n * Default `false`.\n */\n readonly overwriteTarget?: boolean;\n}\n\n/**\n * Result of a successful {@link encryptDatabase} run.\n *\n * @stable\n */\nexport interface EncryptDatabaseResult {\n readonly sourcePath: string;\n readonly targetPath: string;\n readonly cipher: EncryptionCipher;\n readonly integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n readonly swap?: { readonly originalRenamedTo: string };\n}\n\n/**\n * Encrypts an unencrypted SQLite database. Returns once the target\n * file has been written and verified. Throws if the source is missing,\n * the target already exists (and `overwriteTarget` is unset), the\n * cipher peer is missing, or the integrity check fails.\n *\n * @stable\n */\nexport async function encryptDatabase(\n options: EncryptDatabaseOptions,\n): Promise<EncryptDatabaseResult> {\n const sourcePath = absolute(options.sourcePath);\n const targetPath = absolute(options.targetPath);\n const cipher = options.cipher ?? DEFAULT_CIPHER;\n\n if (!existsSync(sourcePath)) {\n throw new Error(`[graphorin/store-sqlite-encrypted] source DB not found: ${sourcePath}`);\n }\n if (sourcePath === targetPath) {\n throw new Error(\n '[graphorin/store-sqlite-encrypted] sourcePath and targetPath must differ. ' +\n 'Pass a temporary targetPath then enable `swap: true` to atomically replace the source.',\n );\n }\n if (existsSync(targetPath)) {\n if (options.overwriteTarget !== true) {\n throw new Error(\n `[graphorin/store-sqlite-encrypted] target DB already exists: ${targetPath}. ` +\n 'Pass `overwriteTarget: true` to replace it.',\n );\n }\n unlinkSync(targetPath);\n }\n\n const Ctor = await loadCipherPeer();\n try {\n // 1+2. Online page-level copy via the driver's backup API\n // (store-05). The old checkpoint-close-then-copyFileSync left a\n // window in which a concurrent writer (a running daemon) could\n // commit WAL frames the byte-copy silently missed; `backup()`\n // is consistent under live writers, preserves rowids (FTS5\n // mappings survive), and includes WAL content.\n const source = new Ctor(sourcePath, { readonly: true });\n try {\n await source.backup(targetPath);\n } finally {\n if (source.open) source.close();\n }\n\n // 3. In-place conversion: cipher pragmas first, then `rekey` -\n // sqlite3mc encrypts a plaintext database in place.\n const target = new Ctor(targetPath);\n try {\n for (const pragma of pragmaSequenceForCipher(cipher)) {\n target.pragma(pragma);\n }\n // sqlite3mc refuses `rekey` in WAL journal mode (real-peer\n // verified) - drop to DELETE for the conversion, restore after.\n target.pragma('journal_mode = DELETE');\n const encodedKey = encodePassphraseForPragma(options.passphrase);\n target.pragma(`rekey = ${encodedKey}`);\n target.pragma('journal_mode = WAL');\n } finally {\n if (target.open) target.close();\n }\n } catch (err) {\n if (existsSync(targetPath)) {\n try {\n unlinkSync(targetPath);\n } catch {\n // best-effort cleanup\n }\n }\n throw new Error(\n `[graphorin/store-sqlite-encrypted] encryption failed: ${(err as Error).message}`,\n { cause: err },\n );\n }\n\n const verify = await createEncryptedConnection({\n path: targetPath,\n skipSqliteVec: true,\n disableWalHardening: true,\n encryption: {\n enabled: true,\n cipher,\n passphraseResolver: async () => options.passphrase,\n },\n });\n let integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n try {\n const result = cipherIntegrityCheck(verify);\n integrityCheck = { ok: result.ok, rows: result.rows };\n } finally {\n verify.close();\n }\n\n if (!integrityCheck.ok) {\n if (existsSync(targetPath)) {\n try {\n unlinkSync(targetPath);\n } catch {\n // best-effort cleanup\n }\n }\n throw new Error(\n '[graphorin/store-sqlite-encrypted] post-encrypt integrity check failed: ' +\n integrityCheck.rows.join('; '),\n );\n }\n\n let swap: { readonly originalRenamedTo: string } | undefined;\n if (options.swap === true) {\n const ts = Date.now();\n const renamedTo = `${sourcePath}.bak.${ts}`;\n renameSync(sourcePath, renamedTo);\n renameSync(targetPath, sourcePath);\n swap = Object.freeze({ originalRenamedTo: renamedTo });\n }\n\n const out: EncryptDatabaseResult = Object.freeze({\n sourcePath,\n targetPath: swap !== undefined ? sourcePath : targetPath,\n cipher,\n integrityCheck: Object.freeze(integrityCheck),\n ...(swap !== undefined ? { swap } : {}),\n });\n return out;\n}\n\nfunction absolute(p: string): string {\n return isAbsolute(p) ? p : resolve(p);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,eAAsB,gBACpB,SACgC;CAChC,MAAM,aAAa,SAAS,QAAQ,WAAW;CAC/C,MAAM,aAAa,SAAS,QAAQ,WAAW;CAC/C,MAAM,SAAS,QAAQ,UAAU;AAEjC,KAAI,CAAC,WAAW,WAAW,CACzB,OAAM,IAAI,MAAM,2DAA2D,aAAa;AAE1F,KAAI,eAAe,WACjB,OAAM,IAAI,MACR,mKAED;AAEH,KAAI,WAAW,WAAW,EAAE;AAC1B,MAAI,QAAQ,oBAAoB,KAC9B,OAAM,IAAI,MACR,gEAAgE,WAAW,iDAE5E;AAEH,aAAW,WAAW;;CAGxB,MAAM,OAAO,MAAM,gBAAgB;AACnC,KAAI;EAOF,MAAM,SAAS,IAAI,KAAK,YAAY,EAAE,UAAU,MAAM,CAAC;AACvD,MAAI;AACF,SAAM,OAAO,OAAO,WAAW;YACvB;AACR,OAAI,OAAO,KAAM,QAAO,OAAO;;EAKjC,MAAM,SAAS,IAAI,KAAK,WAAW;AACnC,MAAI;AACF,QAAK,MAAM,UAAU,wBAAwB,OAAO,CAClD,QAAO,OAAO,OAAO;AAIvB,UAAO,OAAO,wBAAwB;GACtC,MAAM,aAAa,0BAA0B,QAAQ,WAAW;AAChE,UAAO,OAAO,WAAW,aAAa;AACtC,UAAO,OAAO,qBAAqB;YAC3B;AACR,OAAI,OAAO,KAAM,QAAO,OAAO;;UAE1B,KAAK;AACZ,MAAI,WAAW,WAAW,CACxB,KAAI;AACF,cAAW,WAAW;UAChB;AAIV,QAAM,IAAI,MACR,yDAA0D,IAAc,WACxE,EAAE,OAAO,KAAK,CACf;;CAGH,MAAM,SAAS,MAAM,0BAA0B;EAC7C,MAAM;EACN,eAAe;EACf,qBAAqB;EACrB,YAAY;GACV,SAAS;GACT;GACA,oBAAoB,YAAY,QAAQ;GACzC;EACF,CAAC;CACF,IAAIA;AACJ,KAAI;EACF,MAAM,SAAS,qBAAqB,OAAO;AAC3C,mBAAiB;GAAE,IAAI,OAAO;GAAI,MAAM,OAAO;GAAM;WAC7C;AACR,SAAO,OAAO;;AAGhB,KAAI,CAAC,eAAe,IAAI;AACtB,MAAI,WAAW,WAAW,CACxB,KAAI;AACF,cAAW,WAAW;UAChB;AAIV,QAAM,IAAI,MACR,6EACE,eAAe,KAAK,KAAK,KAAK,CACjC;;CAGH,IAAIC;AACJ,KAAI,QAAQ,SAAS,MAAM;EAEzB,MAAM,YAAY,GAAG,WAAW,OADrB,KAAK,KAAK;AAErB,aAAW,YAAY,UAAU;AACjC,aAAW,YAAY,WAAW;AAClC,SAAO,OAAO,OAAO,EAAE,mBAAmB,WAAW,CAAC;;AAUxD,QAPmC,OAAO,OAAO;EAC/C;EACA,YAAY,SAAS,SAAY,aAAa;EAC9C;EACA,gBAAgB,OAAO,OAAO,eAAe;EAC7C,GAAI,SAAS,SAAY,EAAE,MAAM,GAAG,EAAE;EACvC,CAAC;;AAIJ,SAAS,SAAS,GAAmB;AACnC,QAAO,WAAW,EAAE,GAAG,IAAI,QAAQ,EAAE"}
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { RekeyDatabaseOptions, RekeyDatabaseResult, rekeyDatabase } from "./reke
8
8
  //#region src/index.d.ts
9
9
 
10
10
  /**
11
- * @graphorin/store-sqlite-encrypted optional encryption-at-rest
11
+ * @graphorin/store-sqlite-encrypted - optional encryption-at-rest
12
12
  * sub-pack for the Graphorin framework's default SQLite store.
13
13
  *
14
14
  * Installing this package pulls in the cipher peer driver
@@ -19,20 +19,20 @@ import { RekeyDatabaseOptions, RekeyDatabaseResult, rekeyDatabase } from "./reke
19
19
  *
20
20
  * The package exposes:
21
21
  *
22
- * - {@link createEncryptedConnection} convenience wrapper around
22
+ * - {@link createEncryptedConnection} - convenience wrapper around
23
23
  * `openConnection` from `@graphorin/store-sqlite/connection` that
24
24
  * pre-loads the cipher peer.
25
- * - {@link encryptDatabase} converts an unencrypted SQLite file
25
+ * - {@link encryptDatabase} - converts an unencrypted SQLite file
26
26
  * into an encrypted one. Backs `graphorin storage encrypt`.
27
- * - {@link rekeyDatabase} re-keys an already encrypted file. Backs
27
+ * - {@link rekeyDatabase} - re-keys an already encrypted file. Backs
28
28
  * `graphorin storage rekey`.
29
- * - {@link cipherIntegrityCheck} runs `PRAGMA cipher_integrity_
29
+ * - {@link cipherIntegrityCheck} - runs `PRAGMA cipher_integrity_
30
30
  * check`. Used by the triggers daemon's daily verification cron
31
31
  * and the `/v1/health/storage` endpoint.
32
32
  * - {@link DEFAULT_CIPHER}, {@link pragmaSequenceForCipher},
33
- * {@link encodePassphraseForPragma} cipher-config helpers shared
33
+ * {@link encodePassphraseForPragma} - cipher-config helpers shared
34
34
  * by the runners and consumable for advanced setups.
35
- * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError}
35
+ * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError} -
36
36
  * explicit peer-loader surface for callers that want to fail-fast
37
37
  * at startup before opening the DB.
38
38
  *
@@ -45,7 +45,7 @@ import { RekeyDatabaseOptions, RekeyDatabaseResult, rekeyDatabase } from "./reke
45
45
  * @packageDocumentation
46
46
  */
47
47
  /** Canonical version constant. Mirrors the `package.json` version. */
48
- declare const VERSION = "0.5.0";
48
+ declare const VERSION = "0.6.0";
49
49
  //#endregion
50
50
  export { type CipherIntegrityCheckResult, DEFAULT_CIPHER, type EncryptDatabaseOptions, type EncryptDatabaseResult, EncryptedStorePeerMissingError, type EncryptionCipher, type RekeyDatabaseOptions, type RekeyDatabaseResult, VERSION, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, cipherIntegrityCheck, createEncryptedConnection, encodePassphraseForPragma, encryptDatabase, loadCipherPeer, pragmaSequenceForCipher, rekeyDatabase };
51
51
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { rekeyDatabase } from "./rekey.js";
7
7
 
8
8
  //#region src/index.ts
9
9
  /**
10
- * @graphorin/store-sqlite-encrypted optional encryption-at-rest
10
+ * @graphorin/store-sqlite-encrypted - optional encryption-at-rest
11
11
  * sub-pack for the Graphorin framework's default SQLite store.
12
12
  *
13
13
  * Installing this package pulls in the cipher peer driver
@@ -18,20 +18,20 @@ import { rekeyDatabase } from "./rekey.js";
18
18
  *
19
19
  * The package exposes:
20
20
  *
21
- * - {@link createEncryptedConnection} convenience wrapper around
21
+ * - {@link createEncryptedConnection} - convenience wrapper around
22
22
  * `openConnection` from `@graphorin/store-sqlite/connection` that
23
23
  * pre-loads the cipher peer.
24
- * - {@link encryptDatabase} converts an unencrypted SQLite file
24
+ * - {@link encryptDatabase} - converts an unencrypted SQLite file
25
25
  * into an encrypted one. Backs `graphorin storage encrypt`.
26
- * - {@link rekeyDatabase} re-keys an already encrypted file. Backs
26
+ * - {@link rekeyDatabase} - re-keys an already encrypted file. Backs
27
27
  * `graphorin storage rekey`.
28
- * - {@link cipherIntegrityCheck} runs `PRAGMA cipher_integrity_
28
+ * - {@link cipherIntegrityCheck} - runs `PRAGMA cipher_integrity_
29
29
  * check`. Used by the triggers daemon's daily verification cron
30
30
  * and the `/v1/health/storage` endpoint.
31
31
  * - {@link DEFAULT_CIPHER}, {@link pragmaSequenceForCipher},
32
- * {@link encodePassphraseForPragma} cipher-config helpers shared
32
+ * {@link encodePassphraseForPragma} - cipher-config helpers shared
33
33
  * by the runners and consumable for advanced setups.
34
- * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError}
34
+ * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError} -
35
35
  * explicit peer-loader surface for callers that want to fail-fast
36
36
  * at startup before opening the DB.
37
37
  *
@@ -44,7 +44,7 @@ import { rekeyDatabase } from "./rekey.js";
44
44
  * @packageDocumentation
45
45
  */
46
46
  /** Canonical version constant. Mirrors the `package.json` version. */
47
- const VERSION = "0.5.0";
47
+ const VERSION = "0.6.0";
48
48
 
49
49
  //#endregion
50
50
  export { DEFAULT_CIPHER, EncryptedStorePeerMissingError, VERSION, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, cipherIntegrityCheck, createEncryptedConnection, encodePassphraseForPragma, encryptDatabase, loadCipherPeer, pragmaSequenceForCipher, rekeyDatabase };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/store-sqlite-encrypted optional encryption-at-rest\n * sub-pack for the Graphorin framework's default SQLite store.\n *\n * Installing this package pulls in the cipher peer driver\n * (`better-sqlite3-multiple-ciphers@^12.9.0`), which is a drop-in fork\n * of `better-sqlite3` that bundles the SQLite3MultipleCiphers\n * extension (SQLCipher v4 / wxSQLite3 / AES-256-CBC / AES-128-CBC /\n * RC4 cipher modes).\n *\n * The package exposes:\n *\n * - {@link createEncryptedConnection} convenience wrapper around\n * `openConnection` from `@graphorin/store-sqlite/connection` that\n * pre-loads the cipher peer.\n * - {@link encryptDatabase} converts an unencrypted SQLite file\n * into an encrypted one. Backs `graphorin storage encrypt`.\n * - {@link rekeyDatabase} re-keys an already encrypted file. Backs\n * `graphorin storage rekey`.\n * - {@link cipherIntegrityCheck} runs `PRAGMA cipher_integrity_\n * check`. Used by the triggers daemon's daily verification cron\n * and the `/v1/health/storage` endpoint.\n * - {@link DEFAULT_CIPHER}, {@link pragmaSequenceForCipher},\n * {@link encodePassphraseForPragma} cipher-config helpers shared\n * by the runners and consumable for advanced setups.\n * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError} —\n * explicit peer-loader surface for callers that want to fail-fast\n * at startup before opening the DB.\n *\n * Defaults follow ADR-030 / DEC-129:\n * - Cipher: `'sqlcipher'` (SQLCipher v4 compatible, `legacy=4`).\n * - Default OFF; opt-in through `graphorin init --encrypted`.\n * - audit.db is ALWAYS encrypted regardless of this opt-in\n * (DEC-124); this package satisfies that requirement too.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.5.0';\n\nexport {\n DEFAULT_CIPHER,\n type EncryptionCipher,\n encodePassphraseForPragma,\n pragmaSequenceForCipher,\n} from './cipher-config.js';\nexport {\n _resetCipherPeerCacheForTesting,\n _setCipherPeerForTesting,\n EncryptedStorePeerMissingError,\n loadCipherPeer,\n} from './cipher-peer.js';\nexport { createEncryptedConnection } from './connection.js';\nexport {\n type EncryptDatabaseOptions,\n type EncryptDatabaseResult,\n encryptDatabase,\n} from './encrypt.js';\nexport { type CipherIntegrityCheckResult, cipherIntegrityCheck } from './integrity-check.js';\nexport {\n type RekeyDatabaseOptions,\n type RekeyDatabaseResult,\n rekeyDatabase,\n} from './rekey.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,MAAa,UAAU"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/store-sqlite-encrypted - optional encryption-at-rest\n * sub-pack for the Graphorin framework's default SQLite store.\n *\n * Installing this package pulls in the cipher peer driver\n * (`better-sqlite3-multiple-ciphers@^12.9.0`), which is a drop-in fork\n * of `better-sqlite3` that bundles the SQLite3MultipleCiphers\n * extension (SQLCipher v4 / wxSQLite3 / AES-256-CBC / AES-128-CBC /\n * RC4 cipher modes).\n *\n * The package exposes:\n *\n * - {@link createEncryptedConnection} - convenience wrapper around\n * `openConnection` from `@graphorin/store-sqlite/connection` that\n * pre-loads the cipher peer.\n * - {@link encryptDatabase} - converts an unencrypted SQLite file\n * into an encrypted one. Backs `graphorin storage encrypt`.\n * - {@link rekeyDatabase} - re-keys an already encrypted file. Backs\n * `graphorin storage rekey`.\n * - {@link cipherIntegrityCheck} - runs `PRAGMA cipher_integrity_\n * check`. Used by the triggers daemon's daily verification cron\n * and the `/v1/health/storage` endpoint.\n * - {@link DEFAULT_CIPHER}, {@link pragmaSequenceForCipher},\n * {@link encodePassphraseForPragma} - cipher-config helpers shared\n * by the runners and consumable for advanced setups.\n * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError} -\n * explicit peer-loader surface for callers that want to fail-fast\n * at startup before opening the DB.\n *\n * Defaults follow ADR-030 / DEC-129:\n * - Cipher: `'sqlcipher'` (SQLCipher v4 compatible, `legacy=4`).\n * - Default OFF; opt-in through `graphorin init --encrypted`.\n * - audit.db is ALWAYS encrypted regardless of this opt-in\n * (DEC-124); this package satisfies that requirement too.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.6.0';\n\nexport {\n DEFAULT_CIPHER,\n type EncryptionCipher,\n encodePassphraseForPragma,\n pragmaSequenceForCipher,\n} from './cipher-config.js';\nexport {\n _resetCipherPeerCacheForTesting,\n _setCipherPeerForTesting,\n EncryptedStorePeerMissingError,\n loadCipherPeer,\n} from './cipher-peer.js';\nexport { createEncryptedConnection } from './connection.js';\nexport {\n type EncryptDatabaseOptions,\n type EncryptDatabaseResult,\n encryptDatabase,\n} from './encrypt.js';\nexport { type CipherIntegrityCheckResult, cipherIntegrityCheck } from './integrity-check.js';\nexport {\n type RekeyDatabaseOptions,\n type RekeyDatabaseResult,\n rekeyDatabase,\n} from './rekey.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,MAAa,UAAU"}
@@ -18,7 +18,7 @@ interface CipherIntegrityCheckResult {
18
18
  /**
19
19
  * Runs `PRAGMA integrity_check` against the provided connection. The
20
20
  * connection MUST already be open with the cipher key applied
21
- * (typically via {@link createEncryptedConnection}) a wrong key
21
+ * (typically via {@link createEncryptedConnection}) - a wrong key
22
22
  * surfaces as an open/read error before the pragma runs.
23
23
  *
24
24
  * The pragma is read-only so it is safe to run from a triggers daemon
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Runs `PRAGMA integrity_check` against the provided connection. The
4
4
  * connection MUST already be open with the cipher key applied
5
- * (typically via {@link createEncryptedConnection}) a wrong key
5
+ * (typically via {@link createEncryptedConnection}) - a wrong key
6
6
  * surfaces as an open/read error before the pragma runs.
7
7
  *
8
8
  * The pragma is read-only so it is safe to run from a triggers daemon
@@ -1 +1 @@
1
- {"version":3,"file":"integrity-check.js","names":["out: string[]"],"sources":["../src/integrity-check.ts"],"sourcesContent":["/**\n * Integrity-check runner for encrypted databases. sqlite3mc ships no\n * `PRAGMA cipher_integrity_check` (CS-7 the old call returned an\n * empty row-set on every real run, so `ok` was always false); the\n * standard `PRAGMA integrity_check` works through the cipher layer\n * once the connection is keyed, which is exactly the property the\n * CLI / health endpoint needs (\"can this DB be read with this key,\n * and is it internally consistent\").\n *\n * @packageDocumentation\n */\n\nimport type { SqliteConnection } from '@graphorin/store-sqlite/connection';\n\n/**\n * Result of {@link cipherIntegrityCheck}.\n *\n * @stable\n */\nexport interface CipherIntegrityCheckResult {\n /** `true` when SQLite reported a single `'ok'` row. */\n readonly ok: boolean;\n /** Raw rows returned by `PRAGMA integrity_check`. */\n readonly rows: ReadonlyArray<string>;\n /** Wall-clock duration of the pragma call in milliseconds. */\n readonly durationMs: number;\n}\n\n/**\n * Runs `PRAGMA integrity_check` against the provided connection. The\n * connection MUST already be open with the cipher key applied\n * (typically via {@link createEncryptedConnection}) a wrong key\n * surfaces as an open/read error before the pragma runs.\n *\n * The pragma is read-only so it is safe to run from a triggers daemon\n * cron without taking a write lock.\n *\n * @stable\n */\nexport function cipherIntegrityCheck(conn: SqliteConnection): CipherIntegrityCheckResult {\n const started = performance.now();\n const raw = conn.pragma('integrity_check');\n const durationMs = performance.now() - started;\n const rows = normalizeRows(raw);\n const ok = rows.length === 1 && rows[0] === 'ok';\n return Object.freeze({ ok, rows: Object.freeze(rows), durationMs });\n}\n\nfunction normalizeRows(raw: unknown): string[] {\n if (!Array.isArray(raw)) return [];\n const out: string[] = [];\n for (const row of raw as ReadonlyArray<unknown>) {\n if (typeof row === 'string') {\n out.push(row);\n } else if (row !== null && typeof row === 'object') {\n const obj = row as Record<string, unknown>;\n const value = obj.cipher_integrity_check ?? obj.integrity_check ?? Object.values(obj)[0];\n if (typeof value === 'string') out.push(value);\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;AAuCA,SAAgB,qBAAqB,MAAoD;CACvF,MAAM,UAAU,YAAY,KAAK;CACjC,MAAM,MAAM,KAAK,OAAO,kBAAkB;CAC1C,MAAM,aAAa,YAAY,KAAK,GAAG;CACvC,MAAM,OAAO,cAAc,IAAI;CAC/B,MAAM,KAAK,KAAK,WAAW,KAAK,KAAK,OAAO;AAC5C,QAAO,OAAO,OAAO;EAAE;EAAI,MAAM,OAAO,OAAO,KAAK;EAAE;EAAY,CAAC;;AAGrE,SAAS,cAAc,KAAwB;AAC7C,KAAI,CAAC,MAAM,QAAQ,IAAI,CAAE,QAAO,EAAE;CAClC,MAAMA,MAAgB,EAAE;AACxB,MAAK,MAAM,OAAO,IAChB,KAAI,OAAO,QAAQ,SACjB,KAAI,KAAK,IAAI;UACJ,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAClD,MAAM,MAAM;EACZ,MAAM,QAAQ,IAAI,0BAA0B,IAAI,mBAAmB,OAAO,OAAO,IAAI,CAAC;AACtF,MAAI,OAAO,UAAU,SAAU,KAAI,KAAK,MAAM;;AAGlD,QAAO"}
1
+ {"version":3,"file":"integrity-check.js","names":["out: string[]"],"sources":["../src/integrity-check.ts"],"sourcesContent":["/**\n * Integrity-check runner for encrypted databases. sqlite3mc ships no\n * `PRAGMA cipher_integrity_check` (CS-7 - the old call returned an\n * empty row-set on every real run, so `ok` was always false); the\n * standard `PRAGMA integrity_check` works through the cipher layer\n * once the connection is keyed, which is exactly the property the\n * CLI / health endpoint needs (\"can this DB be read with this key,\n * and is it internally consistent\").\n *\n * @packageDocumentation\n */\n\nimport type { SqliteConnection } from '@graphorin/store-sqlite/connection';\n\n/**\n * Result of {@link cipherIntegrityCheck}.\n *\n * @stable\n */\nexport interface CipherIntegrityCheckResult {\n /** `true` when SQLite reported a single `'ok'` row. */\n readonly ok: boolean;\n /** Raw rows returned by `PRAGMA integrity_check`. */\n readonly rows: ReadonlyArray<string>;\n /** Wall-clock duration of the pragma call in milliseconds. */\n readonly durationMs: number;\n}\n\n/**\n * Runs `PRAGMA integrity_check` against the provided connection. The\n * connection MUST already be open with the cipher key applied\n * (typically via {@link createEncryptedConnection}) - a wrong key\n * surfaces as an open/read error before the pragma runs.\n *\n * The pragma is read-only so it is safe to run from a triggers daemon\n * cron without taking a write lock.\n *\n * @stable\n */\nexport function cipherIntegrityCheck(conn: SqliteConnection): CipherIntegrityCheckResult {\n const started = performance.now();\n const raw = conn.pragma('integrity_check');\n const durationMs = performance.now() - started;\n const rows = normalizeRows(raw);\n const ok = rows.length === 1 && rows[0] === 'ok';\n return Object.freeze({ ok, rows: Object.freeze(rows), durationMs });\n}\n\nfunction normalizeRows(raw: unknown): string[] {\n if (!Array.isArray(raw)) return [];\n const out: string[] = [];\n for (const row of raw as ReadonlyArray<unknown>) {\n if (typeof row === 'string') {\n out.push(row);\n } else if (row !== null && typeof row === 'object') {\n const obj = row as Record<string, unknown>;\n const value = obj.cipher_integrity_check ?? obj.integrity_check ?? Object.values(obj)[0];\n if (typeof value === 'string') out.push(value);\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;AAuCA,SAAgB,qBAAqB,MAAoD;CACvF,MAAM,UAAU,YAAY,KAAK;CACjC,MAAM,MAAM,KAAK,OAAO,kBAAkB;CAC1C,MAAM,aAAa,YAAY,KAAK,GAAG;CACvC,MAAM,OAAO,cAAc,IAAI;CAC/B,MAAM,KAAK,KAAK,WAAW,KAAK,KAAK,OAAO;AAC5C,QAAO,OAAO,OAAO;EAAE;EAAI,MAAM,OAAO,OAAO,KAAK;EAAE;EAAY,CAAC;;AAGrE,SAAS,cAAc,KAAwB;AAC7C,KAAI,CAAC,MAAM,QAAQ,IAAI,CAAE,QAAO,EAAE;CAClC,MAAMA,MAAgB,EAAE;AACxB,MAAK,MAAM,OAAO,IAChB,KAAI,OAAO,QAAQ,SACjB,KAAI,KAAK,IAAI;UACJ,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAClD,MAAM,MAAM;EACZ,MAAM,QAAQ,IAAI,0BAA0B,IAAI,mBAAmB,OAAO,OAAO,IAAI,CAAC;AACtF,MAAI,OAAO,UAAU,SAAU,KAAI,KAAK,MAAM;;AAGlD,QAAO"}
package/dist/rekey.js CHANGED
@@ -7,7 +7,7 @@ import { isAbsolute, resolve } from "node:path";
7
7
 
8
8
  //#region src/rekey.ts
9
9
  /**
10
- * `rekeyDatabase` re-keys an already encrypted SQLite database. The
10
+ * `rekeyDatabase` - re-keys an already encrypted SQLite database. The
11
11
  * runner backs `graphorin storage rekey`.
12
12
  *
13
13
  * Strategy: open the DB through the cipher peer with the **old** key,
package/dist/rekey.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"rekey.js","names":["integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> }"],"sources":["../src/rekey.ts"],"sourcesContent":["/**\n * `rekeyDatabase` re-keys an already encrypted SQLite database. The\n * runner backs `graphorin storage rekey`.\n *\n * Strategy: open the DB through the cipher peer with the **old** key,\n * then issue `PRAGMA rekey = <new>;`. The cipher peer rewrites every\n * page transactionally. After the pragma we re-open the DB with the\n * new key and run `PRAGMA cipher_integrity_check` to confirm the\n * rotation succeeded.\n *\n * @packageDocumentation\n */\n\nimport { existsSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\nimport {\n DEFAULT_CIPHER,\n type EncryptionCipher,\n encodePassphraseForPragma,\n} from './cipher-config.js';\nimport { loadCipherPeer } from './cipher-peer.js';\nimport { createEncryptedConnection } from './connection.js';\nimport { cipherIntegrityCheck } from './integrity-check.js';\n\n/**\n * Options for {@link rekeyDatabase}.\n *\n * @stable\n */\nexport interface RekeyDatabaseOptions {\n /** Path to the encrypted DB. */\n readonly path: string;\n /** Existing passphrase the DB is currently encrypted with. */\n readonly oldPassphrase: string | Buffer;\n /** New passphrase to apply. */\n readonly newPassphrase: string | Buffer;\n /** Cipher selection. Default `'sqlcipher'`. */\n readonly cipher?: EncryptionCipher;\n}\n\n/**\n * Result of a successful {@link rekeyDatabase} run.\n *\n * @stable\n */\nexport interface RekeyDatabaseResult {\n readonly path: string;\n readonly cipher: EncryptionCipher;\n readonly integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n}\n\n/**\n * Re-keys an encrypted SQLite database. Throws if the file is missing,\n * the cipher peer cannot be loaded, the old passphrase is wrong (the\n * cipher peer raises `SQLITE_NOTADB` on the first read), or the\n * post-rekey integrity check fails.\n *\n * @stable\n */\nexport async function rekeyDatabase(options: RekeyDatabaseOptions): Promise<RekeyDatabaseResult> {\n const path = absolute(options.path);\n const cipher = options.cipher ?? DEFAULT_CIPHER;\n\n if (!existsSync(path)) {\n throw new Error(`[graphorin/store-sqlite-encrypted] DB not found: ${path}`);\n }\n\n await loadCipherPeer();\n\n const conn = await createEncryptedConnection({\n path,\n skipSqliteVec: true,\n disableWalHardening: true,\n encryption: {\n enabled: true,\n cipher,\n passphraseResolver: async () => options.oldPassphrase,\n },\n });\n try {\n // Sanity-read forces the cipher peer to verify the old key before\n // we rewrite pages with the new one.\n conn.pragma('user_version');\n // sqlite3mc refuses `rekey` in WAL journal mode (real-peer\n // verified) drop to DELETE for the rotation, restore after.\n conn.pragma('journal_mode = DELETE');\n const newEncoded = encodePassphraseForPragma(options.newPassphrase);\n conn.pragma(`rekey = ${newEncoded}`);\n conn.pragma('journal_mode = WAL');\n } catch (err) {\n if (conn.raw().open) conn.close();\n throw new Error(`[graphorin/store-sqlite-encrypted] rekey failed: ${(err as Error).message}`, {\n cause: err,\n });\n } finally {\n if (conn.raw().open) conn.close();\n }\n\n const verify = await createEncryptedConnection({\n path,\n skipSqliteVec: true,\n disableWalHardening: true,\n encryption: {\n enabled: true,\n cipher,\n passphraseResolver: async () => options.newPassphrase,\n },\n });\n let integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n try {\n const result = cipherIntegrityCheck(verify);\n integrityCheck = { ok: result.ok, rows: result.rows };\n } finally {\n verify.close();\n }\n\n if (!integrityCheck.ok) {\n throw new Error(\n '[graphorin/store-sqlite-encrypted] post-rekey integrity check failed: ' +\n integrityCheck.rows.join('; '),\n );\n }\n\n return Object.freeze({ path, cipher, integrityCheck: Object.freeze(integrityCheck) });\n}\n\nfunction absolute(p: string): string {\n return isAbsolute(p) ? p : resolve(p);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,eAAsB,cAAc,SAA6D;CAC/F,MAAM,OAAO,SAAS,QAAQ,KAAK;CACnC,MAAM,SAAS,QAAQ,UAAU;AAEjC,KAAI,CAAC,WAAW,KAAK,CACnB,OAAM,IAAI,MAAM,oDAAoD,OAAO;AAG7E,OAAM,gBAAgB;CAEtB,MAAM,OAAO,MAAM,0BAA0B;EAC3C;EACA,eAAe;EACf,qBAAqB;EACrB,YAAY;GACV,SAAS;GACT;GACA,oBAAoB,YAAY,QAAQ;GACzC;EACF,CAAC;AACF,KAAI;AAGF,OAAK,OAAO,eAAe;AAG3B,OAAK,OAAO,wBAAwB;EACpC,MAAM,aAAa,0BAA0B,QAAQ,cAAc;AACnE,OAAK,OAAO,WAAW,aAAa;AACpC,OAAK,OAAO,qBAAqB;UAC1B,KAAK;AACZ,MAAI,KAAK,KAAK,CAAC,KAAM,MAAK,OAAO;AACjC,QAAM,IAAI,MAAM,oDAAqD,IAAc,WAAW,EAC5F,OAAO,KACR,CAAC;WACM;AACR,MAAI,KAAK,KAAK,CAAC,KAAM,MAAK,OAAO;;CAGnC,MAAM,SAAS,MAAM,0BAA0B;EAC7C;EACA,eAAe;EACf,qBAAqB;EACrB,YAAY;GACV,SAAS;GACT;GACA,oBAAoB,YAAY,QAAQ;GACzC;EACF,CAAC;CACF,IAAIA;AACJ,KAAI;EACF,MAAM,SAAS,qBAAqB,OAAO;AAC3C,mBAAiB;GAAE,IAAI,OAAO;GAAI,MAAM,OAAO;GAAM;WAC7C;AACR,SAAO,OAAO;;AAGhB,KAAI,CAAC,eAAe,GAClB,OAAM,IAAI,MACR,2EACE,eAAe,KAAK,KAAK,KAAK,CACjC;AAGH,QAAO,OAAO,OAAO;EAAE;EAAM;EAAQ,gBAAgB,OAAO,OAAO,eAAe;EAAE,CAAC;;AAGvF,SAAS,SAAS,GAAmB;AACnC,QAAO,WAAW,EAAE,GAAG,IAAI,QAAQ,EAAE"}
1
+ {"version":3,"file":"rekey.js","names":["integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> }"],"sources":["../src/rekey.ts"],"sourcesContent":["/**\n * `rekeyDatabase` - re-keys an already encrypted SQLite database. The\n * runner backs `graphorin storage rekey`.\n *\n * Strategy: open the DB through the cipher peer with the **old** key,\n * then issue `PRAGMA rekey = <new>;`. The cipher peer rewrites every\n * page transactionally. After the pragma we re-open the DB with the\n * new key and run `PRAGMA cipher_integrity_check` to confirm the\n * rotation succeeded.\n *\n * @packageDocumentation\n */\n\nimport { existsSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\nimport {\n DEFAULT_CIPHER,\n type EncryptionCipher,\n encodePassphraseForPragma,\n} from './cipher-config.js';\nimport { loadCipherPeer } from './cipher-peer.js';\nimport { createEncryptedConnection } from './connection.js';\nimport { cipherIntegrityCheck } from './integrity-check.js';\n\n/**\n * Options for {@link rekeyDatabase}.\n *\n * @stable\n */\nexport interface RekeyDatabaseOptions {\n /** Path to the encrypted DB. */\n readonly path: string;\n /** Existing passphrase the DB is currently encrypted with. */\n readonly oldPassphrase: string | Buffer;\n /** New passphrase to apply. */\n readonly newPassphrase: string | Buffer;\n /** Cipher selection. Default `'sqlcipher'`. */\n readonly cipher?: EncryptionCipher;\n}\n\n/**\n * Result of a successful {@link rekeyDatabase} run.\n *\n * @stable\n */\nexport interface RekeyDatabaseResult {\n readonly path: string;\n readonly cipher: EncryptionCipher;\n readonly integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n}\n\n/**\n * Re-keys an encrypted SQLite database. Throws if the file is missing,\n * the cipher peer cannot be loaded, the old passphrase is wrong (the\n * cipher peer raises `SQLITE_NOTADB` on the first read), or the\n * post-rekey integrity check fails.\n *\n * @stable\n */\nexport async function rekeyDatabase(options: RekeyDatabaseOptions): Promise<RekeyDatabaseResult> {\n const path = absolute(options.path);\n const cipher = options.cipher ?? DEFAULT_CIPHER;\n\n if (!existsSync(path)) {\n throw new Error(`[graphorin/store-sqlite-encrypted] DB not found: ${path}`);\n }\n\n await loadCipherPeer();\n\n const conn = await createEncryptedConnection({\n path,\n skipSqliteVec: true,\n disableWalHardening: true,\n encryption: {\n enabled: true,\n cipher,\n passphraseResolver: async () => options.oldPassphrase,\n },\n });\n try {\n // Sanity-read forces the cipher peer to verify the old key before\n // we rewrite pages with the new one.\n conn.pragma('user_version');\n // sqlite3mc refuses `rekey` in WAL journal mode (real-peer\n // verified) - drop to DELETE for the rotation, restore after.\n conn.pragma('journal_mode = DELETE');\n const newEncoded = encodePassphraseForPragma(options.newPassphrase);\n conn.pragma(`rekey = ${newEncoded}`);\n conn.pragma('journal_mode = WAL');\n } catch (err) {\n if (conn.raw().open) conn.close();\n throw new Error(`[graphorin/store-sqlite-encrypted] rekey failed: ${(err as Error).message}`, {\n cause: err,\n });\n } finally {\n if (conn.raw().open) conn.close();\n }\n\n const verify = await createEncryptedConnection({\n path,\n skipSqliteVec: true,\n disableWalHardening: true,\n encryption: {\n enabled: true,\n cipher,\n passphraseResolver: async () => options.newPassphrase,\n },\n });\n let integrityCheck: { readonly ok: boolean; readonly rows: ReadonlyArray<string> };\n try {\n const result = cipherIntegrityCheck(verify);\n integrityCheck = { ok: result.ok, rows: result.rows };\n } finally {\n verify.close();\n }\n\n if (!integrityCheck.ok) {\n throw new Error(\n '[graphorin/store-sqlite-encrypted] post-rekey integrity check failed: ' +\n integrityCheck.rows.join('; '),\n );\n }\n\n return Object.freeze({ path, cipher, integrityCheck: Object.freeze(integrityCheck) });\n}\n\nfunction absolute(p: string): string {\n return isAbsolute(p) ? p : resolve(p);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,eAAsB,cAAc,SAA6D;CAC/F,MAAM,OAAO,SAAS,QAAQ,KAAK;CACnC,MAAM,SAAS,QAAQ,UAAU;AAEjC,KAAI,CAAC,WAAW,KAAK,CACnB,OAAM,IAAI,MAAM,oDAAoD,OAAO;AAG7E,OAAM,gBAAgB;CAEtB,MAAM,OAAO,MAAM,0BAA0B;EAC3C;EACA,eAAe;EACf,qBAAqB;EACrB,YAAY;GACV,SAAS;GACT;GACA,oBAAoB,YAAY,QAAQ;GACzC;EACF,CAAC;AACF,KAAI;AAGF,OAAK,OAAO,eAAe;AAG3B,OAAK,OAAO,wBAAwB;EACpC,MAAM,aAAa,0BAA0B,QAAQ,cAAc;AACnE,OAAK,OAAO,WAAW,aAAa;AACpC,OAAK,OAAO,qBAAqB;UAC1B,KAAK;AACZ,MAAI,KAAK,KAAK,CAAC,KAAM,MAAK,OAAO;AACjC,QAAM,IAAI,MAAM,oDAAqD,IAAc,WAAW,EAC5F,OAAO,KACR,CAAC;WACM;AACR,MAAI,KAAK,KAAK,CAAC,KAAM,MAAK,OAAO;;CAGnC,MAAM,SAAS,MAAM,0BAA0B;EAC7C;EACA,eAAe;EACf,qBAAqB;EACrB,YAAY;GACV,SAAS;GACT;GACA,oBAAoB,YAAY,QAAQ;GACzC;EACF,CAAC;CACF,IAAIA;AACJ,KAAI;EACF,MAAM,SAAS,qBAAqB,OAAO;AAC3C,mBAAiB;GAAE,IAAI,OAAO;GAAI,MAAM,OAAO;GAAM;WAC7C;AACR,SAAO,OAAO;;AAGhB,KAAI,CAAC,eAAe,GAClB,OAAM,IAAI,MACR,2EACE,eAAe,KAAK,KAAK,KAAK,CACjC;AAGH,QAAO,OAAO,OAAO;EAAE;EAAM;EAAQ,gBAAgB,OAAO,OAAO,eAAe;EAAE,CAAC;;AAGvF,SAAS,SAAS,GAAmB;AACnC,QAAO,WAAW,EAAE,GAAG,IAAI,QAAQ,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/store-sqlite-encrypted",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Optional encryption-at-rest sub-pack for the Graphorin framework's default SQLite store. Pulls in `better-sqlite3-multiple-ciphers@^12.9.0` (a drop-in fork of `better-sqlite3` that bundles SQLite3MultipleCiphers / SQLCipher v4), exposes the `encryptDatabase` / `rekeyDatabase` / `cipherIntegrityCheck` runners that back `graphorin storage encrypt` / `graphorin storage rekey` / `graphorin storage status`, and provides a `createEncryptedConnection` convenience wrapper around `@graphorin/store-sqlite/connection` for callers that bypass the CLI. Default cipher: `sqlcipher` (SQLCipher v4 compatible). Opt-in alternatives: `chacha20` (the peer default), `aes256cbc`, `aes128cbc`, `rc4`. Default OFF; opt-in through `graphorin init --encrypted` per ADR-030 / DEC-129. Created and maintained by Oleksiy Stepurenko.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -48,7 +48,7 @@
48
48
  "LICENSE"
49
49
  ],
50
50
  "dependencies": {
51
- "@graphorin/store-sqlite": "0.5.0"
51
+ "@graphorin/store-sqlite": "0.6.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "better-sqlite3-multiple-ciphers": "^12.9.0"
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/better-sqlite3": "^7.6.13",
67
- "@graphorin/security": "0.5.0"
67
+ "@graphorin/security": "0.6.0"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsdown",