@happyvertical/secrets 0.80.0 → 0.80.2
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/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +683 -703
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,321 +1,372 @@
|
|
|
1
1
|
import { syncSchema } from "@happyvertical/sql";
|
|
2
2
|
import { createId } from "@happyvertical/utils";
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/shared/envelope.ts
|
|
17
|
+
/**
|
|
18
|
+
* Core envelope encryption primitives using AES-256-GCM
|
|
19
|
+
*
|
|
20
|
+
* This class provides the cryptographic operations for envelope encryption:
|
|
21
|
+
* - Generate data encryption keys (DEKs)
|
|
22
|
+
* - Wrap/unwrap DEKs with a master key
|
|
23
|
+
* - Encrypt/decrypt data with DEKs
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Generate a new data key
|
|
28
|
+
* const dataKey = EnvelopeEncryption.generateDataKey();
|
|
29
|
+
*
|
|
30
|
+
* // Wrap the data key with an AMK
|
|
31
|
+
* const wrapped = EnvelopeEncryption.wrapKey(dataKey, amk);
|
|
32
|
+
*
|
|
33
|
+
* // Encrypt data with the data key
|
|
34
|
+
* const encrypted = EnvelopeEncryption.encryptData('secret', dataKey);
|
|
35
|
+
*
|
|
36
|
+
* // Later: unwrap and decrypt
|
|
37
|
+
* const unwrappedKey = EnvelopeEncryption.unwrapKey(wrapped.wrappedKey, wrapped.iv, wrapped.authTag, amk);
|
|
38
|
+
* const plaintext = EnvelopeEncryption.decryptData(encrypted.ciphertext, encrypted.iv, encrypted.authTag, unwrappedKey);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
var EnvelopeEncryption = class EnvelopeEncryption {
|
|
42
|
+
static ALGORITHM = "aes-256-gcm";
|
|
43
|
+
static KEY_LENGTH = 32;
|
|
44
|
+
static IV_LENGTH = 12;
|
|
45
|
+
static AUTH_TAG_LENGTH = 16;
|
|
46
|
+
/**
|
|
47
|
+
* Generate a new random data encryption key
|
|
48
|
+
*
|
|
49
|
+
* @returns A 32-byte (256-bit) random key
|
|
50
|
+
*/
|
|
51
|
+
static generateDataKey() {
|
|
52
|
+
return crypto.randomBytes(EnvelopeEncryption.KEY_LENGTH);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate a random IV for encryption
|
|
56
|
+
*
|
|
57
|
+
* @returns A 12-byte (96-bit) random IV
|
|
58
|
+
*/
|
|
59
|
+
static generateIV() {
|
|
60
|
+
return crypto.randomBytes(EnvelopeEncryption.IV_LENGTH);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Wrap (encrypt) a data encryption key with a master key
|
|
64
|
+
*
|
|
65
|
+
* @param dataKey - The data key to wrap (32 bytes)
|
|
66
|
+
* @param masterKey - The master key to wrap with (32 bytes)
|
|
67
|
+
* @returns Object containing wrapped key, IV, and auth tag (all base64-encoded)
|
|
68
|
+
*/
|
|
69
|
+
static wrapKey(dataKey, masterKey) {
|
|
70
|
+
EnvelopeEncryption.validateKeyLength(dataKey);
|
|
71
|
+
EnvelopeEncryption.validateKeyLength(masterKey);
|
|
72
|
+
const iv = EnvelopeEncryption.generateIV();
|
|
73
|
+
const cipher = crypto.createCipheriv(EnvelopeEncryption.ALGORITHM, masterKey, iv);
|
|
74
|
+
const encrypted = Buffer.concat([cipher.update(dataKey), cipher.final()]);
|
|
75
|
+
const authTag = cipher.getAuthTag();
|
|
76
|
+
return {
|
|
77
|
+
wrappedKey: encrypted.toString("base64"),
|
|
78
|
+
iv: iv.toString("base64"),
|
|
79
|
+
authTag: authTag.toString("base64")
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Unwrap (decrypt) a data encryption key with a master key
|
|
84
|
+
*
|
|
85
|
+
* @param wrappedKey - Base64-encoded wrapped key
|
|
86
|
+
* @param iv - Base64-encoded IV
|
|
87
|
+
* @param authTag - Base64-encoded authentication tag
|
|
88
|
+
* @param masterKey - The master key to unwrap with (32 bytes)
|
|
89
|
+
* @returns The unwrapped data key (32 bytes)
|
|
90
|
+
* @throws Error if decryption fails (invalid key, tampered data, etc.)
|
|
91
|
+
*/
|
|
92
|
+
static unwrapKey(wrappedKey, iv, authTag, masterKey) {
|
|
93
|
+
EnvelopeEncryption.validateKeyLength(masterKey);
|
|
94
|
+
const decipher = crypto.createDecipheriv(EnvelopeEncryption.ALGORITHM, masterKey, Buffer.from(iv, "base64"));
|
|
95
|
+
decipher.setAuthTag(Buffer.from(authTag, "base64"));
|
|
96
|
+
return Buffer.concat([decipher.update(Buffer.from(wrappedKey, "base64")), decipher.final()]);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Encrypt data with a data encryption key
|
|
100
|
+
*
|
|
101
|
+
* @param plaintext - The plaintext string to encrypt
|
|
102
|
+
* @param dataKey - The data encryption key (32 bytes)
|
|
103
|
+
* @returns Object containing ciphertext, IV, and auth tag (all base64-encoded)
|
|
104
|
+
*/
|
|
105
|
+
static encryptData(plaintext, dataKey) {
|
|
106
|
+
EnvelopeEncryption.validateKeyLength(dataKey);
|
|
107
|
+
const iv = EnvelopeEncryption.generateIV();
|
|
108
|
+
const cipher = crypto.createCipheriv(EnvelopeEncryption.ALGORITHM, dataKey, iv);
|
|
109
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
110
|
+
const authTag = cipher.getAuthTag();
|
|
111
|
+
return {
|
|
112
|
+
ciphertext: encrypted.toString("base64"),
|
|
113
|
+
iv: iv.toString("base64"),
|
|
114
|
+
authTag: authTag.toString("base64")
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Decrypt data with a data encryption key
|
|
119
|
+
*
|
|
120
|
+
* @param ciphertext - Base64-encoded ciphertext
|
|
121
|
+
* @param iv - Base64-encoded IV
|
|
122
|
+
* @param authTag - Base64-encoded authentication tag
|
|
123
|
+
* @param dataKey - The data encryption key (32 bytes)
|
|
124
|
+
* @returns The decrypted plaintext string
|
|
125
|
+
* @throws Error if decryption fails (invalid key, tampered data, etc.)
|
|
126
|
+
*/
|
|
127
|
+
static decryptData(ciphertext, iv, authTag, dataKey) {
|
|
128
|
+
EnvelopeEncryption.validateKeyLength(dataKey);
|
|
129
|
+
const decipher = crypto.createDecipheriv(EnvelopeEncryption.ALGORITHM, dataKey, Buffer.from(iv, "base64"));
|
|
130
|
+
decipher.setAuthTag(Buffer.from(authTag, "base64"));
|
|
131
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64")), decipher.final()]).toString("utf8");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Create a full encrypted envelope
|
|
135
|
+
*
|
|
136
|
+
* This combines key wrapping and data encryption into a single envelope
|
|
137
|
+
* that can be stored and later decrypted.
|
|
138
|
+
*
|
|
139
|
+
* @param plaintext - The plaintext string to encrypt
|
|
140
|
+
* @param dataKey - The data encryption key (32 bytes)
|
|
141
|
+
* @param wrappedKeyInfo - The wrapped key info (from wrapKey)
|
|
142
|
+
* @param amkKeyId - The ID of the AMK used to wrap the key
|
|
143
|
+
* @param metadata - Optional metadata to include in the envelope
|
|
144
|
+
* @returns A complete encrypted envelope
|
|
145
|
+
*/
|
|
146
|
+
static createEnvelope(plaintext, dataKey, wrappedKeyInfo, amkKeyId, metadata) {
|
|
147
|
+
const encrypted = EnvelopeEncryption.encryptData(plaintext, dataKey);
|
|
148
|
+
return {
|
|
149
|
+
version: 1,
|
|
150
|
+
algorithm: "aes-256-gcm",
|
|
151
|
+
wrappedKey: `${wrappedKeyInfo.wrappedKey}:${wrappedKeyInfo.iv}:${wrappedKeyInfo.authTag}`,
|
|
152
|
+
amkKeyId,
|
|
153
|
+
iv: encrypted.iv,
|
|
154
|
+
authTag: encrypted.authTag,
|
|
155
|
+
ciphertext: encrypted.ciphertext,
|
|
156
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
|
+
metadata
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Parse a combined wrapped key string
|
|
162
|
+
*
|
|
163
|
+
* @param combinedWrappedKey - The combined wrapped key string (wrappedKey:iv:authTag)
|
|
164
|
+
* @returns Object with wrappedKey, iv, and authTag
|
|
165
|
+
*/
|
|
166
|
+
static parseWrappedKey(combinedWrappedKey) {
|
|
167
|
+
const parts = combinedWrappedKey.split(":");
|
|
168
|
+
if (parts.length !== 3) throw new Error("Invalid wrapped key format: expected wrappedKey:iv:authTag");
|
|
169
|
+
const [wrappedKey, iv, authTag] = parts;
|
|
170
|
+
if (!wrappedKey || !iv || !authTag) throw new Error("Invalid wrapped key format: wrappedKey, iv, and authTag must all be non-empty");
|
|
171
|
+
return {
|
|
172
|
+
wrappedKey,
|
|
173
|
+
iv,
|
|
174
|
+
authTag
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Decrypt a full envelope
|
|
179
|
+
*
|
|
180
|
+
* @param envelope - The encrypted envelope
|
|
181
|
+
* @param masterKey - The master key to unwrap the data key
|
|
182
|
+
* @returns The decrypted plaintext string
|
|
183
|
+
*/
|
|
184
|
+
static decryptEnvelope(envelope, masterKey) {
|
|
185
|
+
const { wrappedKey, iv: keyIv, authTag: keyAuthTag } = EnvelopeEncryption.parseWrappedKey(envelope.wrappedKey);
|
|
186
|
+
const dataKey = EnvelopeEncryption.unwrapKey(wrappedKey, keyIv, keyAuthTag, masterKey);
|
|
187
|
+
return EnvelopeEncryption.decryptData(envelope.ciphertext, envelope.iv, envelope.authTag, dataKey);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Validate that a key is the correct length
|
|
191
|
+
*
|
|
192
|
+
* @param key - The key to validate
|
|
193
|
+
* @param expectedLength - Expected length in bytes (default: 32)
|
|
194
|
+
* @throws Error if key length is invalid
|
|
195
|
+
*/
|
|
196
|
+
static validateKeyLength(key, expectedLength = EnvelopeEncryption.KEY_LENGTH) {
|
|
197
|
+
if (key.length !== expectedLength) throw new Error(`Invalid key length: expected ${expectedLength} bytes, got ${key.length} bytes`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Parse a hex-encoded key from string
|
|
201
|
+
*
|
|
202
|
+
* @param hexKey - Hex-encoded key string (64 chars for 32 bytes)
|
|
203
|
+
* @returns The key as a Buffer
|
|
204
|
+
* @throws Error if the hex string is invalid
|
|
205
|
+
*/
|
|
206
|
+
static parseHexKey(hexKey) {
|
|
207
|
+
if (!/^[0-9a-fA-F]+$/.test(hexKey)) throw new Error("Invalid hex key: contains non-hex characters");
|
|
208
|
+
const key = Buffer.from(hexKey, "hex");
|
|
209
|
+
EnvelopeEncryption.validateKeyLength(key);
|
|
210
|
+
return key;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/shared/errors.ts
|
|
215
|
+
/**
|
|
216
|
+
* Base error class for secret operations
|
|
217
|
+
*/
|
|
218
|
+
var SecretError = class extends Error {
|
|
219
|
+
code;
|
|
220
|
+
adapterType;
|
|
221
|
+
cause;
|
|
222
|
+
constructor(message, code, options) {
|
|
223
|
+
super(message);
|
|
224
|
+
this.name = "SecretError";
|
|
225
|
+
this.code = code;
|
|
226
|
+
this.adapterType = options?.adapterType;
|
|
227
|
+
this.cause = options?.cause;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Error thrown when a key is not found
|
|
232
|
+
*/
|
|
233
|
+
var KeyNotFoundError = class extends SecretError {
|
|
234
|
+
constructor(keyId, adapterType) {
|
|
235
|
+
super(`Key not found: ${keyId}`, "KEY_NOT_FOUND", { adapterType });
|
|
236
|
+
this.name = "KeyNotFoundError";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Error thrown when decryption fails
|
|
241
|
+
*/
|
|
242
|
+
var DecryptionError = class extends SecretError {
|
|
243
|
+
constructor(message, adapterType, cause) {
|
|
244
|
+
super(message, "DECRYPTION_FAILED", {
|
|
245
|
+
adapterType,
|
|
246
|
+
cause
|
|
247
|
+
});
|
|
248
|
+
this.name = "DecryptionError";
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Error thrown when encryption fails
|
|
253
|
+
*/
|
|
254
|
+
var EncryptionError = class extends SecretError {
|
|
255
|
+
constructor(message, adapterType, cause) {
|
|
256
|
+
super(message, "ENCRYPTION_FAILED", {
|
|
257
|
+
adapterType,
|
|
258
|
+
cause
|
|
259
|
+
});
|
|
260
|
+
this.name = "EncryptionError";
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Error thrown when key rotation fails
|
|
265
|
+
*/
|
|
266
|
+
var KeyRotationError = class extends SecretError {
|
|
267
|
+
constructor(message, adapterType, cause) {
|
|
268
|
+
super(message, "KEY_ROTATION_FAILED", {
|
|
269
|
+
adapterType,
|
|
270
|
+
cause
|
|
271
|
+
});
|
|
272
|
+
this.name = "KeyRotationError";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Error thrown when a tenant's encryption key is missing
|
|
277
|
+
*/
|
|
278
|
+
var TenantKeyMissingError = class extends SecretError {
|
|
279
|
+
constructor(tenantId) {
|
|
280
|
+
super(`No active encryption key for tenant: ${tenantId}`, "TENANT_KEY_MISSING");
|
|
281
|
+
this.name = "TenantKeyMissingError";
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* Error thrown when the Application Master Key is unavailable
|
|
286
|
+
*/
|
|
287
|
+
var AMKUnavailableError = class extends SecretError {
|
|
288
|
+
constructor(message) {
|
|
289
|
+
super(message, "AMK_UNAVAILABLE");
|
|
290
|
+
this.name = "AMKUnavailableError";
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Error thrown when the store is not initialized
|
|
295
|
+
*/
|
|
296
|
+
var StoreNotInitializedError = class extends SecretError {
|
|
297
|
+
constructor(adapterType) {
|
|
298
|
+
super("Secret store not initialized", "STORE_NOT_INITIALIZED", { adapterType });
|
|
299
|
+
this.name = "StoreNotInitializedError";
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Error thrown when an invalid key format is encountered
|
|
304
|
+
*/
|
|
305
|
+
var InvalidKeyFormatError = class extends SecretError {
|
|
306
|
+
constructor(message, adapterType) {
|
|
307
|
+
super(message, "INVALID_KEY_FORMAT", { adapterType });
|
|
308
|
+
this.name = "InvalidKeyFormatError";
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/adapters/database.ts
|
|
313
|
+
var database_exports = /* @__PURE__ */ __exportAll({
|
|
314
|
+
DatabaseSecretStore: () => DatabaseSecretStore,
|
|
315
|
+
default: () => DatabaseSecretStore
|
|
316
|
+
});
|
|
317
|
+
/**
|
|
318
|
+
* Default rotation period in milliseconds (90 days)
|
|
319
|
+
*/
|
|
320
|
+
var DEFAULT_ROTATION_PERIOD_MS = 2160 * 60 * 60 * 1e3;
|
|
321
|
+
/**
|
|
322
|
+
* Database-backed secret store
|
|
323
|
+
*
|
|
324
|
+
* Uses envelope encryption with AES-256-GCM:
|
|
325
|
+
* - Application Master Key (AMK) from environment variable
|
|
326
|
+
* - Per-tenant Data Encryption Keys (TDEKs) wrapped by AMK
|
|
327
|
+
* - Secrets encrypted by unwrapped TDEKs
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```typescript
|
|
331
|
+
* const store = new DatabaseSecretStore({
|
|
332
|
+
* type: 'database',
|
|
333
|
+
* db,
|
|
334
|
+
* amk: {
|
|
335
|
+
* provider: 'env',
|
|
336
|
+
* keyEnvVar: 'SECRET_MASTER_KEY',
|
|
337
|
+
* keyId: 'amk-v1'
|
|
338
|
+
* }
|
|
339
|
+
* });
|
|
340
|
+
*
|
|
341
|
+
* await store.initialize();
|
|
342
|
+
*
|
|
343
|
+
* // Encrypt a secret for a tenant
|
|
344
|
+
* const envelope = await store.encrypt('tenant-123', 'api-key', 'sk_live_xxx');
|
|
345
|
+
*
|
|
346
|
+
* // Decrypt
|
|
347
|
+
* const { value } = await store.decrypt('tenant-123', envelope);
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
var DatabaseSecretStore = class {
|
|
351
|
+
db;
|
|
352
|
+
keysTable;
|
|
353
|
+
amkConfig;
|
|
354
|
+
cachedAmk = null;
|
|
355
|
+
initialized = false;
|
|
356
|
+
listeners = [];
|
|
357
|
+
constructor(options) {
|
|
358
|
+
this.db = options.db;
|
|
359
|
+
const tableName = options.keysTable ?? "tenant_encryption_keys";
|
|
360
|
+
if (!/^[A-Za-z0-9_]+$/.test(tableName)) throw new Error(`Invalid keysTable name "${tableName}". Table name must contain only alphanumeric characters and underscores.`);
|
|
361
|
+
this.keysTable = tableName;
|
|
362
|
+
this.amkConfig = options.amk;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Initialize the store (create schema if needed)
|
|
366
|
+
*/
|
|
367
|
+
async initialize() {
|
|
368
|
+
if (this.initialized) return;
|
|
369
|
+
const schema = `
|
|
319
370
|
CREATE TABLE IF NOT EXISTS "${this.keysTable}" (
|
|
320
371
|
id TEXT PRIMARY KEY,
|
|
321
372
|
tenant_id TEXT NOT NULL,
|
|
@@ -336,401 +387,330 @@ class DatabaseSecretStore {
|
|
|
336
387
|
ON "${this.keysTable}" (rotate_after)
|
|
337
388
|
WHERE status = 'active';
|
|
338
389
|
`;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
amkKeyId: this.amkConfig.keyId,
|
|
601
|
-
status: "active",
|
|
602
|
-
version: currentKey.version + 1,
|
|
603
|
-
createdAt: now,
|
|
604
|
-
rotateAfter
|
|
605
|
-
};
|
|
606
|
-
} catch (error) {
|
|
607
|
-
throw new KeyRotationError(
|
|
608
|
-
`Failed to rotate key for tenant ${tenantId}: ${error.message}`,
|
|
609
|
-
"database",
|
|
610
|
-
error instanceof Error ? error : void 0
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
async retireTenantKey(tenantId, keyId) {
|
|
615
|
-
this.ensureInitialized();
|
|
616
|
-
const now = /* @__PURE__ */ new Date();
|
|
617
|
-
await this.db.update(
|
|
618
|
-
this.keysTable,
|
|
619
|
-
{ id: keyId, tenant_id: tenantId },
|
|
620
|
-
{
|
|
621
|
-
status: "retired",
|
|
622
|
-
retired_at: now.toISOString(),
|
|
623
|
-
updated_at: now.toISOString()
|
|
624
|
-
}
|
|
625
|
-
);
|
|
626
|
-
await this.emitEvent({
|
|
627
|
-
type: "key.retired",
|
|
628
|
-
tenantId,
|
|
629
|
-
keyId,
|
|
630
|
-
timestamp: now
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
async getActiveAMK() {
|
|
634
|
-
this.getAMK();
|
|
635
|
-
return {
|
|
636
|
-
keyId: this.amkConfig.keyId,
|
|
637
|
-
description: `AMK from env var ${this.amkConfig.keyEnvVar}`,
|
|
638
|
-
status: "active",
|
|
639
|
-
provider: "env",
|
|
640
|
-
keyReference: this.amkConfig.keyEnvVar,
|
|
641
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
async rewrapTenantKey(_tenantId, _newAmkKeyId) {
|
|
645
|
-
throw new Error(
|
|
646
|
-
"AMK rotation not supported for env-based keys. Use AWS KMS, Vault, or Azure Key Vault for AMK rotation support."
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
async listKeyVersions(tenantId) {
|
|
650
|
-
this.ensureInitialized();
|
|
651
|
-
const { rows } = await this.db.query(
|
|
652
|
-
`SELECT * FROM "${this.keysTable}" WHERE tenant_id = ? ORDER BY version DESC`,
|
|
653
|
-
tenantId
|
|
654
|
-
);
|
|
655
|
-
return rows.map((row) => this.parseKeyRow(row));
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Parse a database row into a TenantDataEncryptionKey
|
|
659
|
-
*/
|
|
660
|
-
parseKeyRow(row) {
|
|
661
|
-
return {
|
|
662
|
-
keyId: row.id,
|
|
663
|
-
tenantId: row.tenant_id,
|
|
664
|
-
wrappedKey: row.wrapped_key,
|
|
665
|
-
amkKeyId: row.amk_key_id,
|
|
666
|
-
status: row.status,
|
|
667
|
-
version: row.version,
|
|
668
|
-
createdAt: new Date(row.created_at),
|
|
669
|
-
rotateAfter: row.rotate_after ? new Date(row.rotate_after) : void 0,
|
|
670
|
-
retiredAt: row.retired_at ? new Date(row.retired_at) : void 0
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
const database = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
675
|
-
__proto__: null,
|
|
676
|
-
DatabaseSecretStore
|
|
677
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
390
|
+
await syncSchema({
|
|
391
|
+
db: this.db,
|
|
392
|
+
schema
|
|
393
|
+
});
|
|
394
|
+
this.initialized = true;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Get the Application Master Key from environment
|
|
398
|
+
*/
|
|
399
|
+
getAMK() {
|
|
400
|
+
if (this.cachedAmk) return this.cachedAmk;
|
|
401
|
+
const keyHex = process.env[this.amkConfig.keyEnvVar];
|
|
402
|
+
if (!keyHex) throw new AMKUnavailableError(`Application Master Key not found in environment variable: ${this.amkConfig.keyEnvVar}`);
|
|
403
|
+
try {
|
|
404
|
+
const key = EnvelopeEncryption.parseHexKey(keyHex);
|
|
405
|
+
this.cachedAmk = key;
|
|
406
|
+
return key;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
throw new AMKUnavailableError(`Invalid AMK in ${this.amkConfig.keyEnvVar}: ${error.message}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Ensure the store is initialized
|
|
413
|
+
*/
|
|
414
|
+
ensureInitialized() {
|
|
415
|
+
if (!this.initialized) throw new StoreNotInitializedError("database");
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Emit an event to all listeners
|
|
419
|
+
*/
|
|
420
|
+
async emitEvent(event) {
|
|
421
|
+
for (const listener of this.listeners) await listener(event);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Subscribe to store events
|
|
425
|
+
*/
|
|
426
|
+
subscribe(listener) {
|
|
427
|
+
this.listeners.push(listener);
|
|
428
|
+
return () => {
|
|
429
|
+
const index = this.listeners.indexOf(listener);
|
|
430
|
+
if (index !== -1) this.listeners.splice(index, 1);
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async encrypt(tenantId, secretName, plaintext, options) {
|
|
434
|
+
this.ensureInitialized();
|
|
435
|
+
if (typeof tenantId !== "string" || tenantId.trim().length === 0) throw new EncryptionError("Invalid tenantId provided for encryption", "database");
|
|
436
|
+
if (typeof secretName !== "string" || secretName.trim().length === 0) throw new EncryptionError("Invalid secretName provided for encryption", "database");
|
|
437
|
+
let dataKey = null;
|
|
438
|
+
try {
|
|
439
|
+
let tenantKey = await this.getTenantKey(tenantId);
|
|
440
|
+
if (!tenantKey) tenantKey = await this.createTenantKey(tenantId);
|
|
441
|
+
const amk = this.getAMK();
|
|
442
|
+
const { wrappedKey, iv: keyIv, authTag: keyAuthTag } = EnvelopeEncryption.parseWrappedKey(tenantKey.wrappedKey);
|
|
443
|
+
dataKey = EnvelopeEncryption.unwrapKey(wrappedKey, keyIv, keyAuthTag, amk);
|
|
444
|
+
const envelope = EnvelopeEncryption.createEnvelope(plaintext, dataKey, {
|
|
445
|
+
wrappedKey,
|
|
446
|
+
iv: keyIv,
|
|
447
|
+
authTag: keyAuthTag
|
|
448
|
+
}, tenantKey.amkKeyId, options?.metadata);
|
|
449
|
+
await this.emitEvent({
|
|
450
|
+
type: "secret.encrypted",
|
|
451
|
+
tenantId,
|
|
452
|
+
keyId: tenantKey.keyId,
|
|
453
|
+
secretName,
|
|
454
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
455
|
+
});
|
|
456
|
+
return envelope;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error instanceof AMKUnavailableError || error instanceof TenantKeyMissingError) throw error;
|
|
459
|
+
throw new EncryptionError(`Failed to encrypt secret '${secretName}' for tenant ${tenantId}: ${error.message}`, "database", error instanceof Error ? error : void 0);
|
|
460
|
+
} finally {
|
|
461
|
+
if (dataKey && Buffer.isBuffer(dataKey)) dataKey.fill(0);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async decrypt(tenantId, envelope) {
|
|
465
|
+
this.ensureInitialized();
|
|
466
|
+
if (typeof tenantId !== "string" || tenantId.trim().length === 0) throw new DecryptionError("Invalid tenantId provided for decryption", "database");
|
|
467
|
+
const tenantKeys = await this.listKeyVersions(tenantId);
|
|
468
|
+
if (tenantKeys.length === 0) throw new TenantKeyMissingError(tenantId);
|
|
469
|
+
const envelopeWrappedKey = envelope.wrappedKey.split(":")[0];
|
|
470
|
+
const matchingKey = tenantKeys.find((key) => {
|
|
471
|
+
return key.wrappedKey.split(":")[0] === envelopeWrappedKey;
|
|
472
|
+
});
|
|
473
|
+
if (!matchingKey) throw new DecryptionError(`Envelope was not encrypted with a key belonging to tenant ${tenantId}`, "database");
|
|
474
|
+
try {
|
|
475
|
+
const amk = this.getAMK();
|
|
476
|
+
const plaintext = EnvelopeEncryption.decryptEnvelope(envelope, amk);
|
|
477
|
+
await this.emitEvent({
|
|
478
|
+
type: "secret.decrypted",
|
|
479
|
+
tenantId,
|
|
480
|
+
keyId: matchingKey.keyId,
|
|
481
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
value: plaintext,
|
|
485
|
+
keyId: matchingKey.keyId,
|
|
486
|
+
createdAt: new Date(envelope.createdAt),
|
|
487
|
+
metadata: envelope.metadata
|
|
488
|
+
};
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (error instanceof AMKUnavailableError) throw error;
|
|
491
|
+
throw new DecryptionError(`Failed to decrypt secret for tenant ${tenantId}: ${error.message}`, "database", error instanceof Error ? error : void 0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async getTenantKey(tenantId) {
|
|
495
|
+
this.ensureInitialized();
|
|
496
|
+
const row = await this.db.get(this.keysTable, {
|
|
497
|
+
tenant_id: tenantId,
|
|
498
|
+
status: "active"
|
|
499
|
+
});
|
|
500
|
+
if (!row) return null;
|
|
501
|
+
return this.parseKeyRow(row);
|
|
502
|
+
}
|
|
503
|
+
async createTenantKey(tenantId) {
|
|
504
|
+
this.ensureInitialized();
|
|
505
|
+
const amk = this.getAMK();
|
|
506
|
+
const dataKey = EnvelopeEncryption.generateDataKey();
|
|
507
|
+
const wrapped = EnvelopeEncryption.wrapKey(dataKey, amk);
|
|
508
|
+
const keyId = createId();
|
|
509
|
+
const wrappedKeyStr = `${wrapped.wrappedKey}:${wrapped.iv}:${wrapped.authTag}`;
|
|
510
|
+
const now = /* @__PURE__ */ new Date();
|
|
511
|
+
const rotateAfter = new Date(now.getTime() + DEFAULT_ROTATION_PERIOD_MS);
|
|
512
|
+
await this.db.insert(this.keysTable, {
|
|
513
|
+
id: keyId,
|
|
514
|
+
tenant_id: tenantId,
|
|
515
|
+
wrapped_key: wrappedKeyStr,
|
|
516
|
+
amk_key_id: this.amkConfig.keyId,
|
|
517
|
+
status: "active",
|
|
518
|
+
version: 1,
|
|
519
|
+
rotate_after: rotateAfter.toISOString(),
|
|
520
|
+
created_at: now.toISOString(),
|
|
521
|
+
updated_at: now.toISOString()
|
|
522
|
+
});
|
|
523
|
+
await this.emitEvent({
|
|
524
|
+
type: "key.created",
|
|
525
|
+
tenantId,
|
|
526
|
+
keyId,
|
|
527
|
+
timestamp: now
|
|
528
|
+
});
|
|
529
|
+
return {
|
|
530
|
+
keyId,
|
|
531
|
+
tenantId,
|
|
532
|
+
wrappedKey: wrappedKeyStr,
|
|
533
|
+
amkKeyId: this.amkConfig.keyId,
|
|
534
|
+
status: "active",
|
|
535
|
+
version: 1,
|
|
536
|
+
createdAt: now,
|
|
537
|
+
rotateAfter
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async rotateTenantKey(tenantId) {
|
|
541
|
+
this.ensureInitialized();
|
|
542
|
+
const currentKey = await this.getTenantKey(tenantId);
|
|
543
|
+
if (!currentKey) throw new TenantKeyMissingError(tenantId);
|
|
544
|
+
const now = /* @__PURE__ */ new Date();
|
|
545
|
+
try {
|
|
546
|
+
await this.db.update(this.keysTable, { id: currentKey.keyId }, {
|
|
547
|
+
status: "rotating",
|
|
548
|
+
updated_at: now.toISOString()
|
|
549
|
+
});
|
|
550
|
+
const amk = this.getAMK();
|
|
551
|
+
const dataKey = EnvelopeEncryption.generateDataKey();
|
|
552
|
+
const wrapped = EnvelopeEncryption.wrapKey(dataKey, amk);
|
|
553
|
+
const newKeyId = createId();
|
|
554
|
+
const wrappedKeyStr = `${wrapped.wrappedKey}:${wrapped.iv}:${wrapped.authTag}`;
|
|
555
|
+
const rotateAfter = new Date(now.getTime() + DEFAULT_ROTATION_PERIOD_MS);
|
|
556
|
+
await this.db.insert(this.keysTable, {
|
|
557
|
+
id: newKeyId,
|
|
558
|
+
tenant_id: tenantId,
|
|
559
|
+
wrapped_key: wrappedKeyStr,
|
|
560
|
+
amk_key_id: this.amkConfig.keyId,
|
|
561
|
+
status: "active",
|
|
562
|
+
version: currentKey.version + 1,
|
|
563
|
+
rotate_after: rotateAfter.toISOString(),
|
|
564
|
+
created_at: now.toISOString(),
|
|
565
|
+
updated_at: now.toISOString()
|
|
566
|
+
});
|
|
567
|
+
await this.db.update(this.keysTable, { id: currentKey.keyId }, {
|
|
568
|
+
status: "retired",
|
|
569
|
+
retired_at: now.toISOString(),
|
|
570
|
+
updated_at: now.toISOString()
|
|
571
|
+
});
|
|
572
|
+
await this.emitEvent({
|
|
573
|
+
type: "key.rotated",
|
|
574
|
+
tenantId,
|
|
575
|
+
keyId: newKeyId,
|
|
576
|
+
timestamp: now
|
|
577
|
+
});
|
|
578
|
+
return {
|
|
579
|
+
keyId: newKeyId,
|
|
580
|
+
tenantId,
|
|
581
|
+
wrappedKey: wrappedKeyStr,
|
|
582
|
+
amkKeyId: this.amkConfig.keyId,
|
|
583
|
+
status: "active",
|
|
584
|
+
version: currentKey.version + 1,
|
|
585
|
+
createdAt: now,
|
|
586
|
+
rotateAfter
|
|
587
|
+
};
|
|
588
|
+
} catch (error) {
|
|
589
|
+
throw new KeyRotationError(`Failed to rotate key for tenant ${tenantId}: ${error.message}`, "database", error instanceof Error ? error : void 0);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async retireTenantKey(tenantId, keyId) {
|
|
593
|
+
this.ensureInitialized();
|
|
594
|
+
const now = /* @__PURE__ */ new Date();
|
|
595
|
+
await this.db.update(this.keysTable, {
|
|
596
|
+
id: keyId,
|
|
597
|
+
tenant_id: tenantId
|
|
598
|
+
}, {
|
|
599
|
+
status: "retired",
|
|
600
|
+
retired_at: now.toISOString(),
|
|
601
|
+
updated_at: now.toISOString()
|
|
602
|
+
});
|
|
603
|
+
await this.emitEvent({
|
|
604
|
+
type: "key.retired",
|
|
605
|
+
tenantId,
|
|
606
|
+
keyId,
|
|
607
|
+
timestamp: now
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
async getActiveAMK() {
|
|
611
|
+
this.getAMK();
|
|
612
|
+
return {
|
|
613
|
+
keyId: this.amkConfig.keyId,
|
|
614
|
+
description: `AMK from env var ${this.amkConfig.keyEnvVar}`,
|
|
615
|
+
status: "active",
|
|
616
|
+
provider: "env",
|
|
617
|
+
keyReference: this.amkConfig.keyEnvVar,
|
|
618
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
async rewrapTenantKey(_tenantId, _newAmkKeyId) {
|
|
622
|
+
throw new Error("AMK rotation not supported for env-based keys. Use AWS KMS, Vault, or Azure Key Vault for AMK rotation support.");
|
|
623
|
+
}
|
|
624
|
+
async listKeyVersions(tenantId) {
|
|
625
|
+
this.ensureInitialized();
|
|
626
|
+
const { rows } = await this.db.query(`SELECT * FROM "${this.keysTable}" WHERE tenant_id = ? ORDER BY version DESC`, tenantId);
|
|
627
|
+
return rows.map((row) => this.parseKeyRow(row));
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Parse a database row into a TenantDataEncryptionKey
|
|
631
|
+
*/
|
|
632
|
+
parseKeyRow(row) {
|
|
633
|
+
return {
|
|
634
|
+
keyId: row.id,
|
|
635
|
+
tenantId: row.tenant_id,
|
|
636
|
+
wrappedKey: row.wrapped_key,
|
|
637
|
+
amkKeyId: row.amk_key_id,
|
|
638
|
+
status: row.status,
|
|
639
|
+
version: row.version,
|
|
640
|
+
createdAt: new Date(row.created_at),
|
|
641
|
+
rotateAfter: row.rotate_after ? new Date(row.rotate_after) : void 0,
|
|
642
|
+
retiredAt: row.retired_at ? new Date(row.retired_at) : void 0
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region src/shared/factory.ts
|
|
648
|
+
/**
|
|
649
|
+
* Type guard for database options
|
|
650
|
+
*/
|
|
678
651
|
function isDatabaseOptions(opts) {
|
|
679
|
-
|
|
652
|
+
return opts.type === "database";
|
|
680
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Type guard for AWS KMS options
|
|
656
|
+
*/
|
|
681
657
|
function isAWSKMSOptions(opts) {
|
|
682
|
-
|
|
658
|
+
return opts.type === "aws-kms";
|
|
683
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* Type guard for Vault options
|
|
662
|
+
*/
|
|
684
663
|
function isVaultOptions(opts) {
|
|
685
|
-
|
|
664
|
+
return opts.type === "vault";
|
|
686
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Type guard for Azure Key Vault options
|
|
668
|
+
*/
|
|
687
669
|
function isAzureKeyVaultOptions(opts) {
|
|
688
|
-
|
|
670
|
+
return opts.type === "azure-keyvault";
|
|
689
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Factory function to create secret store instances
|
|
674
|
+
*
|
|
675
|
+
* Uses lazy imports to avoid loading unnecessary adapters and their dependencies.
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* ```typescript
|
|
679
|
+
* // Database-backed store with env-based AMK
|
|
680
|
+
* const store = await getSecretStore({
|
|
681
|
+
* type: 'database',
|
|
682
|
+
* db,
|
|
683
|
+
* amk: {
|
|
684
|
+
* provider: 'env',
|
|
685
|
+
* keyEnvVar: 'SECRET_MASTER_KEY',
|
|
686
|
+
* keyId: 'amk-v1'
|
|
687
|
+
* }
|
|
688
|
+
* });
|
|
689
|
+
*
|
|
690
|
+
* // Encrypt a secret
|
|
691
|
+
* const envelope = await store.encrypt('tenant-123', 'api-key', 'sk_live_xxx');
|
|
692
|
+
*
|
|
693
|
+
* // Decrypt
|
|
694
|
+
* const decrypted = await store.decrypt('tenant-123', envelope);
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
690
697
|
async function getSecretStore(options) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
if (isVaultOptions(options)) {
|
|
703
|
-
throw new Error(
|
|
704
|
-
"HashiCorp Vault adapter not yet implemented. Coming in a future release."
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
if (isAzureKeyVaultOptions(options)) {
|
|
708
|
-
throw new Error(
|
|
709
|
-
"Azure Key Vault adapter not yet implemented. Coming in a future release."
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
throw new Error(
|
|
713
|
-
`Unsupported secret store type: ${options.type}`
|
|
714
|
-
);
|
|
698
|
+
if (isDatabaseOptions(options)) {
|
|
699
|
+
const { DatabaseSecretStore } = await Promise.resolve().then(() => database_exports);
|
|
700
|
+
const store = new DatabaseSecretStore(options);
|
|
701
|
+
await store.initialize();
|
|
702
|
+
return store;
|
|
703
|
+
}
|
|
704
|
+
if (isAWSKMSOptions(options)) throw new Error("AWS KMS adapter not yet implemented. Coming in a future release.");
|
|
705
|
+
if (isVaultOptions(options)) throw new Error("HashiCorp Vault adapter not yet implemented. Coming in a future release.");
|
|
706
|
+
if (isAzureKeyVaultOptions(options)) throw new Error("Azure Key Vault adapter not yet implemented. Coming in a future release.");
|
|
707
|
+
throw new Error(`Unsupported secret store type: ${options.type}`);
|
|
715
708
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
KeyNotFoundError,
|
|
725
|
-
KeyRotationError,
|
|
726
|
-
PACKAGE_VERSION_INITIALIZED,
|
|
727
|
-
SecretError,
|
|
728
|
-
StoreNotInitializedError,
|
|
729
|
-
TenantKeyMissingError,
|
|
730
|
-
getSecretStore,
|
|
731
|
-
isAWSKMSOptions,
|
|
732
|
-
isAzureKeyVaultOptions,
|
|
733
|
-
isDatabaseOptions,
|
|
734
|
-
isVaultOptions
|
|
735
|
-
};
|
|
736
|
-
//# sourceMappingURL=index.js.map
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/index.ts
|
|
711
|
+
/** @internal */
|
|
712
|
+
var PACKAGE_VERSION_INITIALIZED = true;
|
|
713
|
+
//#endregion
|
|
714
|
+
export { AMKUnavailableError, DatabaseSecretStore, DecryptionError, EncryptionError, EnvelopeEncryption, InvalidKeyFormatError, KeyNotFoundError, KeyRotationError, PACKAGE_VERSION_INITIALIZED, SecretError, StoreNotInitializedError, TenantKeyMissingError, getSecretStore, isAWSKMSOptions, isAzureKeyVaultOptions, isDatabaseOptions, isVaultOptions };
|
|
715
|
+
|
|
716
|
+
//# sourceMappingURL=index.js.map
|