@nodellmcache/encryption 1.0.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,12 @@
1
+ # @nodellmcache/encryption
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial release of `@nodellmcache/encryption`: an `EncryptedAdapter` that wraps any `StorageAdapter` and encrypts cached values at rest with AES-256-GCM (authenticated; tampering is detected) using pure `node:crypto`. A fresh random IV is used per write; keys/timestamps/metadata stay in the clear (cache keys are already hashed). Includes `generateKey()` and `deriveKey(passphrase, salt)` helpers; accepts a 32-byte Buffer, hex, or base64 key. Wrong key or tampered data throws `CacheAdapterError`.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [a2633d8]
12
+ - @nodellmcache/core@1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NodeLLMCache contributors
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,48 @@
1
+ # @nodellmcache/encryption
2
+
3
+ Encrypting storage adapter wrapper for [NodeLLMCache](https://github.com/mdmax007/node-llm-cache). Wraps any `StorageAdapter` and encrypts cached **values** at rest with AES-256-GCM (authenticated, so tampering is detected). Pure `node:crypto`, no native bindings.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @nodellmcache/encryption @nodellmcache/core
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { PromptCache } from '@nodellmcache/prompt-cache'
15
+ import { EncryptedAdapter, generateKey } from '@nodellmcache/encryption'
16
+ import { RedisAdapter } from '@nodellmcache/redis'
17
+
18
+ const key = generateKey() // 32 bytes; persist this securely (env/secret manager)
19
+
20
+ const adapter = new EncryptedAdapter({
21
+ adapter: new RedisAdapter({ host: 'localhost', port: 6379 }),
22
+ key,
23
+ })
24
+
25
+ const cache = new PromptCache({ adapter })
26
+ ```
27
+
28
+ Derive a key from a passphrase instead:
29
+
30
+ ```ts
31
+ import { deriveKey } from '@nodellmcache/encryption'
32
+ const key = deriveKey(process.env.CACHE_PASSPHRASE!, process.env.CACHE_SALT!)
33
+ ```
34
+
35
+ ## What is and isn't encrypted
36
+
37
+ - **Encrypted**: the cached value (serialized, then AES-256-GCM with a fresh random IV per write).
38
+ - **In the clear**: the cache key, timestamps, and metadata (sizes, cache type, model). Cache keys are already SHA-256 hashed by `KeyBuilder`, so no plaintext prompt content is exposed either way.
39
+
40
+ ## Notes
41
+
42
+ - The key must be 32 bytes. Pass a `Buffer`, a 64-char hex string, or base64. `generateKey()` makes one; `deriveKey(passphrase, salt)` derives one via scrypt.
43
+ - Decryption with the wrong key, or against tampered data, throws `CacheAdapterError` (the GCM auth tag fails).
44
+ - It is a `StorageAdapter`, so it composes with anything, including `@nodellmcache/tiered`.
45
+
46
+ ## License
47
+
48
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ EncryptedAdapter: () => EncryptedAdapter,
24
+ KEY_BYTES: () => KEY_BYTES,
25
+ deriveKey: () => deriveKey,
26
+ generateKey: () => generateKey,
27
+ normalizeKey: () => normalizeKey
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/EncryptedAdapter.ts
32
+ var import_node_crypto2 = require("crypto");
33
+ var import_core2 = require("@nodellmcache/core");
34
+
35
+ // src/keys.ts
36
+ var import_node_crypto = require("crypto");
37
+ var import_core = require("@nodellmcache/core");
38
+ var KEY_BYTES = 32;
39
+ function generateKey() {
40
+ return (0, import_node_crypto.randomBytes)(KEY_BYTES);
41
+ }
42
+ function deriveKey(passphrase, salt) {
43
+ return (0, import_node_crypto.scryptSync)(passphrase, salt, KEY_BYTES);
44
+ }
45
+ function normalizeKey(key) {
46
+ let buf;
47
+ if (Buffer.isBuffer(key)) {
48
+ buf = key;
49
+ } else if (/^[0-9a-fA-F]{64}$/.test(key)) {
50
+ buf = Buffer.from(key, "hex");
51
+ } else {
52
+ buf = Buffer.from(key, "base64");
53
+ }
54
+ if (buf.length !== KEY_BYTES) {
55
+ throw new import_core.ValidationError(
56
+ `Encryption key must be ${KEY_BYTES} bytes (got ${buf.length}). Use generateKey() or deriveKey(passphrase, salt).`
57
+ );
58
+ }
59
+ return buf;
60
+ }
61
+
62
+ // src/EncryptedAdapter.ts
63
+ var ALGO = "aes-256-gcm";
64
+ var IV_BYTES = 12;
65
+ var EncryptedAdapter = class {
66
+ inner;
67
+ key;
68
+ serializer;
69
+ constructor(options) {
70
+ this.inner = options.adapter;
71
+ this.key = normalizeKey(options.key);
72
+ this.serializer = options.serializer ?? new import_core2.JsonSerializer();
73
+ }
74
+ async get(key) {
75
+ const stored = await this.inner.get(key);
76
+ if (!stored) return null;
77
+ const value = this.decrypt(stored.value);
78
+ return {
79
+ key: stored.key,
80
+ value,
81
+ createdAt: stored.createdAt,
82
+ expiresAt: stored.expiresAt,
83
+ metadata: stored.metadata
84
+ };
85
+ }
86
+ async set(key, entry, ttl) {
87
+ const stored = {
88
+ key: entry.key,
89
+ value: this.encrypt(entry.value),
90
+ createdAt: entry.createdAt,
91
+ expiresAt: entry.expiresAt,
92
+ metadata: entry.metadata
93
+ };
94
+ await this.inner.set(key, stored, ttl);
95
+ }
96
+ async delete(key) {
97
+ await this.inner.delete(key);
98
+ }
99
+ async clear() {
100
+ await this.inner.clear();
101
+ }
102
+ async has(key) {
103
+ return this.inner.has(key);
104
+ }
105
+ async stats() {
106
+ return this.inner.stats();
107
+ }
108
+ // --- crypto --------------------------------------------------------------
109
+ encrypt(value) {
110
+ const iv = (0, import_node_crypto2.randomBytes)(IV_BYTES);
111
+ const cipher = (0, import_node_crypto2.createCipheriv)(ALGO, this.key, iv);
112
+ const plaintext = this.serializer.serialize(value);
113
+ const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
114
+ const tag = cipher.getAuthTag();
115
+ return { v: 1, iv: iv.toString("base64"), tag: tag.toString("base64"), data: data.toString("base64") };
116
+ }
117
+ decrypt(blob) {
118
+ try {
119
+ const decipher = (0, import_node_crypto2.createDecipheriv)(ALGO, this.key, Buffer.from(blob.iv, "base64"));
120
+ decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
121
+ const plaintext = Buffer.concat([
122
+ decipher.update(Buffer.from(blob.data, "base64")),
123
+ decipher.final()
124
+ ]);
125
+ return this.serializer.deserialize(plaintext);
126
+ } catch (cause) {
127
+ throw new import_core2.CacheAdapterError(
128
+ "Failed to decrypt cache entry (wrong key or tampered data)",
129
+ { cause }
130
+ );
131
+ }
132
+ }
133
+ };
134
+ // Annotate the CommonJS export names for ESM import in node:
135
+ 0 && (module.exports = {
136
+ EncryptedAdapter,
137
+ KEY_BYTES,
138
+ deriveKey,
139
+ generateKey,
140
+ normalizeKey
141
+ });
142
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/EncryptedAdapter.ts","../src/keys.ts"],"sourcesContent":["export {\n EncryptedAdapter,\n type EncryptedAdapterOptions,\n type EncryptedBlob,\n} from './EncryptedAdapter.js'\nexport { generateKey, deriveKey, normalizeKey, KEY_BYTES } from './keys.js'\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'\nimport { CacheAdapterError, JsonSerializer } from '@nodellmcache/core'\nimport type { AdapterStats, CacheEntry, Serializer, StorageAdapter } from '@nodellmcache/core'\nimport { normalizeKey } from './keys.js'\n\nconst ALGO = 'aes-256-gcm'\nconst IV_BYTES = 12\n\n/** The encrypted envelope stored in place of a value. */\nexport interface EncryptedBlob {\n v: 1\n iv: string\n tag: string\n data: string\n}\n\nexport interface EncryptedAdapterOptions {\n /** Inner adapter that stores the encrypted envelopes (memory, Redis, ...). */\n adapter: StorageAdapter<EncryptedBlob>\n /** 32-byte key: a Buffer, 64-char hex, or base64. See `generateKey`/`deriveKey`. */\n key: Buffer | string\n /** Serializer used to turn values into bytes before encryption. Defaults to JSON. */\n serializer?: Serializer\n}\n\n/**\n * Wraps any {@link StorageAdapter} and encrypts cached **values** at rest with\n * AES-256-GCM (authenticated, so tampering is detected). Keys, timestamps, and\n * metadata are stored in the clear — only the value is encrypted, and cache keys\n * are already hashed by `KeyBuilder`, so no plaintext prompt content is exposed.\n *\n * Pure `node:crypto`, no native bindings.\n */\nexport class EncryptedAdapter<T = unknown> implements StorageAdapter<T> {\n private readonly inner: StorageAdapter<EncryptedBlob>\n private readonly key: Buffer\n private readonly serializer: Serializer\n\n constructor(options: EncryptedAdapterOptions) {\n this.inner = options.adapter\n this.key = normalizeKey(options.key)\n this.serializer = options.serializer ?? new JsonSerializer()\n }\n\n async get(key: string): Promise<CacheEntry<T> | null> {\n const stored = await this.inner.get(key)\n if (!stored) return null\n const value = this.decrypt(stored.value)\n return {\n key: stored.key,\n value,\n createdAt: stored.createdAt,\n expiresAt: stored.expiresAt,\n metadata: stored.metadata,\n }\n }\n\n async set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void> {\n const stored: CacheEntry<EncryptedBlob> = {\n key: entry.key,\n value: this.encrypt(entry.value),\n createdAt: entry.createdAt,\n expiresAt: entry.expiresAt,\n metadata: entry.metadata,\n }\n await this.inner.set(key, stored, ttl)\n }\n\n async delete(key: string): Promise<void> {\n await this.inner.delete(key)\n }\n\n async clear(): Promise<void> {\n await this.inner.clear()\n }\n\n async has(key: string): Promise<boolean> {\n return this.inner.has(key)\n }\n\n async stats(): Promise<AdapterStats> {\n return this.inner.stats()\n }\n\n // --- crypto --------------------------------------------------------------\n\n private encrypt(value: T): EncryptedBlob {\n const iv = randomBytes(IV_BYTES)\n const cipher = createCipheriv(ALGO, this.key, iv)\n const plaintext = this.serializer.serialize(value)\n const data = Buffer.concat([cipher.update(plaintext), cipher.final()])\n const tag = cipher.getAuthTag()\n return { v: 1, iv: iv.toString('base64'), tag: tag.toString('base64'), data: data.toString('base64') }\n }\n\n private decrypt(blob: EncryptedBlob): T {\n try {\n const decipher = createDecipheriv(ALGO, this.key, Buffer.from(blob.iv, 'base64'))\n decipher.setAuthTag(Buffer.from(blob.tag, 'base64'))\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(blob.data, 'base64')),\n decipher.final(),\n ])\n return this.serializer.deserialize<T>(plaintext)\n } catch (cause) {\n throw new CacheAdapterError(\n 'Failed to decrypt cache entry (wrong key or tampered data)',\n { cause },\n )\n }\n }\n}\n","import { randomBytes, scryptSync } from 'node:crypto'\nimport { ValidationError } from '@nodellmcache/core'\n\n/** AES-256 needs a 32-byte key. */\nexport const KEY_BYTES = 32\n\n/** Generates a fresh random 32-byte AES-256 key. Persist it somewhere safe. */\nexport function generateKey(): Buffer {\n return randomBytes(KEY_BYTES)\n}\n\n/**\n * Derives a 32-byte key from a passphrase and salt using scrypt. Use a stable,\n * unique salt per deployment (store it alongside your config, not the data).\n */\nexport function deriveKey(passphrase: string, salt: string | Buffer): Buffer {\n return scryptSync(passphrase, salt, KEY_BYTES)\n}\n\n/**\n * Normalizes a key option into a 32-byte Buffer. Accepts a Buffer, a 64-char hex\n * string, or a 44-char base64 string.\n */\nexport function normalizeKey(key: Buffer | string): Buffer {\n let buf: Buffer\n if (Buffer.isBuffer(key)) {\n buf = key\n } else if (/^[0-9a-fA-F]{64}$/.test(key)) {\n buf = Buffer.from(key, 'hex')\n } else {\n buf = Buffer.from(key, 'base64')\n }\n if (buf.length !== KEY_BYTES) {\n throw new ValidationError(\n `Encryption key must be ${KEY_BYTES} bytes (got ${buf.length}). ` +\n 'Use generateKey() or deriveKey(passphrase, salt).',\n )\n }\n return buf\n}\n"],"mappings":"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","names":["import_node_crypto","import_core"]}
@@ -0,0 +1,56 @@
1
+ import { StorageAdapter, Serializer, CacheEntry, AdapterStats } from '@nodellmcache/core';
2
+
3
+ /** The encrypted envelope stored in place of a value. */
4
+ interface EncryptedBlob {
5
+ v: 1;
6
+ iv: string;
7
+ tag: string;
8
+ data: string;
9
+ }
10
+ interface EncryptedAdapterOptions {
11
+ /** Inner adapter that stores the encrypted envelopes (memory, Redis, ...). */
12
+ adapter: StorageAdapter<EncryptedBlob>;
13
+ /** 32-byte key: a Buffer, 64-char hex, or base64. See `generateKey`/`deriveKey`. */
14
+ key: Buffer | string;
15
+ /** Serializer used to turn values into bytes before encryption. Defaults to JSON. */
16
+ serializer?: Serializer;
17
+ }
18
+ /**
19
+ * Wraps any {@link StorageAdapter} and encrypts cached **values** at rest with
20
+ * AES-256-GCM (authenticated, so tampering is detected). Keys, timestamps, and
21
+ * metadata are stored in the clear — only the value is encrypted, and cache keys
22
+ * are already hashed by `KeyBuilder`, so no plaintext prompt content is exposed.
23
+ *
24
+ * Pure `node:crypto`, no native bindings.
25
+ */
26
+ declare class EncryptedAdapter<T = unknown> implements StorageAdapter<T> {
27
+ private readonly inner;
28
+ private readonly key;
29
+ private readonly serializer;
30
+ constructor(options: EncryptedAdapterOptions);
31
+ get(key: string): Promise<CacheEntry<T> | null>;
32
+ set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void>;
33
+ delete(key: string): Promise<void>;
34
+ clear(): Promise<void>;
35
+ has(key: string): Promise<boolean>;
36
+ stats(): Promise<AdapterStats>;
37
+ private encrypt;
38
+ private decrypt;
39
+ }
40
+
41
+ /** AES-256 needs a 32-byte key. */
42
+ declare const KEY_BYTES = 32;
43
+ /** Generates a fresh random 32-byte AES-256 key. Persist it somewhere safe. */
44
+ declare function generateKey(): Buffer;
45
+ /**
46
+ * Derives a 32-byte key from a passphrase and salt using scrypt. Use a stable,
47
+ * unique salt per deployment (store it alongside your config, not the data).
48
+ */
49
+ declare function deriveKey(passphrase: string, salt: string | Buffer): Buffer;
50
+ /**
51
+ * Normalizes a key option into a 32-byte Buffer. Accepts a Buffer, a 64-char hex
52
+ * string, or a 44-char base64 string.
53
+ */
54
+ declare function normalizeKey(key: Buffer | string): Buffer;
55
+
56
+ export { EncryptedAdapter, type EncryptedAdapterOptions, type EncryptedBlob, KEY_BYTES, deriveKey, generateKey, normalizeKey };
@@ -0,0 +1,56 @@
1
+ import { StorageAdapter, Serializer, CacheEntry, AdapterStats } from '@nodellmcache/core';
2
+
3
+ /** The encrypted envelope stored in place of a value. */
4
+ interface EncryptedBlob {
5
+ v: 1;
6
+ iv: string;
7
+ tag: string;
8
+ data: string;
9
+ }
10
+ interface EncryptedAdapterOptions {
11
+ /** Inner adapter that stores the encrypted envelopes (memory, Redis, ...). */
12
+ adapter: StorageAdapter<EncryptedBlob>;
13
+ /** 32-byte key: a Buffer, 64-char hex, or base64. See `generateKey`/`deriveKey`. */
14
+ key: Buffer | string;
15
+ /** Serializer used to turn values into bytes before encryption. Defaults to JSON. */
16
+ serializer?: Serializer;
17
+ }
18
+ /**
19
+ * Wraps any {@link StorageAdapter} and encrypts cached **values** at rest with
20
+ * AES-256-GCM (authenticated, so tampering is detected). Keys, timestamps, and
21
+ * metadata are stored in the clear — only the value is encrypted, and cache keys
22
+ * are already hashed by `KeyBuilder`, so no plaintext prompt content is exposed.
23
+ *
24
+ * Pure `node:crypto`, no native bindings.
25
+ */
26
+ declare class EncryptedAdapter<T = unknown> implements StorageAdapter<T> {
27
+ private readonly inner;
28
+ private readonly key;
29
+ private readonly serializer;
30
+ constructor(options: EncryptedAdapterOptions);
31
+ get(key: string): Promise<CacheEntry<T> | null>;
32
+ set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void>;
33
+ delete(key: string): Promise<void>;
34
+ clear(): Promise<void>;
35
+ has(key: string): Promise<boolean>;
36
+ stats(): Promise<AdapterStats>;
37
+ private encrypt;
38
+ private decrypt;
39
+ }
40
+
41
+ /** AES-256 needs a 32-byte key. */
42
+ declare const KEY_BYTES = 32;
43
+ /** Generates a fresh random 32-byte AES-256 key. Persist it somewhere safe. */
44
+ declare function generateKey(): Buffer;
45
+ /**
46
+ * Derives a 32-byte key from a passphrase and salt using scrypt. Use a stable,
47
+ * unique salt per deployment (store it alongside your config, not the data).
48
+ */
49
+ declare function deriveKey(passphrase: string, salt: string | Buffer): Buffer;
50
+ /**
51
+ * Normalizes a key option into a 32-byte Buffer. Accepts a Buffer, a 64-char hex
52
+ * string, or a 44-char base64 string.
53
+ */
54
+ declare function normalizeKey(key: Buffer | string): Buffer;
55
+
56
+ export { EncryptedAdapter, type EncryptedAdapterOptions, type EncryptedBlob, KEY_BYTES, deriveKey, generateKey, normalizeKey };
package/dist/index.mjs ADDED
@@ -0,0 +1,111 @@
1
+ // src/EncryptedAdapter.ts
2
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
3
+ import { CacheAdapterError, JsonSerializer } from "@nodellmcache/core";
4
+
5
+ // src/keys.ts
6
+ import { randomBytes, scryptSync } from "crypto";
7
+ import { ValidationError } from "@nodellmcache/core";
8
+ var KEY_BYTES = 32;
9
+ function generateKey() {
10
+ return randomBytes(KEY_BYTES);
11
+ }
12
+ function deriveKey(passphrase, salt) {
13
+ return scryptSync(passphrase, salt, KEY_BYTES);
14
+ }
15
+ function normalizeKey(key) {
16
+ let buf;
17
+ if (Buffer.isBuffer(key)) {
18
+ buf = key;
19
+ } else if (/^[0-9a-fA-F]{64}$/.test(key)) {
20
+ buf = Buffer.from(key, "hex");
21
+ } else {
22
+ buf = Buffer.from(key, "base64");
23
+ }
24
+ if (buf.length !== KEY_BYTES) {
25
+ throw new ValidationError(
26
+ `Encryption key must be ${KEY_BYTES} bytes (got ${buf.length}). Use generateKey() or deriveKey(passphrase, salt).`
27
+ );
28
+ }
29
+ return buf;
30
+ }
31
+
32
+ // src/EncryptedAdapter.ts
33
+ var ALGO = "aes-256-gcm";
34
+ var IV_BYTES = 12;
35
+ var EncryptedAdapter = class {
36
+ inner;
37
+ key;
38
+ serializer;
39
+ constructor(options) {
40
+ this.inner = options.adapter;
41
+ this.key = normalizeKey(options.key);
42
+ this.serializer = options.serializer ?? new JsonSerializer();
43
+ }
44
+ async get(key) {
45
+ const stored = await this.inner.get(key);
46
+ if (!stored) return null;
47
+ const value = this.decrypt(stored.value);
48
+ return {
49
+ key: stored.key,
50
+ value,
51
+ createdAt: stored.createdAt,
52
+ expiresAt: stored.expiresAt,
53
+ metadata: stored.metadata
54
+ };
55
+ }
56
+ async set(key, entry, ttl) {
57
+ const stored = {
58
+ key: entry.key,
59
+ value: this.encrypt(entry.value),
60
+ createdAt: entry.createdAt,
61
+ expiresAt: entry.expiresAt,
62
+ metadata: entry.metadata
63
+ };
64
+ await this.inner.set(key, stored, ttl);
65
+ }
66
+ async delete(key) {
67
+ await this.inner.delete(key);
68
+ }
69
+ async clear() {
70
+ await this.inner.clear();
71
+ }
72
+ async has(key) {
73
+ return this.inner.has(key);
74
+ }
75
+ async stats() {
76
+ return this.inner.stats();
77
+ }
78
+ // --- crypto --------------------------------------------------------------
79
+ encrypt(value) {
80
+ const iv = randomBytes2(IV_BYTES);
81
+ const cipher = createCipheriv(ALGO, this.key, iv);
82
+ const plaintext = this.serializer.serialize(value);
83
+ const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
84
+ const tag = cipher.getAuthTag();
85
+ return { v: 1, iv: iv.toString("base64"), tag: tag.toString("base64"), data: data.toString("base64") };
86
+ }
87
+ decrypt(blob) {
88
+ try {
89
+ const decipher = createDecipheriv(ALGO, this.key, Buffer.from(blob.iv, "base64"));
90
+ decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
91
+ const plaintext = Buffer.concat([
92
+ decipher.update(Buffer.from(blob.data, "base64")),
93
+ decipher.final()
94
+ ]);
95
+ return this.serializer.deserialize(plaintext);
96
+ } catch (cause) {
97
+ throw new CacheAdapterError(
98
+ "Failed to decrypt cache entry (wrong key or tampered data)",
99
+ { cause }
100
+ );
101
+ }
102
+ }
103
+ };
104
+ export {
105
+ EncryptedAdapter,
106
+ KEY_BYTES,
107
+ deriveKey,
108
+ generateKey,
109
+ normalizeKey
110
+ };
111
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/EncryptedAdapter.ts","../src/keys.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'\nimport { CacheAdapterError, JsonSerializer } from '@nodellmcache/core'\nimport type { AdapterStats, CacheEntry, Serializer, StorageAdapter } from '@nodellmcache/core'\nimport { normalizeKey } from './keys.js'\n\nconst ALGO = 'aes-256-gcm'\nconst IV_BYTES = 12\n\n/** The encrypted envelope stored in place of a value. */\nexport interface EncryptedBlob {\n v: 1\n iv: string\n tag: string\n data: string\n}\n\nexport interface EncryptedAdapterOptions {\n /** Inner adapter that stores the encrypted envelopes (memory, Redis, ...). */\n adapter: StorageAdapter<EncryptedBlob>\n /** 32-byte key: a Buffer, 64-char hex, or base64. See `generateKey`/`deriveKey`. */\n key: Buffer | string\n /** Serializer used to turn values into bytes before encryption. Defaults to JSON. */\n serializer?: Serializer\n}\n\n/**\n * Wraps any {@link StorageAdapter} and encrypts cached **values** at rest with\n * AES-256-GCM (authenticated, so tampering is detected). Keys, timestamps, and\n * metadata are stored in the clear — only the value is encrypted, and cache keys\n * are already hashed by `KeyBuilder`, so no plaintext prompt content is exposed.\n *\n * Pure `node:crypto`, no native bindings.\n */\nexport class EncryptedAdapter<T = unknown> implements StorageAdapter<T> {\n private readonly inner: StorageAdapter<EncryptedBlob>\n private readonly key: Buffer\n private readonly serializer: Serializer\n\n constructor(options: EncryptedAdapterOptions) {\n this.inner = options.adapter\n this.key = normalizeKey(options.key)\n this.serializer = options.serializer ?? new JsonSerializer()\n }\n\n async get(key: string): Promise<CacheEntry<T> | null> {\n const stored = await this.inner.get(key)\n if (!stored) return null\n const value = this.decrypt(stored.value)\n return {\n key: stored.key,\n value,\n createdAt: stored.createdAt,\n expiresAt: stored.expiresAt,\n metadata: stored.metadata,\n }\n }\n\n async set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void> {\n const stored: CacheEntry<EncryptedBlob> = {\n key: entry.key,\n value: this.encrypt(entry.value),\n createdAt: entry.createdAt,\n expiresAt: entry.expiresAt,\n metadata: entry.metadata,\n }\n await this.inner.set(key, stored, ttl)\n }\n\n async delete(key: string): Promise<void> {\n await this.inner.delete(key)\n }\n\n async clear(): Promise<void> {\n await this.inner.clear()\n }\n\n async has(key: string): Promise<boolean> {\n return this.inner.has(key)\n }\n\n async stats(): Promise<AdapterStats> {\n return this.inner.stats()\n }\n\n // --- crypto --------------------------------------------------------------\n\n private encrypt(value: T): EncryptedBlob {\n const iv = randomBytes(IV_BYTES)\n const cipher = createCipheriv(ALGO, this.key, iv)\n const plaintext = this.serializer.serialize(value)\n const data = Buffer.concat([cipher.update(plaintext), cipher.final()])\n const tag = cipher.getAuthTag()\n return { v: 1, iv: iv.toString('base64'), tag: tag.toString('base64'), data: data.toString('base64') }\n }\n\n private decrypt(blob: EncryptedBlob): T {\n try {\n const decipher = createDecipheriv(ALGO, this.key, Buffer.from(blob.iv, 'base64'))\n decipher.setAuthTag(Buffer.from(blob.tag, 'base64'))\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(blob.data, 'base64')),\n decipher.final(),\n ])\n return this.serializer.deserialize<T>(plaintext)\n } catch (cause) {\n throw new CacheAdapterError(\n 'Failed to decrypt cache entry (wrong key or tampered data)',\n { cause },\n )\n }\n }\n}\n","import { randomBytes, scryptSync } from 'node:crypto'\nimport { ValidationError } from '@nodellmcache/core'\n\n/** AES-256 needs a 32-byte key. */\nexport const KEY_BYTES = 32\n\n/** Generates a fresh random 32-byte AES-256 key. Persist it somewhere safe. */\nexport function generateKey(): Buffer {\n return randomBytes(KEY_BYTES)\n}\n\n/**\n * Derives a 32-byte key from a passphrase and salt using scrypt. Use a stable,\n * unique salt per deployment (store it alongside your config, not the data).\n */\nexport function deriveKey(passphrase: string, salt: string | Buffer): Buffer {\n return scryptSync(passphrase, salt, KEY_BYTES)\n}\n\n/**\n * Normalizes a key option into a 32-byte Buffer. Accepts a Buffer, a 64-char hex\n * string, or a 44-char base64 string.\n */\nexport function normalizeKey(key: Buffer | string): Buffer {\n let buf: Buffer\n if (Buffer.isBuffer(key)) {\n buf = key\n } else if (/^[0-9a-fA-F]{64}$/.test(key)) {\n buf = Buffer.from(key, 'hex')\n } else {\n buf = Buffer.from(key, 'base64')\n }\n if (buf.length !== KEY_BYTES) {\n throw new ValidationError(\n `Encryption key must be ${KEY_BYTES} bytes (got ${buf.length}). ` +\n 'Use generateKey() or deriveKey(passphrase, salt).',\n )\n }\n return buf\n}\n"],"mappings":"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","names":["randomBytes","randomBytes"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@nodellmcache/encryption",
3
+ "version": "1.0.0",
4
+ "description": "AES-256-GCM encrypting storage adapter wrapper for NodeLLMCache",
5
+ "keywords": [
6
+ "llm",
7
+ "cache",
8
+ "encryption",
9
+ "aes",
10
+ "gcm",
11
+ "security",
12
+ "nodejs"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mdmax007/node-llm-cache.git",
18
+ "directory": "packages/encryption"
19
+ },
20
+ "homepage": "https://github.com/mdmax007/node-llm-cache/tree/main/packages/encryption#readme",
21
+ "bugs": "https://github.com/mdmax007/node-llm-cache/issues",
22
+ "type": "module",
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.mjs",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.mjs",
30
+ "require": "./dist/index.cjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "CHANGELOG.md"
37
+ ],
38
+ "peerDependencies": {
39
+ "@nodellmcache/core": "^1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.5.0",
45
+ "vitest": "^2.0.0",
46
+ "@vitest/coverage-v8": "^2.0.0",
47
+ "@nodellmcache/core": "1.0.0",
48
+ "@nodellmcache/memory": "1.0.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "echo \"no lint configured\""
60
+ }
61
+ }