@graphorin/store-sqlite-encrypted 0.5.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 ADDED
@@ -0,0 +1,11 @@
1
+ # @graphorin/store-sqlite-encrypted
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial release of the optional encryption-at-rest sub-pack for the Graphorin
8
+ framework's default SQLite store. Pulls in
9
+ `better-sqlite3-multiple-ciphers@^12.9.0` and exposes the `encryptDatabase`,
10
+ `rekeyDatabase`, `cipherIntegrityCheck`, and `createEncryptedConnection`
11
+ helpers consumed by the `graphorin storage` CLI.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksiy Stepurenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @graphorin/store-sqlite-encrypted
2
+
3
+ > Optional encryption-at-rest sub-pack for the [Graphorin](https://github.com/o-stepper/graphorin)
4
+ > framework's default SQLite store. Pulls in
5
+ > `better-sqlite3-multiple-ciphers@^12.9.0` (a drop-in fork of `better-sqlite3`
6
+ > that bundles the SQLite3MultipleCiphers extension) and exposes the
7
+ > encrypt / rekey / integrity-check runners that back the `graphorin storage`
8
+ > CLI subcommand group.
9
+ >
10
+ > Project Graphorin · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko ·
11
+ > <https://github.com/o-stepper/graphorin>
12
+
13
+ ---
14
+
15
+ ## Status
16
+
17
+ - **Published:** v0.5.0 (optional sub-pack)
18
+ - **Default cipher:** `sqlcipher` (SQLCipher v4 compatible, `legacy=4`)
19
+ - **Defaults:** encryption-at-rest is **OFF by default**. Opt in through
20
+ `graphorin init --encrypted`.
21
+ - **`audit.db`:** ALWAYS encrypted regardless of this opt-in. Installing
22
+ this sub-pack is the only supported way to satisfy that requirement on
23
+ fresh installations.
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pnpm add @graphorin/store-sqlite-encrypted
31
+ # Pulls in better-sqlite3-multiple-ciphers@^12.9.0 as a peer dep.
32
+ ```
33
+
34
+ The cipher peer ships prebuilt binaries for every Node 22+ target
35
+ (macOS arm64/x64, Linux x64/arm/arm64 with both glibc and musl, Windows
36
+ x86/x64/arm64) so there is no compile step on `pnpm install` for the
37
+ default platforms.
38
+
39
+ If you are on a platform without a prebuilt binary you will need a C++
40
+ toolchain and Python 3 available; consult the upstream
41
+ [`better-sqlite3-multiple-ciphers`](https://www.npmjs.com/package/better-sqlite3-multiple-ciphers)
42
+ README for details.
43
+
44
+ ---
45
+
46
+ ## Usage
47
+
48
+ ### One-shot encryption migration (CLI flow)
49
+
50
+ ```bash
51
+ # 1. Stop any running graphorin server / writers.
52
+ graphorin stop
53
+
54
+ # 2. Back up the unencrypted DB.
55
+ cp ~/.graphorin/data.db ~/.graphorin/data.db.backup-$(date +%Y%m%d-%H%M)
56
+
57
+ # 3. Encrypt + verify (passphrase resolved from a SecretRef chain).
58
+ graphorin storage encrypt --passphrase-from keyring:graphorin_db_passphrase
59
+
60
+ # 4. Update the config and restart.
61
+ graphorin config set storage.encryption.enabled true
62
+ graphorin config set storage.encryption.passphraseRef keyring:graphorin_db_passphrase
63
+ graphorin start
64
+
65
+ # 5. After a verification window (default 7 days) drop the backup.
66
+ graphorin storage cleanup-backups --older-than 7d
67
+ ```
68
+
69
+ ### Programmatic use
70
+
71
+ ```ts
72
+ import {
73
+ createEncryptedConnection,
74
+ encryptDatabase,
75
+ rekeyDatabase,
76
+ cipherIntegrityCheck,
77
+ } from '@graphorin/store-sqlite-encrypted';
78
+
79
+ // Open an existing encrypted DB.
80
+ const conn = await createEncryptedConnection({
81
+ path: '/var/lib/graphorin/data.db',
82
+ encryption: {
83
+ enabled: true,
84
+ passphraseResolver: async () => process.env.GRAPHORIN_DB_PASSPHRASE!,
85
+ },
86
+ });
87
+
88
+ // Verify the cipher header on startup or via a triggers cron.
89
+ const integrity = cipherIntegrityCheck(conn);
90
+ if (!integrity.ok) {
91
+ throw new Error(`cipher_integrity_check failed: ${integrity.rows.join('; ')}`);
92
+ }
93
+
94
+ // One-shot migration of an unencrypted file into a new encrypted one.
95
+ await encryptDatabase({
96
+ sourcePath: '/var/lib/graphorin/data.db',
97
+ targetPath: '/var/lib/graphorin/data.db.encrypted',
98
+ passphrase: process.env.GRAPHORIN_DB_PASSPHRASE!,
99
+ swap: true, // atomic rename + .bak.<timestamp> kept for recovery
100
+ });
101
+
102
+ // Rotate the passphrase in place (PRAGMA rekey under the hood).
103
+ await rekeyDatabase({
104
+ path: '/var/lib/graphorin/data.db',
105
+ oldPassphrase: process.env.OLD_PASSPHRASE!,
106
+ newPassphrase: process.env.NEW_PASSPHRASE!,
107
+ });
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Cipher selection
113
+
114
+ The default cipher is `'sqlcipher'` with the `legacy=4` parameter set —
115
+ SQLCipher v4 compatible — chosen for ecosystem tooling compatibility
116
+ (DB Browser for SQLCipher, `sqlcipher` CLI, GUI inspectors). Other
117
+ cipher modes shipped by the cipher peer are accepted; pass them via the
118
+ `cipher` option:
119
+
120
+ | Cipher | Notes |
121
+ |---------------|-----------------------------------------------------------------------------|
122
+ | `'sqlcipher'` | Default. AES-256-CBC + HMAC-SHA1 + Argon2id KDF. SQLCipher v4 compatible. |
123
+ | `'chacha20'` | The cipher peer's own default (ChaCha20-Poly1305). |
124
+ | `'aes256cbc'` | Raw AES-256-CBC without the SQLCipher HMAC envelope. |
125
+ | `'aes128cbc'` | AES-128-CBC variant. |
126
+ | `'rc4'` | Legacy interop only. Do **not** use for new deployments. |
127
+
128
+ ---
129
+
130
+ ## Operational notes
131
+
132
+ - **Passphrase loss = total data loss.** The cipher peer cannot
133
+ recover an encrypted DB without the passphrase. Store the passphrase
134
+ in a keyring or vault (the `graphorin storage encrypt` CLI prompts
135
+ for this).
136
+ - **WAL housekeeping bytes** are visible to an attacker on file leak
137
+ (page numbers, lengths). Row contents are not. See ADR-030 § 5
138
+ for the threat-model nuance.
139
+ - **Performance overhead** is typically 5–15 % on OLTP workloads (read
140
+ / write of small rows). The triggers cron that runs the daily
141
+ `cipher_integrity_check` is a read-only pragma so it does not
142
+ block writers.
143
+ - **Edge runtimes** (Cloudflare Workers, Vercel Edge) are **not
144
+ supported**. The cipher peer is a native addon. For edge deployments
145
+ use `@graphorin/store-libsql` (Turso encryption is a separate story).
146
+
147
+ ---
148
+
149
+ ## Related decisions
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).
153
+
154
+ ---
155
+
156
+ ## License
157
+
158
+ MIT © 2026 Oleksiy Stepurenko
159
+
160
+ ---
161
+
162
+ **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
@@ -0,0 +1,37 @@
1
+ import { EncryptionCipher } from "@graphorin/store-sqlite/encryption";
2
+
3
+ //#region src/cipher-config.d.ts
4
+
5
+ /**
6
+ * Default cipher. Matches ADR-030 § 2 — SQLCipher v4 compatible
7
+ * (AES-256-CBC + HMAC-SHA1, `legacy=4` parameter set).
8
+ *
9
+ * @stable
10
+ */
11
+ declare const DEFAULT_CIPHER: EncryptionCipher;
12
+ /**
13
+ * Returns the PRAGMA statements that select a cipher. The list is
14
+ * applied **before** `PRAGMA key = ...` so the cipher peer knows which
15
+ * KDF / mode to use when interpreting the key bytes.
16
+ *
17
+ * @stable
18
+ */
19
+ declare function pragmaSequenceForCipher(cipher: EncryptionCipher): ReadonlyArray<string>;
20
+ /**
21
+ * SQL-literal-encodes a passphrase for use as the right-hand side of
22
+ * `PRAGMA key = ...`.
23
+ *
24
+ * - String input is wrapped in single quotes with internal `'` doubled
25
+ * per the SQL specification.
26
+ * - Buffer input is encoded as the cipher peer's `x'<hex>'` blob form
27
+ * so binary keys round-trip exactly.
28
+ *
29
+ * Empty inputs are rejected at this layer so callers cannot
30
+ * accidentally open an unencrypted DB with an empty key.
31
+ *
32
+ * @stable
33
+ */
34
+ declare function encodePassphraseForPragma(value: string | Buffer): string;
35
+ //#endregion
36
+ export { DEFAULT_CIPHER, type EncryptionCipher, encodePassphraseForPragma, pragmaSequenceForCipher };
37
+ //# sourceMappingURL=cipher-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cipher-config.d.ts","names":[],"sources":["../src/cipher-config.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;cAmBa,gBAAgB;;;;;;;;iBAwBb,uBAAA,SAAgC,mBAAmB;;;;;;;;;;;;;;;iBAyBnD,yBAAA,iBAA0C"}
@@ -0,0 +1,60 @@
1
+ //#region src/cipher-config.ts
2
+ /**
3
+ * Default cipher. Matches ADR-030 § 2 — SQLCipher v4 compatible
4
+ * (AES-256-CBC + HMAC-SHA1, `legacy=4` parameter set).
5
+ *
6
+ * @stable
7
+ */
8
+ const DEFAULT_CIPHER = "sqlcipher";
9
+ /**
10
+ * Cipher-specific PRAGMAs that must be applied **immediately after**
11
+ * `PRAGMA key = ...` for SQLite3MultipleCiphers to interpret the key
12
+ * correctly. The list mirrors the upstream documentation.
13
+ *
14
+ * @internal
15
+ */
16
+ const CIPHER_PRAGMAS = Object.freeze({
17
+ sqlcipher: Object.freeze(["cipher = 'sqlcipher'", "legacy = 4"]),
18
+ chacha20: Object.freeze(["cipher = 'chacha20'"]),
19
+ aes256cbc: Object.freeze(["cipher = 'aes256cbc'"]),
20
+ aes128cbc: Object.freeze(["cipher = 'aes128cbc'"]),
21
+ rc4: Object.freeze(["cipher = 'rc4'"])
22
+ });
23
+ /**
24
+ * Returns the PRAGMA statements that select a cipher. The list is
25
+ * applied **before** `PRAGMA key = ...` so the cipher peer knows which
26
+ * KDF / mode to use when interpreting the key bytes.
27
+ *
28
+ * @stable
29
+ */
30
+ function pragmaSequenceForCipher(cipher) {
31
+ const seq = CIPHER_PRAGMAS[cipher];
32
+ if (seq === void 0) throw new TypeError(`[graphorin/store-sqlite-encrypted] unknown cipher '${String(cipher)}'. Supported: ${Object.keys(CIPHER_PRAGMAS).join(", ")}.`);
33
+ return seq;
34
+ }
35
+ /**
36
+ * SQL-literal-encodes a passphrase for use as the right-hand side of
37
+ * `PRAGMA key = ...`.
38
+ *
39
+ * - String input is wrapped in single quotes with internal `'` doubled
40
+ * per the SQL specification.
41
+ * - Buffer input is encoded as the cipher peer's `x'<hex>'` blob form
42
+ * so binary keys round-trip exactly.
43
+ *
44
+ * Empty inputs are rejected at this layer so callers cannot
45
+ * accidentally open an unencrypted DB with an empty key.
46
+ *
47
+ * @stable
48
+ */
49
+ function encodePassphraseForPragma(value) {
50
+ if (typeof value === "string") {
51
+ if (value.length === 0) throw new Error("[graphorin/store-sqlite-encrypted] passphrase is empty");
52
+ return `'${value.replace(/'/g, "''")}'`;
53
+ }
54
+ if (value.length === 0) throw new Error("[graphorin/store-sqlite-encrypted] passphrase buffer is empty");
55
+ return `x'${value.toString("hex")}'`;
56
+ }
57
+
58
+ //#endregion
59
+ export { DEFAULT_CIPHER, encodePassphraseForPragma, pragmaSequenceForCipher };
60
+ //# sourceMappingURL=cipher-config.js.map
@@ -0,0 +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"}
@@ -0,0 +1,40 @@
1
+ import { BetterSqlite3Constructor } from "@graphorin/store-sqlite/connection";
2
+
3
+ //#region src/cipher-peer.d.ts
4
+
5
+ /**
6
+ * Raised when the cipher peer driver cannot be loaded. Distinct from
7
+ * the matching `CipherPeerMissingError` in `@graphorin/store-sqlite/
8
+ * encryption` so consumers can catch the two layers independently.
9
+ *
10
+ * @stable
11
+ */
12
+ declare class EncryptedStorePeerMissingError extends Error {
13
+ readonly name = "EncryptedStorePeerMissingError";
14
+ }
15
+ /**
16
+ * Loads `better-sqlite3-multiple-ciphers`. The result is cached for
17
+ * the lifetime of the process so repeat callers (encrypt + rekey +
18
+ * connection-open in the same process) share one native handle.
19
+ *
20
+ * @stable
21
+ */
22
+ declare function loadCipherPeer(): Promise<BetterSqlite3Constructor>;
23
+ /**
24
+ * Test-only escape hatch. Drops the cached constructor so the next
25
+ * {@link loadCipherPeer} call re-imports the peer.
26
+ *
27
+ * @internal
28
+ */
29
+ declare function _resetCipherPeerCacheForTesting(): void;
30
+ /**
31
+ * Test-only escape hatch. Pre-populates the cache with a stub driver
32
+ * so unit tests can exercise the encrypt / rekey runners without
33
+ * touching the native cipher addon.
34
+ *
35
+ * @internal
36
+ */
37
+ declare function _setCipherPeerForTesting(ctor: BetterSqlite3Constructor): void;
38
+ //#endregion
39
+ export { EncryptedStorePeerMissingError, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, loadCipherPeer };
40
+ //# sourceMappingURL=cipher-peer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cipher-peer.d.ts","names":[],"sources":["../src/cipher-peer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAwBa,8BAAA,SAAuC,KAAA;;;;;;;;;;iBAc9B,cAAA,CAAA,GAAkB,QAAQ;;;;;;;iBAyBhC,+BAAA,CAAA;;;;;;;;iBAWA,wBAAA,OAA+B"}
@@ -0,0 +1,52 @@
1
+ //#region src/cipher-peer.ts
2
+ /**
3
+ * Raised when the cipher peer driver cannot be loaded. Distinct from
4
+ * the matching `CipherPeerMissingError` in `@graphorin/store-sqlite/
5
+ * encryption` so consumers can catch the two layers independently.
6
+ *
7
+ * @stable
8
+ */
9
+ var EncryptedStorePeerMissingError = class extends Error {
10
+ name = "EncryptedStorePeerMissingError";
11
+ };
12
+ /** @internal */
13
+ let CACHED_CTOR = null;
14
+ /**
15
+ * Loads `better-sqlite3-multiple-ciphers`. The result is cached for
16
+ * the lifetime of the process so repeat callers (encrypt + rekey +
17
+ * connection-open in the same process) share one native handle.
18
+ *
19
+ * @stable
20
+ */
21
+ async function loadCipherPeer() {
22
+ if (CACHED_CTOR !== null) return CACHED_CTOR;
23
+ try {
24
+ CACHED_CTOR = (await import("better-sqlite3-multiple-ciphers")).default;
25
+ return CACHED_CTOR;
26
+ } catch (err) {
27
+ throw new EncryptedStorePeerMissingError("the cipher peer 'better-sqlite3-multiple-ciphers' could not be loaded. This sub-pack declares it as a required peer dependency; install it via `pnpm add better-sqlite3-multiple-ciphers@^12.9.0` (or rerun `pnpm install` after adding @graphorin/store-sqlite-encrypted).", { cause: err });
28
+ }
29
+ }
30
+ /**
31
+ * Test-only escape hatch. Drops the cached constructor so the next
32
+ * {@link loadCipherPeer} call re-imports the peer.
33
+ *
34
+ * @internal
35
+ */
36
+ function _resetCipherPeerCacheForTesting() {
37
+ CACHED_CTOR = null;
38
+ }
39
+ /**
40
+ * Test-only escape hatch. Pre-populates the cache with a stub driver
41
+ * so unit tests can exercise the encrypt / rekey runners without
42
+ * touching the native cipher addon.
43
+ *
44
+ * @internal
45
+ */
46
+ function _setCipherPeerForTesting(ctor) {
47
+ CACHED_CTOR = ctor;
48
+ }
49
+
50
+ //#endregion
51
+ export { EncryptedStorePeerMissingError, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, loadCipherPeer };
52
+ //# sourceMappingURL=cipher-peer.js.map
@@ -0,0 +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"}
@@ -0,0 +1,17 @@
1
+ import { OpenConnectionOptions, SqliteConnection } from "@graphorin/store-sqlite/connection";
2
+
3
+ //#region src/connection.d.ts
4
+
5
+ /**
6
+ * Opens an encrypted SQLite connection. Differs from `openConnection`
7
+ * only in that the cipher peer driver is preloaded — callers that
8
+ * supply an `encryption.passphraseResolver` get the same behaviour as
9
+ * `openConnection({ encryption })` plus an explicit fail-fast on a
10
+ * missing cipher peer.
11
+ *
12
+ * @stable
13
+ */
14
+ declare function createEncryptedConnection(options: OpenConnectionOptions): Promise<SqliteConnection>;
15
+ //#endregion
16
+ export { createEncryptedConnection };
17
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","names":[],"sources":["../src/connection.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;iBAkCsB,yBAAA,UACX,wBACR,QAAQ"}
@@ -0,0 +1,41 @@
1
+ import { loadCipherPeer } from "./cipher-peer.js";
2
+ import { openConnection } from "@graphorin/store-sqlite/connection";
3
+
4
+ //#region src/connection.ts
5
+ /**
6
+ * Convenience wrapper around `openConnection` from `@graphorin/store-
7
+ * sqlite/connection` that pre-loads the cipher peer driver and applies
8
+ * the cipher PRAGMAs in the right order.
9
+ *
10
+ * Most callers never reach this directly — the standard path is to
11
+ * pass `{ encryption: { enabled: true, passphraseResolver } }` to the
12
+ * store-sqlite facade and let it consult the canonical
13
+ * `loadCipherDriver`. This helper exists for the encrypt / rekey
14
+ * runners (which need a "raw" handle on a freshly created cipher DB
15
+ * before the standard schema migrations run) and for advanced
16
+ * consumers that want to build a custom store on top of an encrypted
17
+ * connection.
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ /**
22
+ * Opens an encrypted SQLite connection. Differs from `openConnection`
23
+ * only in that the cipher peer driver is preloaded — callers that
24
+ * supply an `encryption.passphraseResolver` get the same behaviour as
25
+ * `openConnection({ encryption })` plus an explicit fail-fast on a
26
+ * missing cipher peer.
27
+ *
28
+ * @stable
29
+ */
30
+ async function createEncryptedConnection(options) {
31
+ if (options.encryption?.enabled !== true) throw new Error("[graphorin/store-sqlite-encrypted] createEncryptedConnection requires `encryption.enabled: true`. Use `openConnection` directly for unencrypted DBs.");
32
+ const ctor = await loadCipherPeer();
33
+ return openConnection({
34
+ ...options,
35
+ cipherLoader: async () => ctor
36
+ });
37
+ }
38
+
39
+ //#endregion
40
+ export { createEncryptedConnection };
41
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +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"}
@@ -0,0 +1,60 @@
1
+ import { EncryptionCipher } from "./cipher-config.js";
2
+
3
+ //#region src/encrypt.d.ts
4
+
5
+ /**
6
+ * Options for {@link encryptDatabase}.
7
+ *
8
+ * @stable
9
+ */
10
+ interface EncryptDatabaseOptions {
11
+ /** Path to the existing unencrypted source DB. */
12
+ readonly sourcePath: string;
13
+ /** Path the encrypted output is written to. Must not exist. */
14
+ readonly targetPath: string;
15
+ /** Passphrase for the new encrypted DB. */
16
+ readonly passphrase: string | Buffer;
17
+ /** Cipher selection. Default `'sqlcipher'` (SQLCipher v4 compatible). */
18
+ readonly cipher?: EncryptionCipher;
19
+ /**
20
+ * If `true`, atomically rename `targetPath` -> `sourcePath` after the
21
+ * integrity check passes. The original `sourcePath` is renamed to
22
+ * `${sourcePath}.bak.${timestamp}` so an operator can recover.
23
+ * Default `false` — the CLI does the swap explicitly.
24
+ */
25
+ readonly swap?: boolean;
26
+ /**
27
+ * If `true`, overwrite an existing `targetPath` instead of failing.
28
+ * Default `false`.
29
+ */
30
+ readonly overwriteTarget?: boolean;
31
+ }
32
+ /**
33
+ * Result of a successful {@link encryptDatabase} run.
34
+ *
35
+ * @stable
36
+ */
37
+ interface EncryptDatabaseResult {
38
+ readonly sourcePath: string;
39
+ readonly targetPath: string;
40
+ readonly cipher: EncryptionCipher;
41
+ readonly integrityCheck: {
42
+ readonly ok: boolean;
43
+ readonly rows: ReadonlyArray<string>;
44
+ };
45
+ readonly swap?: {
46
+ readonly originalRenamedTo: string;
47
+ };
48
+ }
49
+ /**
50
+ * Encrypts an unencrypted SQLite database. Returns once the target
51
+ * file has been written and verified. Throws if the source is missing,
52
+ * the target already exists (and `overwriteTarget` is unset), the
53
+ * cipher peer is missing, or the integrity check fails.
54
+ *
55
+ * @stable
56
+ */
57
+ declare function encryptDatabase(options: EncryptDatabaseOptions): Promise<EncryptDatabaseResult>;
58
+ //#endregion
59
+ export { EncryptDatabaseOptions, EncryptDatabaseResult, encryptDatabase };
60
+ //# sourceMappingURL=encrypt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypt.d.ts","names":[],"sources":["../src/encrypt.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAuCiB,sBAAA;;;;;;gCAMe;;oBAEZ;;;;;;;;;;;;;;;;;;;UAoBH,qBAAA;;;mBAGE;;;mBAC+C;;;;;;;;;;;;;;iBAY5C,eAAA,UACX,yBACR,QAAQ"}
@@ -0,0 +1,119 @@
1
+ import { DEFAULT_CIPHER, encodePassphraseForPragma, pragmaSequenceForCipher } from "./cipher-config.js";
2
+ import { loadCipherPeer } from "./cipher-peer.js";
3
+ import { createEncryptedConnection } from "./connection.js";
4
+ import { cipherIntegrityCheck } from "./integrity-check.js";
5
+ import { copyFileSync, existsSync, renameSync, unlinkSync } from "node:fs";
6
+ import { isAbsolute, resolve } from "node:path";
7
+
8
+ //#region src/encrypt.ts
9
+ /**
10
+ * `encryptDatabase` — converts an unencrypted SQLite database file
11
+ * into an encrypted one. Backs `graphorin storage encrypt` per
12
+ * ADR-030 § 8.
13
+ *
14
+ * Strategy (CS-7): sqlite3mc ships **no** `sqlcipher_export` function
15
+ * (verified against the real peer — the old ATTACH+export path threw
16
+ * "no such function" on every real run), so the export is a
17
+ * **checkpoint → file copy → in-place `PRAGMA rekey`** sequence:
18
+ *
19
+ * 1. open the plaintext source, `wal_checkpoint(TRUNCATE)`, close;
20
+ * 2. byte-copy the file to the target (this trivially preserves every
21
+ * rowid, so FTS5 external-content mappings stay intact — CS-10);
22
+ * 3. open the copy through the cipher peer with NO key, apply the
23
+ * cipher-selection pragmas, then `PRAGMA rekey = <key>` — sqlite3mc
24
+ * encrypts a plaintext database in place;
25
+ * 4. re-open with the key and verify via `PRAGMA integrity_check`.
26
+ *
27
+ * @packageDocumentation
28
+ */
29
+ /**
30
+ * Encrypts an unencrypted SQLite database. Returns once the target
31
+ * file has been written and verified. Throws if the source is missing,
32
+ * the target already exists (and `overwriteTarget` is unset), the
33
+ * cipher peer is missing, or the integrity check fails.
34
+ *
35
+ * @stable
36
+ */
37
+ async function encryptDatabase(options) {
38
+ const sourcePath = absolute(options.sourcePath);
39
+ const targetPath = absolute(options.targetPath);
40
+ const cipher = options.cipher ?? DEFAULT_CIPHER;
41
+ if (!existsSync(sourcePath)) throw new Error(`[graphorin/store-sqlite-encrypted] source DB not found: ${sourcePath}`);
42
+ if (sourcePath === targetPath) throw new Error("[graphorin/store-sqlite-encrypted] sourcePath and targetPath must differ. Pass a temporary targetPath then enable `swap: true` to atomically replace the source.");
43
+ if (existsSync(targetPath)) {
44
+ if (options.overwriteTarget !== true) throw new Error(`[graphorin/store-sqlite-encrypted] target DB already exists: ${targetPath}. Pass \`overwriteTarget: true\` to replace it.`);
45
+ unlinkSync(targetPath);
46
+ }
47
+ const Ctor = await loadCipherPeer();
48
+ try {
49
+ const source = new Ctor(sourcePath);
50
+ try {
51
+ source.pragma("wal_checkpoint(TRUNCATE)");
52
+ } finally {
53
+ if (source.open) source.close();
54
+ }
55
+ copyFileSync(sourcePath, targetPath);
56
+ const target = new Ctor(targetPath);
57
+ try {
58
+ for (const pragma of pragmaSequenceForCipher(cipher)) target.pragma(pragma);
59
+ target.pragma("journal_mode = DELETE");
60
+ const encodedKey = encodePassphraseForPragma(options.passphrase);
61
+ target.pragma(`rekey = ${encodedKey}`);
62
+ target.pragma("journal_mode = WAL");
63
+ } finally {
64
+ if (target.open) target.close();
65
+ }
66
+ } catch (err) {
67
+ if (existsSync(targetPath)) try {
68
+ unlinkSync(targetPath);
69
+ } catch {}
70
+ throw new Error(`[graphorin/store-sqlite-encrypted] encryption failed: ${err.message}`, { cause: err });
71
+ }
72
+ const verify = await createEncryptedConnection({
73
+ path: targetPath,
74
+ skipSqliteVec: true,
75
+ disableWalHardening: true,
76
+ encryption: {
77
+ enabled: true,
78
+ cipher,
79
+ passphraseResolver: async () => options.passphrase
80
+ }
81
+ });
82
+ let integrityCheck;
83
+ try {
84
+ const result = cipherIntegrityCheck(verify);
85
+ integrityCheck = {
86
+ ok: result.ok,
87
+ rows: result.rows
88
+ };
89
+ } finally {
90
+ verify.close();
91
+ }
92
+ if (!integrityCheck.ok) {
93
+ if (existsSync(targetPath)) try {
94
+ unlinkSync(targetPath);
95
+ } catch {}
96
+ throw new Error("[graphorin/store-sqlite-encrypted] post-encrypt integrity check failed: " + integrityCheck.rows.join("; "));
97
+ }
98
+ let swap;
99
+ if (options.swap === true) {
100
+ const renamedTo = `${sourcePath}.bak.${Date.now()}`;
101
+ renameSync(sourcePath, renamedTo);
102
+ renameSync(targetPath, sourcePath);
103
+ swap = Object.freeze({ originalRenamedTo: renamedTo });
104
+ }
105
+ return Object.freeze({
106
+ sourcePath,
107
+ targetPath: swap !== void 0 ? sourcePath : targetPath,
108
+ cipher,
109
+ integrityCheck: Object.freeze(integrityCheck),
110
+ ...swap !== void 0 ? { swap } : {}
111
+ });
112
+ }
113
+ function absolute(p) {
114
+ return isAbsolute(p) ? p : resolve(p);
115
+ }
116
+
117
+ //#endregion
118
+ export { encryptDatabase };
119
+ //# sourceMappingURL=encrypt.js.map
@@ -0,0 +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"}
@@ -0,0 +1,51 @@
1
+ import { DEFAULT_CIPHER, EncryptionCipher, encodePassphraseForPragma, pragmaSequenceForCipher } from "./cipher-config.js";
2
+ import { EncryptedStorePeerMissingError, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, loadCipherPeer } from "./cipher-peer.js";
3
+ import { createEncryptedConnection } from "./connection.js";
4
+ import { EncryptDatabaseOptions, EncryptDatabaseResult, encryptDatabase } from "./encrypt.js";
5
+ import { CipherIntegrityCheckResult, cipherIntegrityCheck } from "./integrity-check.js";
6
+ import { RekeyDatabaseOptions, RekeyDatabaseResult, rekeyDatabase } from "./rekey.js";
7
+
8
+ //#region src/index.d.ts
9
+
10
+ /**
11
+ * @graphorin/store-sqlite-encrypted — optional encryption-at-rest
12
+ * sub-pack for the Graphorin framework's default SQLite store.
13
+ *
14
+ * Installing this package pulls in the cipher peer driver
15
+ * (`better-sqlite3-multiple-ciphers@^12.9.0`), which is a drop-in fork
16
+ * of `better-sqlite3` that bundles the SQLite3MultipleCiphers
17
+ * extension (SQLCipher v4 / wxSQLite3 / AES-256-CBC / AES-128-CBC /
18
+ * RC4 cipher modes).
19
+ *
20
+ * The package exposes:
21
+ *
22
+ * - {@link createEncryptedConnection} — convenience wrapper around
23
+ * `openConnection` from `@graphorin/store-sqlite/connection` that
24
+ * pre-loads the cipher peer.
25
+ * - {@link encryptDatabase} — converts an unencrypted SQLite file
26
+ * into an encrypted one. Backs `graphorin storage encrypt`.
27
+ * - {@link rekeyDatabase} — re-keys an already encrypted file. Backs
28
+ * `graphorin storage rekey`.
29
+ * - {@link cipherIntegrityCheck} — runs `PRAGMA cipher_integrity_
30
+ * check`. Used by the triggers daemon's daily verification cron
31
+ * and the `/v1/health/storage` endpoint.
32
+ * - {@link DEFAULT_CIPHER}, {@link pragmaSequenceForCipher},
33
+ * {@link encodePassphraseForPragma} — cipher-config helpers shared
34
+ * by the runners and consumable for advanced setups.
35
+ * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError} —
36
+ * explicit peer-loader surface for callers that want to fail-fast
37
+ * at startup before opening the DB.
38
+ *
39
+ * Defaults follow ADR-030 / DEC-129:
40
+ * - Cipher: `'sqlcipher'` (SQLCipher v4 compatible, `legacy=4`).
41
+ * - Default OFF; opt-in through `graphorin init --encrypted`.
42
+ * - audit.db is ALWAYS encrypted regardless of this opt-in
43
+ * (DEC-124); this package satisfies that requirement too.
44
+ *
45
+ * @packageDocumentation
46
+ */
47
+ /** Canonical version constant. Mirrors the `package.json` version. */
48
+ declare const VERSION = "0.5.0";
49
+ //#endregion
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
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,OAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ import { DEFAULT_CIPHER, encodePassphraseForPragma, pragmaSequenceForCipher } from "./cipher-config.js";
2
+ import { EncryptedStorePeerMissingError, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, loadCipherPeer } from "./cipher-peer.js";
3
+ import { createEncryptedConnection } from "./connection.js";
4
+ import { cipherIntegrityCheck } from "./integrity-check.js";
5
+ import { encryptDatabase } from "./encrypt.js";
6
+ import { rekeyDatabase } from "./rekey.js";
7
+
8
+ //#region src/index.ts
9
+ /**
10
+ * @graphorin/store-sqlite-encrypted — optional encryption-at-rest
11
+ * sub-pack for the Graphorin framework's default SQLite store.
12
+ *
13
+ * Installing this package pulls in the cipher peer driver
14
+ * (`better-sqlite3-multiple-ciphers@^12.9.0`), which is a drop-in fork
15
+ * of `better-sqlite3` that bundles the SQLite3MultipleCiphers
16
+ * extension (SQLCipher v4 / wxSQLite3 / AES-256-CBC / AES-128-CBC /
17
+ * RC4 cipher modes).
18
+ *
19
+ * The package exposes:
20
+ *
21
+ * - {@link createEncryptedConnection} — convenience wrapper around
22
+ * `openConnection` from `@graphorin/store-sqlite/connection` that
23
+ * pre-loads the cipher peer.
24
+ * - {@link encryptDatabase} — converts an unencrypted SQLite file
25
+ * into an encrypted one. Backs `graphorin storage encrypt`.
26
+ * - {@link rekeyDatabase} — re-keys an already encrypted file. Backs
27
+ * `graphorin storage rekey`.
28
+ * - {@link cipherIntegrityCheck} — runs `PRAGMA cipher_integrity_
29
+ * check`. Used by the triggers daemon's daily verification cron
30
+ * and the `/v1/health/storage` endpoint.
31
+ * - {@link DEFAULT_CIPHER}, {@link pragmaSequenceForCipher},
32
+ * {@link encodePassphraseForPragma} — cipher-config helpers shared
33
+ * by the runners and consumable for advanced setups.
34
+ * - {@link loadCipherPeer} / {@link EncryptedStorePeerMissingError} —
35
+ * explicit peer-loader surface for callers that want to fail-fast
36
+ * at startup before opening the DB.
37
+ *
38
+ * Defaults follow ADR-030 / DEC-129:
39
+ * - Cipher: `'sqlcipher'` (SQLCipher v4 compatible, `legacy=4`).
40
+ * - Default OFF; opt-in through `graphorin init --encrypted`.
41
+ * - audit.db is ALWAYS encrypted regardless of this opt-in
42
+ * (DEC-124); this package satisfies that requirement too.
43
+ *
44
+ * @packageDocumentation
45
+ */
46
+ /** Canonical version constant. Mirrors the `package.json` version. */
47
+ const VERSION = "0.5.0";
48
+
49
+ //#endregion
50
+ export { DEFAULT_CIPHER, EncryptedStorePeerMissingError, VERSION, _resetCipherPeerCacheForTesting, _setCipherPeerForTesting, cipherIntegrityCheck, createEncryptedConnection, encodePassphraseForPragma, encryptDatabase, loadCipherPeer, pragmaSequenceForCipher, rekeyDatabase };
51
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,32 @@
1
+ import { SqliteConnection } from "@graphorin/store-sqlite/connection";
2
+
3
+ //#region src/integrity-check.d.ts
4
+
5
+ /**
6
+ * Result of {@link cipherIntegrityCheck}.
7
+ *
8
+ * @stable
9
+ */
10
+ interface CipherIntegrityCheckResult {
11
+ /** `true` when SQLite reported a single `'ok'` row. */
12
+ readonly ok: boolean;
13
+ /** Raw rows returned by `PRAGMA integrity_check`. */
14
+ readonly rows: ReadonlyArray<string>;
15
+ /** Wall-clock duration of the pragma call in milliseconds. */
16
+ readonly durationMs: number;
17
+ }
18
+ /**
19
+ * Runs `PRAGMA integrity_check` against the provided connection. The
20
+ * connection MUST already be open with the cipher key applied
21
+ * (typically via {@link createEncryptedConnection}) — a wrong key
22
+ * surfaces as an open/read error before the pragma runs.
23
+ *
24
+ * The pragma is read-only so it is safe to run from a triggers daemon
25
+ * cron without taking a write lock.
26
+ *
27
+ * @stable
28
+ */
29
+ declare function cipherIntegrityCheck(conn: SqliteConnection): CipherIntegrityCheckResult;
30
+ //#endregion
31
+ export { CipherIntegrityCheckResult, cipherIntegrityCheck };
32
+ //# sourceMappingURL=integrity-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrity-check.d.ts","names":[],"sources":["../src/integrity-check.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAmBiB,0BAAA;;;;iBAIA;;;;;;;;;;;;;;;iBAgBD,oBAAA,OAA2B,mBAAmB"}
@@ -0,0 +1,39 @@
1
+ //#region src/integrity-check.ts
2
+ /**
3
+ * Runs `PRAGMA integrity_check` against the provided connection. The
4
+ * connection MUST already be open with the cipher key applied
5
+ * (typically via {@link createEncryptedConnection}) — a wrong key
6
+ * surfaces as an open/read error before the pragma runs.
7
+ *
8
+ * The pragma is read-only so it is safe to run from a triggers daemon
9
+ * cron without taking a write lock.
10
+ *
11
+ * @stable
12
+ */
13
+ function cipherIntegrityCheck(conn) {
14
+ const started = performance.now();
15
+ const raw = conn.pragma("integrity_check");
16
+ const durationMs = performance.now() - started;
17
+ const rows = normalizeRows(raw);
18
+ const ok = rows.length === 1 && rows[0] === "ok";
19
+ return Object.freeze({
20
+ ok,
21
+ rows: Object.freeze(rows),
22
+ durationMs
23
+ });
24
+ }
25
+ function normalizeRows(raw) {
26
+ if (!Array.isArray(raw)) return [];
27
+ const out = [];
28
+ for (const row of raw) if (typeof row === "string") out.push(row);
29
+ else if (row !== null && typeof row === "object") {
30
+ const obj = row;
31
+ const value = obj.cipher_integrity_check ?? obj.integrity_check ?? Object.values(obj)[0];
32
+ if (typeof value === "string") out.push(value);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ //#endregion
38
+ export { cipherIntegrityCheck };
39
+ //# sourceMappingURL=integrity-check.js.map
@@ -0,0 +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"}
@@ -0,0 +1,44 @@
1
+ import { EncryptionCipher } from "./cipher-config.js";
2
+
3
+ //#region src/rekey.d.ts
4
+
5
+ /**
6
+ * Options for {@link rekeyDatabase}.
7
+ *
8
+ * @stable
9
+ */
10
+ interface RekeyDatabaseOptions {
11
+ /** Path to the encrypted DB. */
12
+ readonly path: string;
13
+ /** Existing passphrase the DB is currently encrypted with. */
14
+ readonly oldPassphrase: string | Buffer;
15
+ /** New passphrase to apply. */
16
+ readonly newPassphrase: string | Buffer;
17
+ /** Cipher selection. Default `'sqlcipher'`. */
18
+ readonly cipher?: EncryptionCipher;
19
+ }
20
+ /**
21
+ * Result of a successful {@link rekeyDatabase} run.
22
+ *
23
+ * @stable
24
+ */
25
+ interface RekeyDatabaseResult {
26
+ readonly path: string;
27
+ readonly cipher: EncryptionCipher;
28
+ readonly integrityCheck: {
29
+ readonly ok: boolean;
30
+ readonly rows: ReadonlyArray<string>;
31
+ };
32
+ }
33
+ /**
34
+ * Re-keys an encrypted SQLite database. Throws if the file is missing,
35
+ * the cipher peer cannot be loaded, the old passphrase is wrong (the
36
+ * cipher peer raises `SQLITE_NOTADB` on the first read), or the
37
+ * post-rekey integrity check fails.
38
+ *
39
+ * @stable
40
+ */
41
+ declare function rekeyDatabase(options: RekeyDatabaseOptions): Promise<RekeyDatabaseResult>;
42
+ //#endregion
43
+ export { RekeyDatabaseOptions, RekeyDatabaseResult, rekeyDatabase };
44
+ //# sourceMappingURL=rekey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rekey.d.ts","names":[],"sources":["../src/rekey.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UA8BiB,oBAAA;;;;mCAIkB;;mCAEA;;oBAEf;;;;;;;UAQH,mBAAA;;mBAEE;;;mBAC+C;;;;;;;;;;;iBAW5C,aAAA,UAAuB,uBAAuB,QAAQ"}
package/dist/rekey.js ADDED
@@ -0,0 +1,89 @@
1
+ import { DEFAULT_CIPHER, encodePassphraseForPragma } from "./cipher-config.js";
2
+ import { loadCipherPeer } from "./cipher-peer.js";
3
+ import { createEncryptedConnection } from "./connection.js";
4
+ import { cipherIntegrityCheck } from "./integrity-check.js";
5
+ import { existsSync } from "node:fs";
6
+ import { isAbsolute, resolve } from "node:path";
7
+
8
+ //#region src/rekey.ts
9
+ /**
10
+ * `rekeyDatabase` — re-keys an already encrypted SQLite database. The
11
+ * runner backs `graphorin storage rekey`.
12
+ *
13
+ * Strategy: open the DB through the cipher peer with the **old** key,
14
+ * then issue `PRAGMA rekey = <new>;`. The cipher peer rewrites every
15
+ * page transactionally. After the pragma we re-open the DB with the
16
+ * new key and run `PRAGMA cipher_integrity_check` to confirm the
17
+ * rotation succeeded.
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ /**
22
+ * Re-keys an encrypted SQLite database. Throws if the file is missing,
23
+ * the cipher peer cannot be loaded, the old passphrase is wrong (the
24
+ * cipher peer raises `SQLITE_NOTADB` on the first read), or the
25
+ * post-rekey integrity check fails.
26
+ *
27
+ * @stable
28
+ */
29
+ async function rekeyDatabase(options) {
30
+ const path = absolute(options.path);
31
+ const cipher = options.cipher ?? DEFAULT_CIPHER;
32
+ if (!existsSync(path)) throw new Error(`[graphorin/store-sqlite-encrypted] DB not found: ${path}`);
33
+ await loadCipherPeer();
34
+ const conn = await createEncryptedConnection({
35
+ path,
36
+ skipSqliteVec: true,
37
+ disableWalHardening: true,
38
+ encryption: {
39
+ enabled: true,
40
+ cipher,
41
+ passphraseResolver: async () => options.oldPassphrase
42
+ }
43
+ });
44
+ try {
45
+ conn.pragma("user_version");
46
+ conn.pragma("journal_mode = DELETE");
47
+ const newEncoded = encodePassphraseForPragma(options.newPassphrase);
48
+ conn.pragma(`rekey = ${newEncoded}`);
49
+ conn.pragma("journal_mode = WAL");
50
+ } catch (err) {
51
+ if (conn.raw().open) conn.close();
52
+ throw new Error(`[graphorin/store-sqlite-encrypted] rekey failed: ${err.message}`, { cause: err });
53
+ } finally {
54
+ if (conn.raw().open) conn.close();
55
+ }
56
+ const verify = await createEncryptedConnection({
57
+ path,
58
+ skipSqliteVec: true,
59
+ disableWalHardening: true,
60
+ encryption: {
61
+ enabled: true,
62
+ cipher,
63
+ passphraseResolver: async () => options.newPassphrase
64
+ }
65
+ });
66
+ let integrityCheck;
67
+ try {
68
+ const result = cipherIntegrityCheck(verify);
69
+ integrityCheck = {
70
+ ok: result.ok,
71
+ rows: result.rows
72
+ };
73
+ } finally {
74
+ verify.close();
75
+ }
76
+ if (!integrityCheck.ok) throw new Error("[graphorin/store-sqlite-encrypted] post-rekey integrity check failed: " + integrityCheck.rows.join("; "));
77
+ return Object.freeze({
78
+ path,
79
+ cipher,
80
+ integrityCheck: Object.freeze(integrityCheck)
81
+ });
82
+ }
83
+ function absolute(p) {
84
+ return isAbsolute(p) ? p : resolve(p);
85
+ }
86
+
87
+ //#endregion
88
+ export { rekeyDatabase };
89
+ //# sourceMappingURL=rekey.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@graphorin/store-sqlite-encrypted",
3
+ "version": "0.5.0",
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
+ "license": "MIT",
6
+ "author": "Oleksiy Stepurenko",
7
+ "homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/store-sqlite-encrypted",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/o-stepper/graphorin.git",
11
+ "directory": "packages/store-sqlite-encrypted"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/o-stepper/graphorin/issues"
15
+ },
16
+ "keywords": [
17
+ "graphorin",
18
+ "ai",
19
+ "agents",
20
+ "framework",
21
+ "sqlite",
22
+ "sqlcipher",
23
+ "encryption",
24
+ "encryption-at-rest",
25
+ "better-sqlite3-multiple-ciphers",
26
+ "rekey",
27
+ "vacuum-into"
28
+ ],
29
+ "type": "module",
30
+ "engines": {
31
+ "node": ">=22.0.0"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "sideEffects": false,
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "LICENSE"
49
+ ],
50
+ "dependencies": {
51
+ "@graphorin/store-sqlite": "0.5.0"
52
+ },
53
+ "peerDependencies": {
54
+ "better-sqlite3-multiple-ciphers": "^12.9.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "better-sqlite3-multiple-ciphers": {
58
+ "optional": false
59
+ }
60
+ },
61
+ "publishConfig": {
62
+ "access": "public",
63
+ "provenance": true
64
+ },
65
+ "devDependencies": {
66
+ "@types/better-sqlite3": "^7.6.13",
67
+ "@graphorin/security": "0.5.0"
68
+ },
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
72
+ "test": "vitest run",
73
+ "lint": "biome check .",
74
+ "clean": "rimraf dist .turbo *.tsbuildinfo"
75
+ }
76
+ }