@fgv/ts-extras 5.0.2 → 5.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.
Files changed (92) hide show
  1. package/dist/index.browser.js +6 -2
  2. package/dist/index.js +5 -1
  3. package/dist/packlets/ai-assist/apiClient.js +484 -0
  4. package/dist/packlets/ai-assist/converters.js +121 -0
  5. package/dist/packlets/ai-assist/index.js +10 -0
  6. package/dist/packlets/ai-assist/model.js +90 -0
  7. package/dist/packlets/ai-assist/registry.js +145 -0
  8. package/dist/packlets/ai-assist/toolFormats.js +160 -0
  9. package/dist/packlets/crypto-utils/constants.js +48 -0
  10. package/dist/packlets/crypto-utils/converters.js +155 -0
  11. package/dist/packlets/crypto-utils/directEncryptionProvider.js +86 -0
  12. package/dist/packlets/crypto-utils/encryptedFile.js +161 -0
  13. package/dist/packlets/crypto-utils/index.browser.js +41 -0
  14. package/dist/packlets/crypto-utils/index.js +41 -0
  15. package/dist/packlets/crypto-utils/keystore/converters.js +84 -0
  16. package/dist/packlets/crypto-utils/keystore/index.js +31 -0
  17. package/dist/packlets/crypto-utils/keystore/keyStore.js +758 -0
  18. package/dist/packlets/crypto-utils/keystore/model.js +64 -0
  19. package/dist/packlets/crypto-utils/model.js +39 -0
  20. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +159 -0
  21. package/dist/packlets/experimental/formatter.js +1 -1
  22. package/dist/packlets/mustache/index.js +23 -0
  23. package/dist/packlets/mustache/interfaces.js +25 -0
  24. package/dist/packlets/mustache/mustacheTemplate.js +242 -0
  25. package/dist/packlets/record-jar/recordJarHelpers.js +1 -1
  26. package/dist/packlets/yaml/converters.js +46 -0
  27. package/dist/packlets/yaml/index.js +23 -0
  28. package/dist/packlets/zip-file-tree/index.js +1 -0
  29. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +43 -2
  30. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +40 -0
  31. package/dist/ts-extras.d.ts +1990 -112
  32. package/dist/tsdoc-metadata.json +1 -1
  33. package/lib/index.browser.d.ts +3 -1
  34. package/lib/index.browser.js +6 -1
  35. package/lib/index.d.ts +5 -1
  36. package/lib/index.js +9 -1
  37. package/lib/packlets/ai-assist/apiClient.d.ts +60 -0
  38. package/lib/packlets/ai-assist/apiClient.js +488 -0
  39. package/lib/packlets/ai-assist/converters.d.ts +55 -0
  40. package/lib/packlets/ai-assist/converters.js +124 -0
  41. package/lib/packlets/ai-assist/index.d.ts +10 -0
  42. package/lib/packlets/ai-assist/index.js +33 -0
  43. package/lib/packlets/ai-assist/model.d.ts +222 -0
  44. package/lib/packlets/ai-assist/model.js +95 -0
  45. package/lib/packlets/ai-assist/registry.d.ts +25 -0
  46. package/lib/packlets/ai-assist/registry.js +150 -0
  47. package/lib/packlets/ai-assist/toolFormats.d.ts +44 -0
  48. package/lib/packlets/ai-assist/toolFormats.js +166 -0
  49. package/lib/packlets/crypto-utils/constants.d.ts +26 -0
  50. package/lib/packlets/crypto-utils/constants.js +51 -0
  51. package/lib/packlets/crypto-utils/converters.d.ts +58 -0
  52. package/lib/packlets/crypto-utils/converters.js +192 -0
  53. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts +69 -0
  54. package/lib/packlets/crypto-utils/directEncryptionProvider.js +90 -0
  55. package/lib/packlets/crypto-utils/encryptedFile.d.ts +88 -0
  56. package/lib/packlets/crypto-utils/encryptedFile.js +201 -0
  57. package/lib/packlets/crypto-utils/index.browser.d.ts +14 -0
  58. package/lib/packlets/crypto-utils/index.browser.js +91 -0
  59. package/lib/packlets/crypto-utils/index.d.ts +15 -0
  60. package/lib/packlets/crypto-utils/index.js +88 -0
  61. package/lib/packlets/crypto-utils/keystore/converters.d.ts +29 -0
  62. package/lib/packlets/crypto-utils/keystore/converters.js +87 -0
  63. package/lib/packlets/crypto-utils/keystore/index.d.ts +9 -0
  64. package/lib/packlets/crypto-utils/keystore/index.js +71 -0
  65. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +239 -0
  66. package/lib/packlets/crypto-utils/keystore/keyStore.js +795 -0
  67. package/lib/packlets/crypto-utils/keystore/model.d.ts +245 -0
  68. package/lib/packlets/crypto-utils/keystore/model.js +68 -0
  69. package/lib/packlets/crypto-utils/model.d.ts +236 -0
  70. package/lib/packlets/crypto-utils/model.js +76 -0
  71. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +62 -0
  72. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +196 -0
  73. package/lib/packlets/experimental/formatter.d.ts +1 -1
  74. package/lib/packlets/experimental/formatter.js +1 -1
  75. package/lib/packlets/mustache/index.d.ts +3 -0
  76. package/lib/packlets/mustache/index.js +27 -0
  77. package/lib/packlets/mustache/interfaces.d.ts +97 -0
  78. package/lib/packlets/mustache/interfaces.js +26 -0
  79. package/lib/packlets/mustache/mustacheTemplate.d.ts +76 -0
  80. package/lib/packlets/mustache/mustacheTemplate.js +249 -0
  81. package/lib/packlets/record-jar/recordJarHelpers.js +1 -1
  82. package/lib/packlets/yaml/converters.d.ts +9 -0
  83. package/lib/packlets/yaml/converters.js +82 -0
  84. package/lib/packlets/yaml/index.d.ts +2 -0
  85. package/lib/packlets/yaml/index.js +39 -0
  86. package/lib/packlets/zip-file-tree/index.d.ts +1 -0
  87. package/lib/packlets/zip-file-tree/index.js +15 -0
  88. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +31 -2
  89. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +42 -1
  90. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +27 -0
  91. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +43 -0
  92. package/package.json +37 -18
@@ -0,0 +1,795 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.KeyStore = void 0;
56
+ const ts_utils_1 = require("@fgv/ts-utils");
57
+ const Constants = __importStar(require("../constants"));
58
+ const encryptedFile_1 = require("../encryptedFile");
59
+ const model_1 = require("./model");
60
+ const converters_1 = require("./converters");
61
+ /**
62
+ * Gets the current ISO timestamp.
63
+ */
64
+ function getCurrentTimestamp() {
65
+ return new Date().toISOString();
66
+ }
67
+ // ============================================================================
68
+ // KeyStore Class
69
+ // ============================================================================
70
+ /**
71
+ * Password-protected key store for managing encryption secrets.
72
+ *
73
+ * The KeyStore provides a secure vault for storing named encryption keys.
74
+ * The vault is encrypted at rest using a master password via PBKDF2 key derivation.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * // Create new key store
79
+ * const keystore = KeyStore.create({ cryptoProvider: nodeCryptoProvider }).orThrow();
80
+ * await keystore.initialize('master-password');
81
+ *
82
+ * // Add secrets
83
+ * await keystore.addSecret('my-key', { description: 'Production key' });
84
+ *
85
+ * // Save to file
86
+ * const fileContent = await keystore.save();
87
+ *
88
+ * // Later: Open existing key store
89
+ * const keystore2 = KeyStore.open({
90
+ * cryptoProvider: nodeCryptoProvider,
91
+ * keystoreFile: fileContent.value
92
+ * }).orThrow();
93
+ * await keystore2.unlock('master-password');
94
+ *
95
+ * // Use as secret provider for encrypted file loading
96
+ * const encryptionConfig = keystore2.getEncryptionConfig().orThrow();
97
+ * ```
98
+ *
99
+ * @public
100
+ */
101
+ class KeyStore {
102
+ constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
103
+ this._cryptoProvider = cryptoProvider;
104
+ this._iterations = iterations;
105
+ this._keystoreFile = keystoreFile;
106
+ this._state = 'locked';
107
+ this._dirty = false;
108
+ this._isNew = isNew;
109
+ }
110
+ // ============================================================================
111
+ // Factory Methods
112
+ // ============================================================================
113
+ /**
114
+ * Creates a new, empty key store.
115
+ * Call `initialize(password)` to set the master password.
116
+ * @param params - Creation parameters
117
+ * @returns Success with new KeyStore instance, or Failure if parameters invalid
118
+ * @public
119
+ */
120
+ static create(params) {
121
+ var _a;
122
+ const iterations = (_a = params.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_KEYSTORE_ITERATIONS;
123
+ if (iterations < 1) {
124
+ return (0, ts_utils_1.fail)('Iterations must be at least 1');
125
+ }
126
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true));
127
+ }
128
+ /**
129
+ * Opens an existing encrypted key store.
130
+ * Call `unlock(password)` to decrypt and access secrets.
131
+ * @param params - Open parameters including the encrypted file
132
+ * @returns Success with KeyStore instance, or Failure if file format invalid
133
+ * @public
134
+ */
135
+ static open(params) {
136
+ // Validate the file format
137
+ const fileResult = converters_1.keystoreFile.convert(params.keystoreFile);
138
+ if (fileResult.isFailure()) {
139
+ return (0, ts_utils_1.fail)(`Invalid key store file: ${fileResult.message}`);
140
+ }
141
+ const iterations = fileResult.value.keyDerivation.iterations;
142
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
143
+ }
144
+ // ============================================================================
145
+ // Lifecycle Methods
146
+ // ============================================================================
147
+ /**
148
+ * Initializes a new key store with the master password.
149
+ * Generates a random salt for key derivation.
150
+ * Only valid for newly created (not opened) key stores.
151
+ * @param password - The master password
152
+ * @returns Success with this instance when initialized, Failure if already initialized or opened
153
+ * @public
154
+ */
155
+ async initialize(password) {
156
+ if (!this._isNew) {
157
+ return (0, ts_utils_1.fail)('Cannot initialize an opened key store - use unlock() instead');
158
+ }
159
+ if (this._state === 'unlocked') {
160
+ return (0, ts_utils_1.fail)('Key store is already initialized');
161
+ }
162
+ if (!password || password.length === 0) {
163
+ return (0, ts_utils_1.fail)('Password cannot be empty');
164
+ }
165
+ // Generate salt for this key store using crypto provider
166
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
167
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
168
+ if (saltResult.isFailure()) {
169
+ return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
170
+ }
171
+ this._salt = saltResult.value;
172
+ this._secrets = new Map();
173
+ this._state = 'unlocked';
174
+ this._dirty = true; // New store needs to be saved
175
+ return (0, ts_utils_1.succeed)(this);
176
+ }
177
+ /**
178
+ * Unlocks an existing key store with the master password.
179
+ * Decrypts the vault and loads secrets into memory.
180
+ * @param password - The master password
181
+ * @returns Success with this instance when unlocked, Failure if password incorrect
182
+ * @public
183
+ */
184
+ async unlock(password) {
185
+ var _a;
186
+ if (this._isNew) {
187
+ return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
188
+ }
189
+ /* c8 ignore next 6 - error paths tested but coverage intermittently missed */
190
+ if (this._state === 'unlocked') {
191
+ return (0, ts_utils_1.fail)('Key store is already unlocked');
192
+ }
193
+ if (!this._keystoreFile) {
194
+ return (0, ts_utils_1.fail)('No key store file to unlock');
195
+ }
196
+ const keyDerivation = this._keystoreFile.keyDerivation;
197
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
198
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
199
+ if (saltResult.isFailure()) {
200
+ return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
201
+ }
202
+ const salt = saltResult.value;
203
+ // Derive the key from password
204
+ const keyResult = await this._cryptoProvider.deriveKey(password, salt, keyDerivation.iterations);
205
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
206
+ if (keyResult.isFailure()) {
207
+ return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
208
+ }
209
+ // Decrypt the vault
210
+ const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
211
+ const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
212
+ const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
213
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
214
+ if (ivResult.isFailure()) {
215
+ return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
216
+ }
217
+ if (authTagResult.isFailure()) {
218
+ return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
219
+ }
220
+ if (encryptedDataResult.isFailure()) {
221
+ return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
222
+ }
223
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
224
+ if (decryptResult.isFailure()) {
225
+ return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
226
+ }
227
+ // Parse the vault contents
228
+ const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
229
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
230
+ if (parseResult.isFailure()) {
231
+ return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
232
+ }
233
+ const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
234
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
235
+ if (vaultResult.isFailure()) {
236
+ return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
237
+ }
238
+ // Load secrets into memory
239
+ this._salt = salt;
240
+ this._secrets = new Map();
241
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
242
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
243
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
244
+ if (keyBytesResult.isFailure()) {
245
+ return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
246
+ }
247
+ const entry = {
248
+ name,
249
+ /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
250
+ type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
251
+ key: keyBytesResult.value,
252
+ description: jsonEntry.description,
253
+ createdAt: jsonEntry.createdAt
254
+ };
255
+ this._secrets.set(name, entry);
256
+ }
257
+ this._state = 'unlocked';
258
+ this._dirty = false;
259
+ return (0, ts_utils_1.succeed)(this);
260
+ }
261
+ /**
262
+ * Locks the key store, clearing all secrets from memory.
263
+ * @param force - If true, discards unsaved changes
264
+ * @returns Success when locked, Failure if unsaved changes and !force
265
+ * @public
266
+ */
267
+ lock(force) {
268
+ if (this._state === 'locked') {
269
+ return (0, ts_utils_1.succeed)(this);
270
+ }
271
+ if (this._dirty && !force) {
272
+ return (0, ts_utils_1.fail)('Unsaved changes - use force=true to discard or save() first');
273
+ }
274
+ // Clear secrets from memory (overwrite for security)
275
+ if (this._secrets) {
276
+ for (const entry of this._secrets.values()) {
277
+ entry.key.fill(0);
278
+ }
279
+ this._secrets.clear();
280
+ this._secrets = undefined;
281
+ }
282
+ this._state = 'locked';
283
+ this._dirty = false;
284
+ return (0, ts_utils_1.succeed)(this);
285
+ }
286
+ /**
287
+ * Checks if the key store is unlocked.
288
+ * @public
289
+ */
290
+ get isUnlocked() {
291
+ return this._state === 'unlocked';
292
+ }
293
+ /**
294
+ * Checks if there are unsaved changes.
295
+ * @public
296
+ */
297
+ get isDirty() {
298
+ return this._dirty;
299
+ }
300
+ /**
301
+ * Whether this is a newly created key store (not opened from a file).
302
+ * A new key store must be initialized with a password before use.
303
+ * An opened key store must be unlocked with the existing password.
304
+ * @public
305
+ */
306
+ get isNew() {
307
+ return this._isNew;
308
+ }
309
+ /**
310
+ * Gets the current lock state.
311
+ * @public
312
+ */
313
+ get state() {
314
+ return this._state;
315
+ }
316
+ /**
317
+ * Gets the crypto provider used by this key store.
318
+ * Available regardless of lock state.
319
+ * @public
320
+ */
321
+ get cryptoProvider() {
322
+ return this._cryptoProvider;
323
+ }
324
+ // ============================================================================
325
+ // Secret Management
326
+ // ============================================================================
327
+ /**
328
+ * Lists all secret names in the key store.
329
+ * @returns Success with array of secret names, Failure if locked
330
+ * @public
331
+ */
332
+ listSecrets() {
333
+ if (!this._secrets) {
334
+ return (0, ts_utils_1.fail)('Key store is locked');
335
+ }
336
+ return (0, ts_utils_1.succeed)(Array.from(this._secrets.keys()));
337
+ }
338
+ /**
339
+ * Gets a secret by name.
340
+ * @param name - Name of the secret
341
+ * @returns Success with secret entry, Failure if not found or locked
342
+ * @public
343
+ */
344
+ getSecret(name) {
345
+ if (!this._secrets) {
346
+ return (0, ts_utils_1.fail)('Key store is locked');
347
+ }
348
+ const entry = this._secrets.get(name);
349
+ if (!entry) {
350
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
351
+ }
352
+ return (0, ts_utils_1.succeed)(entry);
353
+ }
354
+ /**
355
+ * Checks if a secret exists.
356
+ * @param name - Name of the secret
357
+ * @returns Success with boolean, Failure if locked
358
+ * @public
359
+ */
360
+ hasSecret(name) {
361
+ if (!this._secrets) {
362
+ return (0, ts_utils_1.fail)('Key store is locked');
363
+ }
364
+ return (0, ts_utils_1.succeed)(this._secrets.has(name));
365
+ }
366
+ /**
367
+ * Adds a new secret with a randomly generated key.
368
+ * @param name - Unique name for the secret
369
+ * @param options - Optional description
370
+ * @returns Success with the generated entry, Failure if locked or name invalid
371
+ * @public
372
+ */
373
+ async addSecret(name, options) {
374
+ if (!this._secrets) {
375
+ return (0, ts_utils_1.fail)('Key store is locked');
376
+ }
377
+ if (!name || name.length === 0) {
378
+ return (0, ts_utils_1.fail)('Secret name cannot be empty');
379
+ }
380
+ const replaced = this._secrets.has(name);
381
+ // Generate a new random key
382
+ const keyResult = await this._cryptoProvider.generateKey();
383
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
384
+ if (keyResult.isFailure()) {
385
+ return (0, ts_utils_1.fail)(`Failed to generate key: ${keyResult.message}`);
386
+ }
387
+ const entry = {
388
+ name,
389
+ type: 'encryption-key',
390
+ key: keyResult.value,
391
+ description: options === null || options === void 0 ? void 0 : options.description,
392
+ createdAt: getCurrentTimestamp()
393
+ };
394
+ this._secrets.set(name, entry);
395
+ this._dirty = true;
396
+ return (0, ts_utils_1.succeed)({ entry, replaced });
397
+ }
398
+ /**
399
+ * Imports an existing secret key.
400
+ * @param name - Unique name for the secret
401
+ * @param key - The 32-byte AES-256 key
402
+ * @param options - Optional description, whether to replace existing
403
+ * @returns Success with entry, Failure if locked, key invalid, or exists and !replace
404
+ * @public
405
+ */
406
+ importSecret(name, key, options) {
407
+ if (!this._secrets) {
408
+ return (0, ts_utils_1.fail)('Key store is locked');
409
+ }
410
+ if (!name || name.length === 0) {
411
+ return (0, ts_utils_1.fail)('Secret name cannot be empty');
412
+ }
413
+ if (key.length !== Constants.AES_256_KEY_SIZE) {
414
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
415
+ }
416
+ const exists = this._secrets.has(name);
417
+ if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
418
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
419
+ }
420
+ const entry = {
421
+ name,
422
+ type: 'encryption-key',
423
+ key: new Uint8Array(key), // Copy to prevent external modification
424
+ description: options === null || options === void 0 ? void 0 : options.description,
425
+ createdAt: getCurrentTimestamp()
426
+ };
427
+ this._secrets.set(name, entry);
428
+ this._dirty = true;
429
+ return (0, ts_utils_1.succeed)({ entry, replaced: exists });
430
+ }
431
+ /**
432
+ * Adds a secret derived from a password using PBKDF2.
433
+ *
434
+ * Generates a random salt, derives a 32-byte AES-256 key from the password,
435
+ * and stores it in the vault. Returns the key derivation parameters so they
436
+ * can be stored alongside encrypted files, enabling decryption with just the
437
+ * password (without unlocking the keystore).
438
+ *
439
+ * @param name - Unique name for the secret
440
+ * @param password - Password to derive the key from
441
+ * @param options - Optional description, iterations, replace flag
442
+ * @returns Success with entry and keyDerivation params, Failure if locked or invalid
443
+ * @public
444
+ */
445
+ async addSecretFromPassword(name, password, options) {
446
+ var _a;
447
+ if (!this._secrets) {
448
+ return (0, ts_utils_1.fail)('Key store is locked');
449
+ }
450
+ if (!name || name.length === 0) {
451
+ return (0, ts_utils_1.fail)('Secret name cannot be empty');
452
+ }
453
+ if (!password || password.length === 0) {
454
+ return (0, ts_utils_1.fail)('Password cannot be empty');
455
+ }
456
+ const exists = this._secrets.has(name);
457
+ if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
458
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
459
+ }
460
+ const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_SECRET_ITERATIONS;
461
+ // Generate a random salt for this secret's key derivation
462
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
463
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
464
+ if (saltResult.isFailure()) {
465
+ return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
466
+ }
467
+ // Derive the key from password
468
+ const keyResult = await this._cryptoProvider.deriveKey(password, saltResult.value, iterations);
469
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
470
+ if (keyResult.isFailure()) {
471
+ return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
472
+ }
473
+ const entry = {
474
+ name,
475
+ type: 'encryption-key',
476
+ key: keyResult.value,
477
+ description: options === null || options === void 0 ? void 0 : options.description,
478
+ createdAt: getCurrentTimestamp()
479
+ };
480
+ this._secrets.set(name, entry);
481
+ this._dirty = true;
482
+ return (0, ts_utils_1.succeed)({
483
+ entry,
484
+ replaced: exists,
485
+ keyDerivation: {
486
+ kdf: 'pbkdf2',
487
+ salt: this._cryptoProvider.toBase64(saltResult.value),
488
+ iterations
489
+ }
490
+ });
491
+ }
492
+ /**
493
+ * Removes a secret by name.
494
+ * @param name - Name of the secret to remove
495
+ * @returns Success with removed entry, Failure if not found or locked
496
+ * @public
497
+ */
498
+ removeSecret(name) {
499
+ if (!this._secrets) {
500
+ return (0, ts_utils_1.fail)('Key store is locked');
501
+ }
502
+ const entry = this._secrets.get(name);
503
+ if (!entry) {
504
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
505
+ }
506
+ // Clear the key before removing (security)
507
+ entry.key.fill(0);
508
+ this._secrets.delete(name);
509
+ this._dirty = true;
510
+ return (0, ts_utils_1.succeed)(entry);
511
+ }
512
+ /**
513
+ * Imports an API key string into the vault.
514
+ * The string is UTF-8 encoded and stored with type `'api-key'`.
515
+ * @param name - Unique name for the secret
516
+ * @param apiKey - The API key string
517
+ * @param options - Optional description, whether to replace existing
518
+ * @returns Success with entry, Failure if locked, empty, or exists and !replace
519
+ * @public
520
+ */
521
+ importApiKey(name, apiKey, options) {
522
+ if (!this._secrets) {
523
+ return (0, ts_utils_1.fail)('Key store is locked');
524
+ }
525
+ if (!name || name.length === 0) {
526
+ return (0, ts_utils_1.fail)('Secret name cannot be empty');
527
+ }
528
+ if (!apiKey || apiKey.length === 0) {
529
+ return (0, ts_utils_1.fail)('API key cannot be empty');
530
+ }
531
+ const exists = this._secrets.has(name);
532
+ if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
533
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
534
+ }
535
+ const encoder = new TextEncoder();
536
+ const entry = {
537
+ name,
538
+ type: 'api-key',
539
+ key: encoder.encode(apiKey),
540
+ description: options === null || options === void 0 ? void 0 : options.description,
541
+ createdAt: getCurrentTimestamp()
542
+ };
543
+ this._secrets.set(name, entry);
544
+ this._dirty = true;
545
+ return (0, ts_utils_1.succeed)({ entry, replaced: exists });
546
+ }
547
+ /**
548
+ * Retrieves an API key string by name.
549
+ * Only works for secrets with type `'api-key'`.
550
+ * @param name - Name of the secret
551
+ * @returns Success with the API key string, Failure if not found, locked, or wrong type
552
+ * @public
553
+ */
554
+ getApiKey(name) {
555
+ if (!this._secrets) {
556
+ return (0, ts_utils_1.fail)('Key store is locked');
557
+ }
558
+ const entry = this._secrets.get(name);
559
+ if (!entry) {
560
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
561
+ }
562
+ if (entry.type !== 'api-key') {
563
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an API key (type: ${entry.type})`);
564
+ }
565
+ const decoder = new TextDecoder();
566
+ return (0, ts_utils_1.succeed)(decoder.decode(entry.key));
567
+ }
568
+ /**
569
+ * Lists secret names filtered by type.
570
+ * @param type - The secret type to filter by
571
+ * @returns Success with array of matching secret names, Failure if locked
572
+ * @public
573
+ */
574
+ listSecretsByType(type) {
575
+ if (!this._secrets) {
576
+ return (0, ts_utils_1.fail)('Key store is locked');
577
+ }
578
+ const names = [];
579
+ for (const [name, entry] of this._secrets) {
580
+ if (entry.type === type) {
581
+ names.push(name);
582
+ }
583
+ }
584
+ return (0, ts_utils_1.succeed)(names);
585
+ }
586
+ /**
587
+ * Renames a secret.
588
+ * @param oldName - Current name
589
+ * @param newName - New name
590
+ * @returns Success with updated entry, Failure if source not found, target exists, or locked
591
+ * @public
592
+ */
593
+ renameSecret(oldName, newName) {
594
+ if (!this._secrets) {
595
+ return (0, ts_utils_1.fail)('Key store is locked');
596
+ }
597
+ if (!newName || newName.length === 0) {
598
+ return (0, ts_utils_1.fail)('New name cannot be empty');
599
+ }
600
+ const entry = this._secrets.get(oldName);
601
+ if (!entry) {
602
+ return (0, ts_utils_1.fail)(`Secret '${oldName}' not found`);
603
+ }
604
+ if (oldName !== newName && this._secrets.has(newName)) {
605
+ return (0, ts_utils_1.fail)(`Secret '${newName}' already exists`);
606
+ }
607
+ // Create new entry with new name (preserve type)
608
+ const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
609
+ this._secrets.delete(oldName);
610
+ this._secrets.set(newName, newEntry);
611
+ this._dirty = true;
612
+ return (0, ts_utils_1.succeed)(newEntry);
613
+ }
614
+ // ============================================================================
615
+ // Persistence
616
+ // ============================================================================
617
+ /**
618
+ * Saves the key store, returning the encrypted file content.
619
+ * Requires the master password to encrypt.
620
+ * @param password - The master password
621
+ * @returns Success with IKeyStoreFile, Failure if locked
622
+ * @public
623
+ */
624
+ async save(password) {
625
+ if (!this._secrets || !this._salt) {
626
+ return (0, ts_utils_1.fail)('Key store is locked');
627
+ }
628
+ if (!password || password.length === 0) {
629
+ return (0, ts_utils_1.fail)('Password cannot be empty');
630
+ }
631
+ // Derive the encryption key
632
+ const keyResult = await this._cryptoProvider.deriveKey(password, this._salt, this._iterations);
633
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
634
+ if (keyResult.isFailure()) {
635
+ return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
636
+ }
637
+ // Build vault contents
638
+ const secrets = {};
639
+ for (const [name, entry] of this._secrets) {
640
+ secrets[name] = {
641
+ name: entry.name,
642
+ type: entry.type,
643
+ key: this._cryptoProvider.toBase64(entry.key),
644
+ description: entry.description,
645
+ createdAt: entry.createdAt
646
+ };
647
+ }
648
+ const vaultContents = {
649
+ version: model_1.KEYSTORE_FORMAT,
650
+ secrets
651
+ };
652
+ // Serialize and encrypt
653
+ const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
654
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
655
+ if (jsonResult.isFailure()) {
656
+ return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
657
+ }
658
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, keyResult.value);
659
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
660
+ if (encryptResult.isFailure()) {
661
+ return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
662
+ }
663
+ const { iv, authTag, encryptedData } = encryptResult.value;
664
+ const keystoreFileData = {
665
+ format: model_1.KEYSTORE_FORMAT,
666
+ algorithm: Constants.DEFAULT_ALGORITHM,
667
+ iv: this._cryptoProvider.toBase64(iv),
668
+ authTag: this._cryptoProvider.toBase64(authTag),
669
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
670
+ keyDerivation: {
671
+ kdf: 'pbkdf2',
672
+ salt: this._cryptoProvider.toBase64(this._salt),
673
+ iterations: this._iterations
674
+ }
675
+ };
676
+ this._keystoreFile = keystoreFileData;
677
+ this._dirty = false;
678
+ this._isNew = false;
679
+ return (0, ts_utils_1.succeed)(keystoreFileData);
680
+ }
681
+ /**
682
+ * Changes the master password.
683
+ * Re-encrypts the vault with the new password-derived key.
684
+ * @param currentPassword - Current master password (for verification)
685
+ * @param newPassword - New master password
686
+ * @returns Success when password changed, Failure if locked or current password incorrect
687
+ * @public
688
+ */
689
+ async changePassword(currentPassword, newPassword) {
690
+ if (!this._secrets || !this._salt) {
691
+ return (0, ts_utils_1.fail)('Key store is locked');
692
+ }
693
+ if (!newPassword || newPassword.length === 0) {
694
+ return (0, ts_utils_1.fail)('New password cannot be empty');
695
+ }
696
+ // Verify current password by trying to derive and re-encrypt
697
+ // (For opened stores, we'd need to verify against the stored file)
698
+ if (this._keystoreFile) {
699
+ const saltResult = this._cryptoProvider.fromBase64(this._keystoreFile.keyDerivation.salt);
700
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
701
+ if (saltResult.isFailure()) {
702
+ return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
703
+ }
704
+ const keyResult = await this._cryptoProvider.deriveKey(currentPassword, saltResult.value, this._keystoreFile.keyDerivation.iterations);
705
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
706
+ if (keyResult.isFailure()) {
707
+ return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
708
+ }
709
+ // Try to decrypt to verify password
710
+ const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
711
+ const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
712
+ const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
713
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
714
+ if (ivResult.isFailure() || authTagResult.isFailure() || encryptedDataResult.isFailure()) {
715
+ return (0, ts_utils_1.fail)('Invalid key store file format');
716
+ }
717
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
718
+ if (decryptResult.isFailure()) {
719
+ return (0, ts_utils_1.fail)('Current password is incorrect');
720
+ }
721
+ }
722
+ // Generate new salt for the new password using crypto provider
723
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
724
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
725
+ if (saltResult.isFailure()) {
726
+ return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
727
+ }
728
+ this._salt = saltResult.value;
729
+ this._dirty = true;
730
+ // Save with new password
731
+ const saveResult = await this.save(newPassword);
732
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
733
+ if (saveResult.isFailure()) {
734
+ return (0, ts_utils_1.fail)(saveResult.message);
735
+ }
736
+ return (0, ts_utils_1.succeed)(this);
737
+ }
738
+ // ============================================================================
739
+ // IEncryptionProvider
740
+ // ============================================================================
741
+ /** {@inheritDoc IEncryptionProvider.encryptByName} */
742
+ async encryptByName(secretName, content, metadata) {
743
+ const secretResult = this.getSecret(secretName);
744
+ if (secretResult.isFailure()) {
745
+ return (0, ts_utils_1.fail)(`encryptByName: ${secretResult.message}`);
746
+ }
747
+ return (0, encryptedFile_1.createEncryptedFile)({
748
+ content,
749
+ secretName,
750
+ key: secretResult.value.key,
751
+ cryptoProvider: this._cryptoProvider,
752
+ metadata
753
+ });
754
+ }
755
+ // ============================================================================
756
+ // Integration with IEncryptionConfig
757
+ // ============================================================================
758
+ /**
759
+ * Creates a SecretProvider function for use with IEncryptionConfig.
760
+ * The returned function looks up secrets from this key store.
761
+ * @returns Success with SecretProvider, Failure if locked
762
+ * @public
763
+ */
764
+ getSecretProvider() {
765
+ if (!this._secrets) {
766
+ return (0, ts_utils_1.fail)('Key store is locked');
767
+ }
768
+ const secrets = this._secrets;
769
+ const provider = async (secretName) => {
770
+ const entry = secrets.get(secretName);
771
+ if (!entry) {
772
+ return (0, ts_utils_1.fail)(`Secret '${secretName}' not found in key store`);
773
+ }
774
+ return (0, ts_utils_1.succeed)(entry.key);
775
+ };
776
+ return (0, ts_utils_1.succeed)(provider);
777
+ }
778
+ /**
779
+ * Creates a partial IEncryptionConfig using this key store as the secret source.
780
+ * @returns Partial config that can be spread into a full IEncryptionConfig
781
+ * @public
782
+ */
783
+ getEncryptionConfig() {
784
+ const providerResult = this.getSecretProvider();
785
+ if (providerResult.isFailure()) {
786
+ return (0, ts_utils_1.fail)(providerResult.message);
787
+ }
788
+ return (0, ts_utils_1.succeed)({
789
+ secretProvider: providerResult.value,
790
+ cryptoProvider: this._cryptoProvider
791
+ });
792
+ }
793
+ }
794
+ exports.KeyStore = KeyStore;
795
+ //# sourceMappingURL=keyStore.js.map