@molecule/api-encryption-aes 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/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser guard — `@molecule/api-encryption-aes` is SERVER-ONLY.
3
+ *
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
10
+ */
11
+ const g = globalThis;
12
+ if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
13
+ throw new Error('@molecule/api-encryption-aes is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
14
+ 'from server code (a server route/function or your API), or dynamic-import it inside ' +
15
+ 'the server handler — never from components or shared client modules, and never ' +
16
+ 'polyfill Buffer/process to silence this.');
17
+ }
18
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * AES-256-GCM encryption provider for molecule.dev.
3
+ *
4
+ * Uses Node.js built-in `crypto` for AES-256-GCM authenticated encryption,
5
+ * SHA-256 hashing, and timing-safe verification. Supports key rotation with
6
+ * versioned ciphertext format.
7
+ *
8
+ * @module
9
+ * @example
10
+ * ```typescript
11
+ * import { setProvider } from '@molecule/api-encryption'
12
+ * import { provider } from '@molecule/api-encryption-aes'
13
+ *
14
+ * // Wire the provider at startup (reads ENCRYPTION_KEY from env)
15
+ * setProvider(provider)
16
+ *
17
+ * // Or create with explicit config
18
+ * import { createProvider } from '@molecule/api-encryption-aes'
19
+ * const customProvider = createProvider({ key: 'your-64-char-hex-key' })
20
+ * setProvider(customProvider)
21
+ * ```
22
+ *
23
+ * @remarks
24
+ * - **`rotateKey()` is transition-safe — it never orphans data.** Keys live in
25
+ * a version-keyed keyring: `encrypt()` tags each ciphertext with the current
26
+ * `v{n}`, `decrypt()` reads that tag and selects the matching key, and
27
+ * `rotateKey(oldKey, newKey)` ADDS `newKey` at the next version while RETAINING
28
+ * the prior key(s). So ciphertext written before a rotation still decrypts
29
+ * afterward (the core contract's "previously encrypted data can still be
30
+ * decrypted during a transition period"). Once you have re-encrypted the old
31
+ * ciphertext under the new key, retire the old keys explicitly with
32
+ * `pruneKeyVersions()` — rotation alone deliberately keeps them.
33
+ * - A ciphertext whose `v{n}` version is not in the keyring (unknown/pruned key)
34
+ * fails cleanly with a descriptive error — never a silent wrong decrypt.
35
+ * - Rotation state is per-process and in-memory: the lazy `provider` singleton
36
+ * starts a fresh keyring at version 1 from `ENCRYPTION_KEY` on each process,
37
+ * so a rotation done in a prior process is not restored. For rotation that
38
+ * survives restarts, build with `createProvider({ key, priorKeys })`, seeding
39
+ * the historical `{ version, key }` entries from your secret store.
40
+ * - `hash()`/`verify()` are plain unsalted SHA-256 — integrity checks only.
41
+ * NEVER use them for passwords; use `@molecule/api-password` with a bond
42
+ * like `@molecule/api-password-bcrypt`.
43
+ * - `encrypt(plaintext, context)`: the optional `context` is GCM AAD — the
44
+ * SAME context string must be supplied to `decrypt()` or authentication
45
+ * fails.
46
+ */
47
+ export * from './browser-guard.js';
48
+ export * from './provider.js';
49
+ export * from './secrets.js';
50
+ export * from './types.js';
51
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * AES-256-GCM encryption provider for molecule.dev.
3
+ *
4
+ * Uses Node.js built-in `crypto` for AES-256-GCM authenticated encryption,
5
+ * SHA-256 hashing, and timing-safe verification. Supports key rotation with
6
+ * versioned ciphertext format.
7
+ *
8
+ * @module
9
+ * @example
10
+ * ```typescript
11
+ * import { setProvider } from '@molecule/api-encryption'
12
+ * import { provider } from '@molecule/api-encryption-aes'
13
+ *
14
+ * // Wire the provider at startup (reads ENCRYPTION_KEY from env)
15
+ * setProvider(provider)
16
+ *
17
+ * // Or create with explicit config
18
+ * import { createProvider } from '@molecule/api-encryption-aes'
19
+ * const customProvider = createProvider({ key: 'your-64-char-hex-key' })
20
+ * setProvider(customProvider)
21
+ * ```
22
+ *
23
+ * @remarks
24
+ * - **`rotateKey()` is transition-safe — it never orphans data.** Keys live in
25
+ * a version-keyed keyring: `encrypt()` tags each ciphertext with the current
26
+ * `v{n}`, `decrypt()` reads that tag and selects the matching key, and
27
+ * `rotateKey(oldKey, newKey)` ADDS `newKey` at the next version while RETAINING
28
+ * the prior key(s). So ciphertext written before a rotation still decrypts
29
+ * afterward (the core contract's "previously encrypted data can still be
30
+ * decrypted during a transition period"). Once you have re-encrypted the old
31
+ * ciphertext under the new key, retire the old keys explicitly with
32
+ * `pruneKeyVersions()` — rotation alone deliberately keeps them.
33
+ * - A ciphertext whose `v{n}` version is not in the keyring (unknown/pruned key)
34
+ * fails cleanly with a descriptive error — never a silent wrong decrypt.
35
+ * - Rotation state is per-process and in-memory: the lazy `provider` singleton
36
+ * starts a fresh keyring at version 1 from `ENCRYPTION_KEY` on each process,
37
+ * so a rotation done in a prior process is not restored. For rotation that
38
+ * survives restarts, build with `createProvider({ key, priorKeys })`, seeding
39
+ * the historical `{ version, key }` entries from your secret store.
40
+ * - `hash()`/`verify()` are plain unsalted SHA-256 — integrity checks only.
41
+ * NEVER use them for passwords; use `@molecule/api-password` with a bond
42
+ * like `@molecule/api-password-bcrypt`.
43
+ * - `encrypt(plaintext, context)`: the optional `context` is GCM AAD — the
44
+ * SAME context string must be supplied to `decrypt()` or authentication
45
+ * fails.
46
+ */
47
+ export * from './browser-guard.js';
48
+ export * from './provider.js';
49
+ export * from './secrets.js';
50
+ export * from './types.js';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * AES-256-GCM implementation of `EncryptionProvider`.
3
+ *
4
+ * Uses Node.js built-in `crypto` module for all operations:
5
+ * - **encrypt/decrypt**: AES-256-GCM with random IV and authentication tag
6
+ * - **hash**: SHA-256
7
+ * - **verify**: Timing-safe comparison of SHA-256 hashes
8
+ * - **rotateKey**: Adds the new key to a versioned keyring (retaining prior
9
+ * keys) so ciphertext encrypted before the rotation still decrypts
10
+ *
11
+ * Ciphertext format: `v{keyVersion}:{iv}:{authTag}:{ciphertext}` (all
12
+ * hex-encoded). `decrypt()` reads the `v{n}` tag and selects the matching key
13
+ * from the keyring — so a rotation never orphans existing data.
14
+ *
15
+ * @module
16
+ */
17
+ import './secrets.js';
18
+ import type { AesConfig, AesEncryptionProvider } from './types.js';
19
+ /**
20
+ * Creates an AES-256-GCM encryption provider.
21
+ *
22
+ * Rotation is transition-safe: keys are held in a version-keyed keyring.
23
+ * `encrypt()` tags each ciphertext with the current key version, `decrypt()`
24
+ * selects the key named by that tag, and `rotateKey()` ADDS the new key while
25
+ * keeping prior keys — so ciphertext from before a rotation still decrypts.
26
+ * Retire old keys explicitly (once their data is re-encrypted) via
27
+ * `pruneKeyVersions()`.
28
+ *
29
+ * @param config - Provider configuration including the hex-encoded 256-bit key.
30
+ * @returns An `AesEncryptionProvider` using AES-256-GCM.
31
+ */
32
+ export declare const createProvider: (config: AesConfig) => AesEncryptionProvider;
33
+ /**
34
+ * Default AES-256-GCM encryption provider instance.
35
+ *
36
+ * Lazily initializes on first property access using the `ENCRYPTION_KEY`
37
+ * environment variable (hex-encoded 256-bit key). This singleton starts a
38
+ * fresh keyring at version 1 on each process, so a rotation performed in a
39
+ * prior process is NOT restored here — for rotation that survives restarts,
40
+ * construct with `createProvider({ key, priorKeys })` seeding the historical
41
+ * keys from your secret store.
42
+ *
43
+ * @throws {Error} If `ENCRYPTION_KEY` environment variable is not set.
44
+ */
45
+ export declare const provider: AesEncryptionProvider;
46
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAaH,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AA0ClE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,SAAS,KAAG,qBAoHlD,CAAA;AAKD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,EAAE,qBA0BrB,CAAA"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * AES-256-GCM implementation of `EncryptionProvider`.
3
+ *
4
+ * Uses Node.js built-in `crypto` module for all operations:
5
+ * - **encrypt/decrypt**: AES-256-GCM with random IV and authentication tag
6
+ * - **hash**: SHA-256
7
+ * - **verify**: Timing-safe comparison of SHA-256 hashes
8
+ * - **rotateKey**: Adds the new key to a versioned keyring (retaining prior
9
+ * keys) so ciphertext encrypted before the rotation still decrypts
10
+ *
11
+ * Ciphertext format: `v{keyVersion}:{iv}:{authTag}:{ciphertext}` (all
12
+ * hex-encoded). `decrypt()` reads the `v{n}` tag and selects the matching key
13
+ * from the keyring — so a rotation never orphans existing data.
14
+ *
15
+ * @module
16
+ */
17
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual, } from 'node:crypto';
18
+ // Side-effect import: registers this bond's secret definitions so the
19
+ // runtime registry is populated even when provider.js is imported directly
20
+ // (not through the package barrel).
21
+ import './secrets.js';
22
+ /** AES-256-GCM algorithm identifier. */
23
+ const ALGORITHM = 'aes-256-gcm';
24
+ /** IV length in bytes (96-bit / 12 bytes recommended for GCM). */
25
+ const IV_LENGTH = 12;
26
+ /** Authentication tag length in bytes. */
27
+ const AUTH_TAG_LENGTH = 16;
28
+ /** Parses the `v{n}` key-version prefix of a ciphertext tag. */
29
+ const parseKeyVersion = (tag) => {
30
+ const match = /^v(\d+)$/.exec(tag);
31
+ return match ? Number(match[1]) : null;
32
+ };
33
+ /** A 256-bit key is exactly 64 hex characters. */
34
+ const HEX_256_BIT = /^[0-9a-fA-F]{64}$/;
35
+ /**
36
+ * Decode a hex-encoded 256-bit key, refusing anything that isn't exactly 64 hex
37
+ * chars. `Buffer.from(x, 'hex')` SILENTLY truncates at the first non-hex byte
38
+ * and silently drops an odd trailing nibble, so a typo'd or short key would
39
+ * otherwise yield a wrong-length buffer that either 500s on every encrypt or —
40
+ * worse — encrypts under a key nobody can reproduce. Fail loud at construction.
41
+ *
42
+ * @param key - The hex-encoded key to validate and decode.
43
+ * @param label - Human context for the error (e.g. `'ENCRYPTION_KEY'`).
44
+ * @returns The 32-byte key buffer.
45
+ * @throws {Error} When the key is not exactly 64 hex characters.
46
+ */
47
+ const decodeKey = (key, label) => {
48
+ if (!HEX_256_BIT.test(key)) {
49
+ throw new Error(`Invalid AES ${label}: expected exactly 64 hex characters (a 256-bit key); ` +
50
+ `got ${key.length} character(s). Generate one with \`openssl rand -hex 32\`.`);
51
+ }
52
+ return Buffer.from(key, 'hex');
53
+ };
54
+ /**
55
+ * Creates an AES-256-GCM encryption provider.
56
+ *
57
+ * Rotation is transition-safe: keys are held in a version-keyed keyring.
58
+ * `encrypt()` tags each ciphertext with the current key version, `decrypt()`
59
+ * selects the key named by that tag, and `rotateKey()` ADDS the new key while
60
+ * keeping prior keys — so ciphertext from before a rotation still decrypts.
61
+ * Retire old keys explicitly (once their data is re-encrypted) via
62
+ * `pruneKeyVersions()`.
63
+ *
64
+ * @param config - Provider configuration including the hex-encoded 256-bit key.
65
+ * @returns An `AesEncryptionProvider` using AES-256-GCM.
66
+ */
67
+ export const createProvider = (config) => {
68
+ let currentVersion = config.keyVersion ?? 1;
69
+ let currentKey = decodeKey(config.key, 'key');
70
+ // Version -> key buffer. Rotation adds a new version and retains prior keys,
71
+ // so ciphertext tagged with an older `v{n}` still decrypts. Seed historical
72
+ // keys first, then set the current one so it wins on any version collision.
73
+ const keyring = new Map();
74
+ for (const prior of config.priorKeys ?? []) {
75
+ keyring.set(prior.version, decodeKey(prior.key, `priorKeys[v${prior.version}]`));
76
+ }
77
+ keyring.set(currentVersion, currentKey);
78
+ return {
79
+ async encrypt(plaintext, context) {
80
+ const iv = randomBytes(IV_LENGTH);
81
+ const cipher = createCipheriv(ALGORITHM, currentKey, iv, { authTagLength: AUTH_TAG_LENGTH });
82
+ if (context) {
83
+ cipher.setAAD(Buffer.from(context, 'utf-8'));
84
+ }
85
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
86
+ const authTag = cipher.getAuthTag();
87
+ return `v${currentVersion}:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
88
+ },
89
+ async decrypt(ciphertext, context) {
90
+ const parts = ciphertext.split(':');
91
+ if (parts.length !== 4) {
92
+ throw new Error('Invalid ciphertext format');
93
+ }
94
+ const version = parseKeyVersion(parts[0]);
95
+ if (version === null) {
96
+ throw new Error('Invalid ciphertext format: missing or malformed key version prefix');
97
+ }
98
+ // Select the key that ENCRYPTED this ciphertext by its version tag — not
99
+ // the current key — so pre-rotation ciphertext decrypts with its own key.
100
+ const key = keyring.get(version);
101
+ if (!key) {
102
+ throw new Error(`No encryption key available for key version ${version}; ` +
103
+ `seed it via priorKeys or do not prune it before re-encrypting its ciphertext`);
104
+ }
105
+ const iv = Buffer.from(parts[1], 'hex');
106
+ const authTag = Buffer.from(parts[2], 'hex');
107
+ const encrypted = Buffer.from(parts[3], 'hex');
108
+ const decipher = createDecipheriv(ALGORITHM, key, iv, {
109
+ authTagLength: AUTH_TAG_LENGTH,
110
+ });
111
+ decipher.setAuthTag(authTag);
112
+ if (context) {
113
+ decipher.setAAD(Buffer.from(context, 'utf-8'));
114
+ }
115
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
116
+ return decrypted.toString('utf-8');
117
+ },
118
+ async hash(data) {
119
+ return createHash('sha256').update(data, 'utf-8').digest('hex');
120
+ },
121
+ async verify(data, hashed) {
122
+ const computed = createHash('sha256').update(data, 'utf-8').digest('hex');
123
+ if (computed.length !== hashed.length) {
124
+ return false;
125
+ }
126
+ return timingSafeEqual(Buffer.from(computed, 'utf-8'), Buffer.from(hashed, 'utf-8'));
127
+ },
128
+ async rotateKey(oldKey, newKey) {
129
+ // Validate the NEW key before anything else: a malformed newKey must not
130
+ // become currentKey (it would encrypt future data under a wrong-length /
131
+ // truncated key). oldKey is only compared, so a bad one just fails to match.
132
+ const newBuffer = decodeKey(newKey, 'newKey');
133
+ const oldBuffer = Buffer.from(oldKey, 'hex');
134
+ if (oldBuffer.length !== currentKey.length || !timingSafeEqual(oldBuffer, currentKey)) {
135
+ throw new Error('Old key does not match the current encryption key');
136
+ }
137
+ // Add the new key at the next version and make it current. The prior key
138
+ // stays in the keyring so ciphertext tagged with the old version still
139
+ // decrypts — rotation NEVER orphans existing data. Retire old keys
140
+ // explicitly with pruneKeyVersions() once their data is re-encrypted.
141
+ currentVersion += 1;
142
+ currentKey = newBuffer;
143
+ keyring.set(currentVersion, currentKey);
144
+ },
145
+ pruneKeyVersions(keep) {
146
+ const retained = new Set([currentVersion, ...(keep ?? [])]);
147
+ const pruned = [];
148
+ for (const version of keyring.keys()) {
149
+ if (!retained.has(version)) {
150
+ pruned.push(version);
151
+ }
152
+ }
153
+ for (const version of pruned) {
154
+ keyring.delete(version);
155
+ }
156
+ return pruned;
157
+ },
158
+ };
159
+ };
160
+ /** Lazily-initialized default provider instance. */
161
+ let _provider = null;
162
+ /**
163
+ * Default AES-256-GCM encryption provider instance.
164
+ *
165
+ * Lazily initializes on first property access using the `ENCRYPTION_KEY`
166
+ * environment variable (hex-encoded 256-bit key). This singleton starts a
167
+ * fresh keyring at version 1 on each process, so a rotation performed in a
168
+ * prior process is NOT restored here — for rotation that survives restarts,
169
+ * construct with `createProvider({ key, priorKeys })` seeding the historical
170
+ * keys from your secret store.
171
+ *
172
+ * @throws {Error} If `ENCRYPTION_KEY` environment variable is not set.
173
+ */
174
+ export const provider = new Proxy({}, {
175
+ get(_, prop, receiver) {
176
+ if (!_provider) {
177
+ const key = process.env['ENCRYPTION_KEY'];
178
+ if (!key) {
179
+ throw new Error('ENCRYPTION_KEY environment variable is required for AES encryption provider');
180
+ }
181
+ _provider = createProvider({ key });
182
+ }
183
+ return Reflect.get(_provider, prop, receiver);
184
+ },
185
+ // set trap: methods run with `this` bound to the proxy — without it, instance-state writes land on the dummy target and are lost (see api-push-notifications-web-push)
186
+ set(_, prop, value) {
187
+ if (!_provider) {
188
+ const key = process.env['ENCRYPTION_KEY'];
189
+ if (!key) {
190
+ throw new Error('ENCRYPTION_KEY environment variable is required for AES encryption provider');
191
+ }
192
+ _provider = createProvider({ key });
193
+ }
194
+ return Reflect.set(_provider, prop, value);
195
+ },
196
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * AES encryption secret definitions — self-registered at import time so the
3
+ * runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
4
+ * configuration reports and actionable "not configured" errors.
5
+ *
6
+ * Content is derived MECHANICALLY from this package's mlcl registry secrets
7
+ * entry (label/instructions/setupUrl/example) via the fleet formula, so
8
+ * packages sharing a key register byte-identical definitions and
9
+ * registration order never matters.
10
+ *
11
+ * @module
12
+ */
13
+ import type { SecretDefinition } from '@molecule/api-secrets';
14
+ /** Secret definitions required by the AES encryption bond. */
15
+ export declare const encryptionAesSecretDefinitions: SecretDefinition[];
16
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAG7D,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,EAAE,gBAAgB,EAO5D,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * AES encryption secret definitions — self-registered at import time so the
3
+ * runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
4
+ * configuration reports and actionable "not configured" errors.
5
+ *
6
+ * Content is derived MECHANICALLY from this package's mlcl registry secrets
7
+ * entry (label/instructions/setupUrl/example) via the fleet formula, so
8
+ * packages sharing a key register byte-identical definitions and
9
+ * registration order never matters.
10
+ *
11
+ * @module
12
+ */
13
+ import { registerSecrets } from '@molecule/api-secrets';
14
+ /** Secret definitions required by the AES encryption bond. */
15
+ export const encryptionAesSecretDefinitions = [
16
+ {
17
+ key: 'ENCRYPTION_KEY',
18
+ description: 'AES-256 encryption key — 64-character hex (256-bit) key for AES-GCM encryption. Auto-generated at scaffold — no manual setup needed.',
19
+ required: true,
20
+ },
21
+ ];
22
+ registerSecrets(encryptionAesSecretDefinitions);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * AES-256-GCM encryption provider configuration.
3
+ *
4
+ * @module
5
+ */
6
+ import type { EncryptionProvider } from '@molecule/api-encryption';
7
+ /**
8
+ * A historical (pre-rotation) key, retained so ciphertext encrypted under it
9
+ * remains decryptable.
10
+ *
11
+ * Ciphertext is tagged with the `v{version}` of the key that produced it; the
12
+ * provider selects the matching key from its keyring at decrypt time. Seed the
13
+ * keyring with the app's prior keys (from a secret store) so rotations survive
14
+ * a process restart — see {@link AesConfig.priorKeys}.
15
+ */
16
+ export interface PriorKey {
17
+ /** The key version tag this key decrypts (matches the `v{n}` ciphertext prefix). */
18
+ version: number;
19
+ /** The 256-bit key, hex-encoded (64 hex characters). */
20
+ key: string;
21
+ }
22
+ /**
23
+ * Configuration options for the AES-256-GCM encryption provider.
24
+ */
25
+ export interface AesConfig {
26
+ /** The current 256-bit encryption key, hex-encoded (64 hex characters). */
27
+ key: string;
28
+ /**
29
+ * Version of the current `key`. Ciphertext produced by `encrypt()` is tagged
30
+ * with this version; `decrypt()` selects the key to use from the `v{n}` tag,
31
+ * so it must be stable for a given key. `rotateKey()` advances it.
32
+ *
33
+ * @default 1
34
+ */
35
+ keyVersion?: number;
36
+ /**
37
+ * Prior keys to seed the keyring with, so ciphertext encrypted before a
38
+ * rotation still decrypts after this provider is (re)constructed — e.g. on a
39
+ * process restart, when the in-memory rotation state would otherwise be lost.
40
+ * Each entry maps a historical `v{n}` version to its key. The current `key`
41
+ * (at {@link AesConfig.keyVersion}) always wins over any prior entry sharing
42
+ * its version.
43
+ *
44
+ * @default []
45
+ */
46
+ priorKeys?: PriorKey[];
47
+ }
48
+ /**
49
+ * AES-256-GCM encryption provider.
50
+ *
51
+ * Fully satisfies the core `EncryptionProvider` contract, so it bonds anywhere
52
+ * an `EncryptionProvider` is expected. It additionally exposes
53
+ * {@link AesEncryptionProvider.pruneKeyVersions} to explicitly retire old keys
54
+ * once their ciphertext has been re-encrypted — rotation alone never drops a
55
+ * key, so it can never orphan data.
56
+ */
57
+ export interface AesEncryptionProvider extends EncryptionProvider {
58
+ /**
59
+ * Retires key versions from the in-memory keyring. Call this only AFTER every
60
+ * ciphertext encrypted under those versions has been re-encrypted under the
61
+ * current key — a pruned version can no longer decrypt its ciphertext.
62
+ *
63
+ * The current key version is ALWAYS retained (it cannot be pruned). By
64
+ * default every non-current version is removed; pass `keep` to retain
65
+ * specific older versions during a staged migration.
66
+ *
67
+ * @param keep - Old versions to retain in addition to the current one.
68
+ * @returns The versions that were removed.
69
+ */
70
+ pruneKeyVersions(keep?: number[]): number[];
71
+ }
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAElE;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAA;IAEf,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAA;IAEX;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;CAC5C"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * AES-256-GCM encryption provider configuration.
3
+ *
4
+ * @module
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@molecule/api-encryption-aes",
3
+ "version": "1.0.0",
4
+ "description": "AES-256-GCM encryption provider for molecule.dev — authenticated encryption, SHA-256 hashing, and key rotation",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "molecule",
24
+ "encryption",
25
+ "aes",
26
+ "aes-256-gcm",
27
+ "field-level-encryption",
28
+ "key-rotation"
29
+ ],
30
+ "license": "Apache-2.0",
31
+ "peerDependencies": {
32
+ "@molecule/api-encryption": "^1.0.0",
33
+ "@molecule/api-secrets": "^1.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@molecule/api-encryption": "1.0.0",
37
+ "@types/node": "26.1.2",
38
+ "typescript": "6.0.3",
39
+ "vitest": "4.1.10"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/molecule-dev/molecule.git",
44
+ "directory": "packages/api/bonds/encryption/aes"
45
+ },
46
+ "homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/bonds/encryption/aes",
47
+ "bugs": "https://github.com/molecule-dev/molecule/issues",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }