@fgv/ts-extras 5.1.0-2 → 5.1.0-21
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/index.browser.js +2 -1
- package/dist/index.browser.js.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/packlets/ai-assist/apiClient.js +807 -67
- package/dist/packlets/ai-assist/apiClient.js.map +1 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js +180 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
- package/dist/packlets/ai-assist/converters.js +2 -1
- package/dist/packlets/ai-assist/converters.js.map +1 -0
- package/dist/packlets/ai-assist/endpoint.js +78 -0
- package/dist/packlets/ai-assist/endpoint.js.map +1 -0
- package/dist/packlets/ai-assist/index.js +4 -3
- package/dist/packlets/ai-assist/index.js.map +1 -0
- package/dist/packlets/ai-assist/model.js +20 -3
- package/dist/packlets/ai-assist/model.js.map +1 -0
- package/dist/packlets/ai-assist/registry.js +137 -10
- package/dist/packlets/ai-assist/registry.js.map +1 -0
- package/dist/packlets/ai-assist/sseParser.js +122 -0
- package/dist/packlets/ai-assist/sseParser.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +192 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js +77 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +160 -0
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +150 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +164 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +157 -0
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
- package/dist/packlets/ai-assist/streamingClient.js +94 -0
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -0
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -0
- package/dist/packlets/conversion/converters.js +34 -1
- package/dist/packlets/conversion/converters.js.map +1 -0
- package/dist/packlets/conversion/index.js.map +1 -0
- package/dist/packlets/crypto-utils/constants.js.map +1 -0
- package/dist/packlets/crypto-utils/converters.js.map +1 -0
- package/dist/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
- package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -0
- package/dist/packlets/crypto-utils/index.browser.js +2 -0
- package/dist/packlets/crypto-utils/index.browser.js.map +1 -0
- package/dist/packlets/crypto-utils/index.js +2 -0
- package/dist/packlets/crypto-utils/index.js.map +1 -0
- package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +63 -0
- package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/converters.js +101 -9
- package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/index.js +1 -0
- package/dist/packlets/crypto-utils/keystore/index.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js +431 -118
- package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/model.js +22 -1
- package/dist/packlets/crypto-utils/keystore/model.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
- package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
- package/dist/packlets/crypto-utils/model.js +10 -0
- package/dist/packlets/crypto-utils/model.js.map +1 -0
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +163 -1
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
- package/dist/packlets/csv/csvFileHelpers.js.map +1 -0
- package/dist/packlets/csv/csvHelpers.js.map +1 -0
- package/dist/packlets/csv/index.browser.js.map +1 -0
- package/dist/packlets/csv/index.js.map +1 -0
- package/dist/packlets/experimental/extendedArray.js.map +1 -0
- package/dist/packlets/experimental/formatter.js.map +1 -0
- package/dist/packlets/experimental/index.js.map +1 -0
- package/dist/packlets/experimental/rangeOf.js.map +1 -0
- package/dist/packlets/hash/index.browser.js.map +1 -0
- package/dist/packlets/hash/index.js.map +1 -0
- package/dist/packlets/hash/index.node.js.map +1 -0
- package/dist/packlets/hash/md5Normalizer.browser.js.map +1 -0
- package/dist/packlets/hash/md5Normalizer.js.map +1 -0
- package/dist/packlets/mustache/index.js.map +1 -0
- package/dist/packlets/mustache/interfaces.js.map +1 -0
- package/dist/packlets/mustache/mustacheTemplate.js.map +1 -0
- package/dist/packlets/record-jar/index.browser.js.map +1 -0
- package/dist/packlets/record-jar/index.js.map +1 -0
- package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
- package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -0
- package/dist/packlets/yaml/converters.js.map +1 -0
- package/dist/packlets/yaml/index.js +1 -0
- package/dist/packlets/yaml/index.js.map +1 -0
- package/dist/packlets/yaml/serializers.js +48 -0
- package/dist/packlets/yaml/serializers.js.map +1 -0
- package/dist/packlets/zip-file-tree/index.js.map +1 -0
- package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
- package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
- package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
- package/dist/ts-extras.d.ts +1499 -41
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/index.browser.d.ts +2 -1
- package/lib/index.browser.d.ts.map +1 -0
- package/lib/index.browser.js +3 -1
- package/lib/index.browser.js.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js.map +1 -0
- package/lib/packlets/ai-assist/apiClient.d.ts +140 -1
- package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/apiClient.js +810 -66
- package/lib/packlets/ai-assist/apiClient.js.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +89 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.js +189 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
- package/lib/packlets/ai-assist/converters.d.ts.map +1 -0
- package/lib/packlets/ai-assist/converters.js +2 -1
- package/lib/packlets/ai-assist/converters.js.map +1 -0
- package/lib/packlets/ai-assist/endpoint.d.ts +28 -0
- package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
- package/lib/packlets/ai-assist/endpoint.js +82 -0
- package/lib/packlets/ai-assist/endpoint.js.map +1 -0
- package/lib/packlets/ai-assist/index.d.ts +4 -3
- package/lib/packlets/ai-assist/index.d.ts.map +1 -0
- package/lib/packlets/ai-assist/index.js +12 -1
- package/lib/packlets/ai-assist/index.js.map +1 -0
- package/lib/packlets/ai-assist/model.d.ts +341 -3
- package/lib/packlets/ai-assist/model.d.ts.map +1 -0
- package/lib/packlets/ai-assist/model.js +21 -3
- package/lib/packlets/ai-assist/model.js.map +1 -0
- package/lib/packlets/ai-assist/registry.d.ts +34 -1
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -0
- package/lib/packlets/ai-assist/registry.js +140 -11
- package/lib/packlets/ai-assist/registry.js.map +1 -0
- package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
- package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -0
- package/lib/packlets/ai-assist/sseParser.js +127 -0
- package/lib/packlets/ai-assist/sseParser.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +18 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +195 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +79 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.js +81 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +19 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +163 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +18 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +153 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +19 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +167 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +160 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts +33 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingClient.js +99 -0
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -0
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -0
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -0
- package/lib/packlets/conversion/converters.d.ts +8 -1
- package/lib/packlets/conversion/converters.d.ts.map +1 -0
- package/lib/packlets/conversion/converters.js +35 -2
- package/lib/packlets/conversion/converters.js.map +1 -0
- package/lib/packlets/conversion/index.d.ts.map +1 -0
- package/lib/packlets/conversion/index.js.map +1 -0
- package/lib/packlets/crypto-utils/constants.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/constants.js.map +1 -0
- package/lib/packlets/crypto-utils/converters.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/converters.js.map +1 -0
- package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
- package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -0
- package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
- package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/index.browser.js +4 -1
- package/lib/packlets/crypto-utils/index.browser.js.map +1 -0
- package/lib/packlets/crypto-utils/index.d.ts +1 -0
- package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/index.js +4 -1
- package/lib/packlets/crypto-utils/index.js.map +1 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +50 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +66 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
- package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/converters.js +100 -8
- package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
- package/lib/packlets/crypto-utils/keystore/index.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/index.js +1 -0
- package/lib/packlets/crypto-utils/keystore/index.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +125 -12
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.js +431 -118
- package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/model.d.ts +248 -17
- package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/model.js +24 -2
- package/lib/packlets/crypto-utils/keystore/model.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
- package/lib/packlets/crypto-utils/model.d.ts +145 -0
- package/lib/packlets/crypto-utils/model.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/model.js +11 -1
- package/lib/packlets/crypto-utils/model.js.map +1 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +51 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +162 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
- package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -0
- package/lib/packlets/csv/csvFileHelpers.js.map +1 -0
- package/lib/packlets/csv/csvHelpers.d.ts.map +1 -0
- package/lib/packlets/csv/csvHelpers.js.map +1 -0
- package/lib/packlets/csv/index.browser.d.ts.map +1 -0
- package/lib/packlets/csv/index.browser.js.map +1 -0
- package/lib/packlets/csv/index.d.ts.map +1 -0
- package/lib/packlets/csv/index.js.map +1 -0
- package/lib/packlets/experimental/extendedArray.d.ts.map +1 -0
- package/lib/packlets/experimental/extendedArray.js.map +1 -0
- package/lib/packlets/experimental/formatter.d.ts.map +1 -0
- package/lib/packlets/experimental/formatter.js.map +1 -0
- package/lib/packlets/experimental/index.d.ts.map +1 -0
- package/lib/packlets/experimental/index.js.map +1 -0
- package/lib/packlets/experimental/rangeOf.d.ts.map +1 -0
- package/lib/packlets/experimental/rangeOf.js.map +1 -0
- package/lib/packlets/hash/index.browser.d.ts.map +1 -0
- package/lib/packlets/hash/index.browser.js.map +1 -0
- package/lib/packlets/hash/index.d.ts.map +1 -0
- package/lib/packlets/hash/index.js.map +1 -0
- package/lib/packlets/hash/index.node.d.ts.map +1 -0
- package/lib/packlets/hash/index.node.js.map +1 -0
- package/lib/packlets/hash/md5Normalizer.browser.d.ts.map +1 -0
- package/lib/packlets/hash/md5Normalizer.browser.js.map +1 -0
- package/lib/packlets/hash/md5Normalizer.d.ts.map +1 -0
- package/lib/packlets/hash/md5Normalizer.js.map +1 -0
- package/lib/packlets/mustache/index.d.ts.map +1 -0
- package/lib/packlets/mustache/index.js.map +1 -0
- package/lib/packlets/mustache/interfaces.d.ts.map +1 -0
- package/lib/packlets/mustache/interfaces.js.map +1 -0
- package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -0
- package/lib/packlets/mustache/mustacheTemplate.js.map +1 -0
- package/lib/packlets/record-jar/index.browser.d.ts.map +1 -0
- package/lib/packlets/record-jar/index.browser.js.map +1 -0
- package/lib/packlets/record-jar/index.d.ts.map +1 -0
- package/lib/packlets/record-jar/index.js.map +1 -0
- package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -0
- package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
- package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -0
- package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -0
- package/lib/packlets/yaml/converters.d.ts.map +1 -0
- package/lib/packlets/yaml/converters.js.map +1 -0
- package/lib/packlets/yaml/index.d.ts +1 -0
- package/lib/packlets/yaml/index.d.ts.map +1 -0
- package/lib/packlets/yaml/index.js +1 -0
- package/lib/packlets/yaml/index.js.map +1 -0
- package/lib/packlets/yaml/serializers.d.ts +45 -0
- package/lib/packlets/yaml/serializers.d.ts.map +1 -0
- package/lib/packlets/yaml/serializers.js +84 -0
- package/lib/packlets/yaml/serializers.js.map +1 -0
- package/lib/packlets/zip-file-tree/index.d.ts.map +1 -0
- package/lib/packlets/zip-file-tree/index.js.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
- package/package.json +24 -23
|
@@ -99,8 +99,9 @@ function getCurrentTimestamp() {
|
|
|
99
99
|
* @public
|
|
100
100
|
*/
|
|
101
101
|
class KeyStore {
|
|
102
|
-
constructor(cryptoProvider, iterations, keystoreFile, isNew
|
|
102
|
+
constructor(cryptoProvider, iterations, keystoreFile, isNew, privateKeyStorage) {
|
|
103
103
|
this._cryptoProvider = cryptoProvider;
|
|
104
|
+
this._privateKeyStorage = privateKeyStorage;
|
|
104
105
|
this._iterations = iterations;
|
|
105
106
|
this._keystoreFile = keystoreFile;
|
|
106
107
|
this._state = 'locked';
|
|
@@ -123,7 +124,7 @@ class KeyStore {
|
|
|
123
124
|
if (iterations < 1) {
|
|
124
125
|
return (0, ts_utils_1.fail)('Iterations must be at least 1');
|
|
125
126
|
}
|
|
126
|
-
return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true));
|
|
127
|
+
return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true, params.privateKeyStorage));
|
|
127
128
|
}
|
|
128
129
|
/**
|
|
129
130
|
* Opens an existing encrypted key store.
|
|
@@ -139,7 +140,7 @@ class KeyStore {
|
|
|
139
140
|
return (0, ts_utils_1.fail)(`Invalid key store file: ${fileResult.message}`);
|
|
140
141
|
}
|
|
141
142
|
const iterations = fileResult.value.keyDerivation.iterations;
|
|
142
|
-
return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
|
|
143
|
+
return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false, params.privateKeyStorage));
|
|
143
144
|
}
|
|
144
145
|
// ============================================================================
|
|
145
146
|
// Lifecycle Methods
|
|
@@ -182,7 +183,6 @@ class KeyStore {
|
|
|
182
183
|
* @public
|
|
183
184
|
*/
|
|
184
185
|
async unlock(password) {
|
|
185
|
-
var _a;
|
|
186
186
|
if (this._isNew) {
|
|
187
187
|
return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
|
|
188
188
|
}
|
|
@@ -206,57 +206,37 @@ class KeyStore {
|
|
|
206
206
|
if (keyResult.isFailure()) {
|
|
207
207
|
return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
|
|
208
208
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
209
|
+
return this._decryptVault(keyResult.value);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Unlocks an existing key store with a pre-derived key, bypassing
|
|
213
|
+
* PBKDF2 key derivation. Use this when the derived key has been
|
|
214
|
+
* stored externally (e.g., in another key store) and the original
|
|
215
|
+
* password is no longer available.
|
|
216
|
+
*
|
|
217
|
+
* The supplied key must have been derived from the correct password
|
|
218
|
+
* using the key store file's own PBKDF2 parameters (salt and
|
|
219
|
+
* iteration count).
|
|
220
|
+
*
|
|
221
|
+
* @param derivedKey - The pre-derived master key (32 bytes for AES-256)
|
|
222
|
+
* @returns Success with this instance when unlocked, Failure if key is incorrect
|
|
223
|
+
* @public
|
|
224
|
+
*/
|
|
225
|
+
async unlockWithKey(derivedKey) {
|
|
226
|
+
if (this._isNew) {
|
|
227
|
+
return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
|
|
226
228
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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}`);
|
|
229
|
+
if (this._state === 'unlocked') {
|
|
230
|
+
return (0, ts_utils_1.fail)('Key store is already unlocked');
|
|
232
231
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (vaultResult.isFailure()) {
|
|
236
|
-
return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
|
|
232
|
+
if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
|
|
233
|
+
return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
|
|
237
234
|
}
|
|
238
|
-
|
|
239
|
-
this.
|
|
240
|
-
|
|
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);
|
|
235
|
+
/* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
|
|
236
|
+
if (!this._keystoreFile) {
|
|
237
|
+
return (0, ts_utils_1.fail)('No key store file to unlock');
|
|
256
238
|
}
|
|
257
|
-
this.
|
|
258
|
-
this._dirty = false;
|
|
259
|
-
return (0, ts_utils_1.succeed)(this);
|
|
239
|
+
return this._decryptVault(derivedKey);
|
|
260
240
|
}
|
|
261
241
|
/**
|
|
262
242
|
* Locks the key store, clearing all secrets from memory.
|
|
@@ -274,7 +254,9 @@ class KeyStore {
|
|
|
274
254
|
// Clear secrets from memory (overwrite for security)
|
|
275
255
|
if (this._secrets) {
|
|
276
256
|
for (const entry of this._secrets.values()) {
|
|
277
|
-
entry.
|
|
257
|
+
if (entry.type !== 'asymmetric-keypair') {
|
|
258
|
+
entry.key.fill(0);
|
|
259
|
+
}
|
|
278
260
|
}
|
|
279
261
|
this._secrets.clear();
|
|
280
262
|
this._secrets = undefined;
|
|
@@ -336,7 +318,9 @@ class KeyStore {
|
|
|
336
318
|
return (0, ts_utils_1.succeed)(Array.from(this._secrets.keys()));
|
|
337
319
|
}
|
|
338
320
|
/**
|
|
339
|
-
* Gets a secret by name.
|
|
321
|
+
* Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
|
|
322
|
+
* — callers must check `entry.type` before accessing `key`/`id` since asymmetric
|
|
323
|
+
* entries carry no raw key material.
|
|
340
324
|
* @param name - Name of the secret
|
|
341
325
|
* @returns Success with secret entry, Failure if not found or locked
|
|
342
326
|
* @public
|
|
@@ -351,6 +335,27 @@ class KeyStore {
|
|
|
351
335
|
}
|
|
352
336
|
return (0, ts_utils_1.succeed)(entry);
|
|
353
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Returns the public-key JWK for an asymmetric-keypair entry.
|
|
340
|
+
* Available without {@link CryptoUtils.KeyStore.IPrivateKeyStorage} since the
|
|
341
|
+
* public key lives in the vault metadata directly.
|
|
342
|
+
* @param name - Name of the entry
|
|
343
|
+
* @returns Success with the JWK, Failure if not found, locked, or wrong type
|
|
344
|
+
* @public
|
|
345
|
+
*/
|
|
346
|
+
getPublicKeyJwk(name) {
|
|
347
|
+
if (!this._secrets) {
|
|
348
|
+
return (0, ts_utils_1.fail)('Key store is locked');
|
|
349
|
+
}
|
|
350
|
+
const entry = this._secrets.get(name);
|
|
351
|
+
if (!entry) {
|
|
352
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
|
|
353
|
+
}
|
|
354
|
+
if (entry.type !== 'asymmetric-keypair') {
|
|
355
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
|
|
356
|
+
}
|
|
357
|
+
return (0, ts_utils_1.succeed)(entry.publicKeyJwk);
|
|
358
|
+
}
|
|
354
359
|
/**
|
|
355
360
|
* Checks if a secret exists.
|
|
356
361
|
* @param name - Name of the secret
|
|
@@ -377,7 +382,6 @@ class KeyStore {
|
|
|
377
382
|
if (!name || name.length === 0) {
|
|
378
383
|
return (0, ts_utils_1.fail)('Secret name cannot be empty');
|
|
379
384
|
}
|
|
380
|
-
const replaced = this._secrets.has(name);
|
|
381
385
|
// Generate a new random key
|
|
382
386
|
const keyResult = await this._cryptoProvider.generateKey();
|
|
383
387
|
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
@@ -391,19 +395,28 @@ class KeyStore {
|
|
|
391
395
|
description: options === null || options === void 0 ? void 0 : options.description,
|
|
392
396
|
createdAt: getCurrentTimestamp()
|
|
393
397
|
};
|
|
398
|
+
const existing = this._secrets.get(name);
|
|
399
|
+
const warning = existing ? await this._releaseEntryResources(existing) : undefined;
|
|
394
400
|
this._secrets.set(name, entry);
|
|
395
401
|
this._dirty = true;
|
|
396
|
-
return (0, ts_utils_1.succeed)({ entry, replaced });
|
|
402
|
+
return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
|
|
397
403
|
}
|
|
398
404
|
/**
|
|
399
|
-
* Imports
|
|
405
|
+
* Imports raw 32-byte key material into the vault.
|
|
406
|
+
*
|
|
407
|
+
* Always validates that the key is exactly 32 bytes (AES-256). The optional
|
|
408
|
+
* `type` field is a classification label stored with the entry; it does not
|
|
409
|
+
* change the validation rules. For importing UTF-8 API key strings (variable
|
|
410
|
+
* length), use {@link KeyStore.importApiKey} instead.
|
|
411
|
+
*
|
|
400
412
|
* @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
|
|
413
|
+
* @param key - The 32-byte AES-256 key material
|
|
414
|
+
* @param options - Optional type classification, description, whether to replace existing
|
|
403
415
|
* @returns Success with entry, Failure if locked, key invalid, or exists and !replace
|
|
404
416
|
* @public
|
|
405
417
|
*/
|
|
406
|
-
importSecret(name, key, options) {
|
|
418
|
+
async importSecret(name, key, options) {
|
|
419
|
+
var _a;
|
|
407
420
|
if (!this._secrets) {
|
|
408
421
|
return (0, ts_utils_1.fail)('Key store is locked');
|
|
409
422
|
}
|
|
@@ -413,20 +426,21 @@ class KeyStore {
|
|
|
413
426
|
if (key.length !== Constants.AES_256_KEY_SIZE) {
|
|
414
427
|
return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
|
|
415
428
|
}
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
429
|
+
const existing = this._secrets.get(name);
|
|
430
|
+
if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
|
|
418
431
|
return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
419
432
|
}
|
|
420
433
|
const entry = {
|
|
421
434
|
name,
|
|
422
|
-
type: 'encryption-key',
|
|
435
|
+
type: (_a = options === null || options === void 0 ? void 0 : options.type) !== null && _a !== void 0 ? _a : 'encryption-key',
|
|
423
436
|
key: new Uint8Array(key), // Copy to prevent external modification
|
|
424
437
|
description: options === null || options === void 0 ? void 0 : options.description,
|
|
425
438
|
createdAt: getCurrentTimestamp()
|
|
426
439
|
};
|
|
440
|
+
const warning = existing ? await this._releaseEntryResources(existing) : undefined;
|
|
427
441
|
this._secrets.set(name, entry);
|
|
428
442
|
this._dirty = true;
|
|
429
|
-
return (0, ts_utils_1.succeed)({ entry, replaced:
|
|
443
|
+
return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
|
|
430
444
|
}
|
|
431
445
|
/**
|
|
432
446
|
* Adds a secret derived from a password using PBKDF2.
|
|
@@ -453,8 +467,8 @@ class KeyStore {
|
|
|
453
467
|
if (!password || password.length === 0) {
|
|
454
468
|
return (0, ts_utils_1.fail)('Password cannot be empty');
|
|
455
469
|
}
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
470
|
+
const existing = this._secrets.get(name);
|
|
471
|
+
if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
|
|
458
472
|
return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
459
473
|
}
|
|
460
474
|
const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_SECRET_ITERATIONS;
|
|
@@ -477,11 +491,13 @@ class KeyStore {
|
|
|
477
491
|
description: options === null || options === void 0 ? void 0 : options.description,
|
|
478
492
|
createdAt: getCurrentTimestamp()
|
|
479
493
|
};
|
|
494
|
+
const warning = existing ? await this._releaseEntryResources(existing) : undefined;
|
|
480
495
|
this._secrets.set(name, entry);
|
|
481
496
|
this._dirty = true;
|
|
482
497
|
return (0, ts_utils_1.succeed)({
|
|
483
498
|
entry,
|
|
484
|
-
replaced:
|
|
499
|
+
replaced: existing !== undefined,
|
|
500
|
+
warning,
|
|
485
501
|
keyDerivation: {
|
|
486
502
|
kdf: 'pbkdf2',
|
|
487
503
|
salt: this._cryptoProvider.toBase64(saltResult.value),
|
|
@@ -490,12 +506,16 @@ class KeyStore {
|
|
|
490
506
|
});
|
|
491
507
|
}
|
|
492
508
|
/**
|
|
493
|
-
* Removes a secret by name.
|
|
509
|
+
* Removes a secret by name. Vault-first: the in-memory vault entry is dropped
|
|
510
|
+
* before any storage cleanup runs. For asymmetric-keypair entries, best-effort
|
|
511
|
+
* calls {@link CryptoUtils.KeyStore.IPrivateKeyStorage}.delete on the entry's
|
|
512
|
+
* `id`; a failure is reported via `warning` on the result but does not roll
|
|
513
|
+
* back the vault removal.
|
|
494
514
|
* @param name - Name of the secret to remove
|
|
495
|
-
* @returns Success with removed entry, Failure if not found or locked
|
|
515
|
+
* @returns Success with removed entry (and optional warning), Failure if not found or locked
|
|
496
516
|
* @public
|
|
497
517
|
*/
|
|
498
|
-
removeSecret(name) {
|
|
518
|
+
async removeSecret(name) {
|
|
499
519
|
if (!this._secrets) {
|
|
500
520
|
return (0, ts_utils_1.fail)('Key store is locked');
|
|
501
521
|
}
|
|
@@ -503,11 +523,12 @@ class KeyStore {
|
|
|
503
523
|
if (!entry) {
|
|
504
524
|
return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
|
|
505
525
|
}
|
|
506
|
-
//
|
|
507
|
-
|
|
526
|
+
// Vault-first: drop the in-memory entry before touching storage so a
|
|
527
|
+
// storage failure cannot block removal.
|
|
508
528
|
this._secrets.delete(name);
|
|
509
529
|
this._dirty = true;
|
|
510
|
-
|
|
530
|
+
const warning = await this._releaseEntryResources(entry);
|
|
531
|
+
return (0, ts_utils_1.succeed)({ entry, warning });
|
|
511
532
|
}
|
|
512
533
|
/**
|
|
513
534
|
* Imports an API key string into the vault.
|
|
@@ -518,7 +539,7 @@ class KeyStore {
|
|
|
518
539
|
* @returns Success with entry, Failure if locked, empty, or exists and !replace
|
|
519
540
|
* @public
|
|
520
541
|
*/
|
|
521
|
-
importApiKey(name, apiKey, options) {
|
|
542
|
+
async importApiKey(name, apiKey, options) {
|
|
522
543
|
if (!this._secrets) {
|
|
523
544
|
return (0, ts_utils_1.fail)('Key store is locked');
|
|
524
545
|
}
|
|
@@ -528,8 +549,8 @@ class KeyStore {
|
|
|
528
549
|
if (!apiKey || apiKey.length === 0) {
|
|
529
550
|
return (0, ts_utils_1.fail)('API key cannot be empty');
|
|
530
551
|
}
|
|
531
|
-
const
|
|
532
|
-
if (
|
|
552
|
+
const existing = this._secrets.get(name);
|
|
553
|
+
if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
|
|
533
554
|
return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
534
555
|
}
|
|
535
556
|
const encoder = new TextEncoder();
|
|
@@ -540,9 +561,10 @@ class KeyStore {
|
|
|
540
561
|
description: options === null || options === void 0 ? void 0 : options.description,
|
|
541
562
|
createdAt: getCurrentTimestamp()
|
|
542
563
|
};
|
|
564
|
+
const warning = existing ? await this._releaseEntryResources(existing) : undefined;
|
|
543
565
|
this._secrets.set(name, entry);
|
|
544
566
|
this._dirty = true;
|
|
545
|
-
return (0, ts_utils_1.succeed)({ entry, replaced:
|
|
567
|
+
return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
|
|
546
568
|
}
|
|
547
569
|
/**
|
|
548
570
|
* Retrieves an API key string by name.
|
|
@@ -565,6 +587,118 @@ class KeyStore {
|
|
|
565
587
|
const decoder = new TextDecoder();
|
|
566
588
|
return (0, ts_utils_1.succeed)(decoder.decode(entry.key));
|
|
567
589
|
}
|
|
590
|
+
// ============================================================================
|
|
591
|
+
// Asymmetric Keypair Management
|
|
592
|
+
// ============================================================================
|
|
593
|
+
/**
|
|
594
|
+
* Adds a new asymmetric keypair to the vault. Storage-first: the private key
|
|
595
|
+
* is stored under a freshly-minted `id` before the public-key vault entry is
|
|
596
|
+
* committed. If the storage call fails, no vault entry is written and the
|
|
597
|
+
* operation returns Failure.
|
|
598
|
+
*
|
|
599
|
+
* When `replace: true` displaces an existing entry (asymmetric or symmetric),
|
|
600
|
+
* a fresh `id` is minted; the displaced entry's resources are released
|
|
601
|
+
* best-effort. Failure of the storage delete is reported via `warning` on the
|
|
602
|
+
* result but does not roll back the replacement.
|
|
603
|
+
*
|
|
604
|
+
* Requires a {@link CryptoUtils.KeyStore.IPrivateKeyStorage} backend
|
|
605
|
+
* supplied at construction.
|
|
606
|
+
*
|
|
607
|
+
* @param name - Unique name for the entry
|
|
608
|
+
* @param options - Algorithm, optional description, replace flag
|
|
609
|
+
* @returns Success with the new entry, Failure if locked, no provider, or storage write failed
|
|
610
|
+
* @public
|
|
611
|
+
*/
|
|
612
|
+
async addKeyPair(name, options) {
|
|
613
|
+
if (!this._secrets) {
|
|
614
|
+
return (0, ts_utils_1.fail)('Key store is locked');
|
|
615
|
+
}
|
|
616
|
+
if (!name || name.length === 0) {
|
|
617
|
+
return (0, ts_utils_1.fail)('Entry name cannot be empty');
|
|
618
|
+
}
|
|
619
|
+
if (!this._privateKeyStorage) {
|
|
620
|
+
return (0, ts_utils_1.fail)('No private key storage configured');
|
|
621
|
+
}
|
|
622
|
+
const existing = this._secrets.get(name);
|
|
623
|
+
if (existing && !options.replace) {
|
|
624
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
|
|
625
|
+
}
|
|
626
|
+
// Generate the keypair before touching storage. extractable=true on backends
|
|
627
|
+
// that round-trip via JWK; extractable=false on backends that hold CryptoKey
|
|
628
|
+
// refs directly.
|
|
629
|
+
const extractable = !this._privateKeyStorage.supportsNonExtractable;
|
|
630
|
+
const keyPairResult = await this._cryptoProvider.generateKeyPair(options.algorithm, extractable);
|
|
631
|
+
/* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests; cannot be triggered here without mocking */
|
|
632
|
+
if (keyPairResult.isFailure()) {
|
|
633
|
+
return (0, ts_utils_1.fail)(`Failed to generate keypair for '${name}': ${keyPairResult.message}`);
|
|
634
|
+
}
|
|
635
|
+
const { publicKey, privateKey } = keyPairResult.value;
|
|
636
|
+
const jwkResult = await this._cryptoProvider.exportPublicKeyJwk(publicKey);
|
|
637
|
+
/* c8 ignore next 3 - export of an extractable freshly-generated public key is hard to fail */
|
|
638
|
+
if (jwkResult.isFailure()) {
|
|
639
|
+
return (0, ts_utils_1.fail)(`Failed to export public key for '${name}': ${jwkResult.message}`);
|
|
640
|
+
}
|
|
641
|
+
const idResult = this._generateId();
|
|
642
|
+
/* c8 ignore next 3 - random-bytes failure is hard to trigger with a healthy provider */
|
|
643
|
+
if (idResult.isFailure()) {
|
|
644
|
+
return (0, ts_utils_1.fail)(`Failed to mint storage id for '${name}': ${idResult.message}`);
|
|
645
|
+
}
|
|
646
|
+
const id = idResult.value;
|
|
647
|
+
// Storage-first: write the private key before committing the vault entry.
|
|
648
|
+
const storeResult = await this._privateKeyStorage.store(id, privateKey);
|
|
649
|
+
if (storeResult.isFailure()) {
|
|
650
|
+
return (0, ts_utils_1.fail)(`Failed to persist private key for '${name}': ${storeResult.message}`);
|
|
651
|
+
}
|
|
652
|
+
const entry = {
|
|
653
|
+
name,
|
|
654
|
+
type: 'asymmetric-keypair',
|
|
655
|
+
id,
|
|
656
|
+
algorithm: options.algorithm,
|
|
657
|
+
publicKeyJwk: jwkResult.value,
|
|
658
|
+
description: options.description,
|
|
659
|
+
createdAt: getCurrentTimestamp()
|
|
660
|
+
};
|
|
661
|
+
const warning = existing ? await this._releaseEntryResources(existing) : undefined;
|
|
662
|
+
this._secrets.set(name, entry);
|
|
663
|
+
this._dirty = true;
|
|
664
|
+
return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Retrieves the keypair for an asymmetric-keypair entry. The private key is
|
|
668
|
+
* loaded from {@link CryptoUtils.KeyStore.IPrivateKeyStorage} on every call —
|
|
669
|
+
* the keystore never caches private `CryptoKey` references between calls.
|
|
670
|
+
* The public key is re-imported from the vault's JWK so callers always
|
|
671
|
+
* receive a `CryptoKey` rather than the JWK form.
|
|
672
|
+
* @param name - Name of the entry
|
|
673
|
+
* @returns Success with `{ publicKey, privateKey }`, Failure if not found,
|
|
674
|
+
* locked, wrong type, no provider, or storage load failed.
|
|
675
|
+
* @public
|
|
676
|
+
*/
|
|
677
|
+
async getKeyPair(name) {
|
|
678
|
+
if (!this._secrets) {
|
|
679
|
+
return (0, ts_utils_1.fail)('Key store is locked');
|
|
680
|
+
}
|
|
681
|
+
const entry = this._secrets.get(name);
|
|
682
|
+
if (!entry) {
|
|
683
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
|
|
684
|
+
}
|
|
685
|
+
if (entry.type !== 'asymmetric-keypair') {
|
|
686
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
|
|
687
|
+
}
|
|
688
|
+
if (!this._privateKeyStorage) {
|
|
689
|
+
return (0, ts_utils_1.fail)('No private key storage configured');
|
|
690
|
+
}
|
|
691
|
+
const privateResult = await this._privateKeyStorage.load(entry.id);
|
|
692
|
+
if (privateResult.isFailure()) {
|
|
693
|
+
return (0, ts_utils_1.fail)(`Failed to load private key for '${name}': ${privateResult.message}`);
|
|
694
|
+
}
|
|
695
|
+
const publicResult = await this._cryptoProvider.importPublicKeyJwk(entry.publicKeyJwk, entry.algorithm);
|
|
696
|
+
/* c8 ignore next 3 - vault JWKs that previously exported cleanly are extremely unlikely to fail re-import */
|
|
697
|
+
if (publicResult.isFailure()) {
|
|
698
|
+
return (0, ts_utils_1.fail)(`Failed to re-import public key for '${name}': ${publicResult.message}`);
|
|
699
|
+
}
|
|
700
|
+
return (0, ts_utils_1.succeed)({ publicKey: publicResult.value, privateKey: privateResult.value });
|
|
701
|
+
}
|
|
568
702
|
/**
|
|
569
703
|
* Lists secret names filtered by type.
|
|
570
704
|
* @param type - The secret type to filter by
|
|
@@ -604,7 +738,8 @@ class KeyStore {
|
|
|
604
738
|
if (oldName !== newName && this._secrets.has(newName)) {
|
|
605
739
|
return (0, ts_utils_1.fail)(`Secret '${newName}' already exists`);
|
|
606
740
|
}
|
|
607
|
-
// Create new entry with new name
|
|
741
|
+
// Create new entry with new name. For asymmetric entries the spread
|
|
742
|
+
// preserves `id` so the storage handle survives the rename.
|
|
608
743
|
const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
|
|
609
744
|
this._secrets.delete(oldName);
|
|
610
745
|
this._secrets.set(newName, newEntry);
|
|
@@ -634,49 +769,29 @@ class KeyStore {
|
|
|
634
769
|
if (keyResult.isFailure()) {
|
|
635
770
|
return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
|
|
636
771
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (jsonResult.isFailure()) {
|
|
656
|
-
return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
|
|
772
|
+
return this._encryptVault(keyResult.value);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Saves the key store using a pre-derived key, bypassing PBKDF2 key
|
|
776
|
+
* derivation. Use this when the derived key has been stored externally
|
|
777
|
+
* (e.g., in another key store) and the original password is no longer
|
|
778
|
+
* available.
|
|
779
|
+
*
|
|
780
|
+
* The supplied key must be the same key that was (or would be) derived
|
|
781
|
+
* from the master password using the key store's PBKDF2 parameters.
|
|
782
|
+
*
|
|
783
|
+
* @param derivedKey - The pre-derived master key (32 bytes for AES-256)
|
|
784
|
+
* @returns Success with IKeyStoreFile, Failure if locked or key invalid
|
|
785
|
+
* @public
|
|
786
|
+
*/
|
|
787
|
+
async saveWithKey(derivedKey) {
|
|
788
|
+
if (!this._secrets || !this._salt) {
|
|
789
|
+
return (0, ts_utils_1.fail)('Key store is locked');
|
|
657
790
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (encryptResult.isFailure()) {
|
|
661
|
-
return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
|
|
791
|
+
if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
|
|
792
|
+
return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
|
|
662
793
|
}
|
|
663
|
-
|
|
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);
|
|
794
|
+
return this._encryptVault(derivedKey);
|
|
680
795
|
}
|
|
681
796
|
/**
|
|
682
797
|
* Changes the master password.
|
|
@@ -744,6 +859,9 @@ class KeyStore {
|
|
|
744
859
|
if (secretResult.isFailure()) {
|
|
745
860
|
return (0, ts_utils_1.fail)(`encryptByName: ${secretResult.message}`);
|
|
746
861
|
}
|
|
862
|
+
if (secretResult.value.type === 'asymmetric-keypair') {
|
|
863
|
+
return (0, ts_utils_1.fail)(`encryptByName: secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
|
|
864
|
+
}
|
|
747
865
|
return (0, encryptedFile_1.createEncryptedFile)({
|
|
748
866
|
content,
|
|
749
867
|
secretName,
|
|
@@ -771,6 +889,9 @@ class KeyStore {
|
|
|
771
889
|
if (!entry) {
|
|
772
890
|
return (0, ts_utils_1.fail)(`Secret '${secretName}' not found in key store`);
|
|
773
891
|
}
|
|
892
|
+
if (entry.type === 'asymmetric-keypair') {
|
|
893
|
+
return (0, ts_utils_1.fail)(`Secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
|
|
894
|
+
}
|
|
774
895
|
return (0, ts_utils_1.succeed)(entry.key);
|
|
775
896
|
};
|
|
776
897
|
return (0, ts_utils_1.succeed)(provider);
|
|
@@ -790,6 +911,198 @@ class KeyStore {
|
|
|
790
911
|
cryptoProvider: this._cryptoProvider
|
|
791
912
|
});
|
|
792
913
|
}
|
|
914
|
+
// ============================================================================
|
|
915
|
+
// Private: Vault Encryption / Decryption
|
|
916
|
+
// ============================================================================
|
|
917
|
+
/**
|
|
918
|
+
* Encrypts the vault with a derived key and returns the key store file.
|
|
919
|
+
* Shared by `save()` and `saveWithKey()`.
|
|
920
|
+
*/
|
|
921
|
+
async _encryptVault(derivedKey) {
|
|
922
|
+
// _secrets and _salt are guaranteed non-undefined by callers
|
|
923
|
+
const secrets = this._secrets;
|
|
924
|
+
const salt = this._salt;
|
|
925
|
+
// Build vault contents
|
|
926
|
+
const secretEntries = {};
|
|
927
|
+
for (const [name, entry] of secrets) {
|
|
928
|
+
if (entry.type === 'asymmetric-keypair') {
|
|
929
|
+
secretEntries[name] = {
|
|
930
|
+
name: entry.name,
|
|
931
|
+
type: entry.type,
|
|
932
|
+
id: entry.id,
|
|
933
|
+
algorithm: entry.algorithm,
|
|
934
|
+
publicKeyJwk: entry.publicKeyJwk,
|
|
935
|
+
description: entry.description,
|
|
936
|
+
createdAt: entry.createdAt
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
secretEntries[name] = {
|
|
941
|
+
name: entry.name,
|
|
942
|
+
type: entry.type,
|
|
943
|
+
key: this._cryptoProvider.toBase64(entry.key),
|
|
944
|
+
description: entry.description,
|
|
945
|
+
createdAt: entry.createdAt
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const vaultContents = {
|
|
950
|
+
version: model_1.KEYSTORE_FORMAT,
|
|
951
|
+
secrets: secretEntries
|
|
952
|
+
};
|
|
953
|
+
// Serialize and encrypt
|
|
954
|
+
const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
|
|
955
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
956
|
+
if (jsonResult.isFailure()) {
|
|
957
|
+
return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
|
|
958
|
+
}
|
|
959
|
+
const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
|
|
960
|
+
/* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
|
|
961
|
+
if (encryptResult.isFailure()) {
|
|
962
|
+
return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
|
|
963
|
+
}
|
|
964
|
+
const { iv, authTag, encryptedData } = encryptResult.value;
|
|
965
|
+
const keystoreFileData = {
|
|
966
|
+
format: model_1.KEYSTORE_FORMAT,
|
|
967
|
+
algorithm: Constants.DEFAULT_ALGORITHM,
|
|
968
|
+
iv: this._cryptoProvider.toBase64(iv),
|
|
969
|
+
authTag: this._cryptoProvider.toBase64(authTag),
|
|
970
|
+
encryptedData: this._cryptoProvider.toBase64(encryptedData),
|
|
971
|
+
keyDerivation: {
|
|
972
|
+
kdf: 'pbkdf2',
|
|
973
|
+
salt: this._cryptoProvider.toBase64(salt),
|
|
974
|
+
iterations: this._iterations
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
this._keystoreFile = keystoreFileData;
|
|
978
|
+
this._dirty = false;
|
|
979
|
+
this._isNew = false;
|
|
980
|
+
return (0, ts_utils_1.succeed)(keystoreFileData);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Decrypts the vault with a derived key and loads secrets into memory.
|
|
984
|
+
* Shared by `unlock()` and `unlockWithKey()`.
|
|
985
|
+
*/
|
|
986
|
+
async _decryptVault(derivedKey) {
|
|
987
|
+
const keystoreFile = this._keystoreFile;
|
|
988
|
+
if (keystoreFile === undefined) {
|
|
989
|
+
return (0, ts_utils_1.fail)('No key store file loaded');
|
|
990
|
+
}
|
|
991
|
+
const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
|
|
992
|
+
const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
|
|
993
|
+
const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
|
|
994
|
+
/* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
|
|
995
|
+
if (ivResult.isFailure()) {
|
|
996
|
+
return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
|
|
997
|
+
}
|
|
998
|
+
if (authTagResult.isFailure()) {
|
|
999
|
+
return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (encryptedDataResult.isFailure()) {
|
|
1002
|
+
return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
|
|
1005
|
+
if (decryptResult.isFailure()) {
|
|
1006
|
+
return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
|
|
1007
|
+
}
|
|
1008
|
+
// Parse the vault contents
|
|
1009
|
+
const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
|
|
1010
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
1011
|
+
if (parseResult.isFailure()) {
|
|
1012
|
+
return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
|
|
1013
|
+
}
|
|
1014
|
+
const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
|
|
1015
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
1016
|
+
if (vaultResult.isFailure()) {
|
|
1017
|
+
return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
|
|
1018
|
+
}
|
|
1019
|
+
// Build secrets into local variables to avoid partial state on failure
|
|
1020
|
+
const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
|
|
1021
|
+
if (saltResult.isFailure()) {
|
|
1022
|
+
return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
|
|
1023
|
+
}
|
|
1024
|
+
const secrets = new Map();
|
|
1025
|
+
for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
|
|
1026
|
+
if (jsonEntry.type === 'asymmetric-keypair') {
|
|
1027
|
+
const entry = {
|
|
1028
|
+
name,
|
|
1029
|
+
type: jsonEntry.type,
|
|
1030
|
+
id: jsonEntry.id,
|
|
1031
|
+
algorithm: jsonEntry.algorithm,
|
|
1032
|
+
publicKeyJwk: jsonEntry.publicKeyJwk,
|
|
1033
|
+
description: jsonEntry.description,
|
|
1034
|
+
createdAt: jsonEntry.createdAt
|
|
1035
|
+
};
|
|
1036
|
+
secrets.set(name, entry);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
|
|
1040
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
1041
|
+
if (keyBytesResult.isFailure()) {
|
|
1042
|
+
return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
|
|
1043
|
+
}
|
|
1044
|
+
const entry = {
|
|
1045
|
+
name,
|
|
1046
|
+
type: jsonEntry.type,
|
|
1047
|
+
key: keyBytesResult.value,
|
|
1048
|
+
description: jsonEntry.description,
|
|
1049
|
+
createdAt: jsonEntry.createdAt
|
|
1050
|
+
};
|
|
1051
|
+
secrets.set(name, entry);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// All validation passed — commit state atomically
|
|
1055
|
+
this._salt = saltResult.value;
|
|
1056
|
+
this._secrets = secrets;
|
|
1057
|
+
this._state = 'unlocked';
|
|
1058
|
+
this._dirty = false;
|
|
1059
|
+
return (0, ts_utils_1.succeed)(this);
|
|
1060
|
+
}
|
|
1061
|
+
// ============================================================================
|
|
1062
|
+
// Private: Helpers for asymmetric flows
|
|
1063
|
+
// ============================================================================
|
|
1064
|
+
/**
|
|
1065
|
+
* Releases the resources held by an entry being displaced from the vault.
|
|
1066
|
+
* Symmetric entries get their key buffer zeroed in place. Asymmetric entries
|
|
1067
|
+
* have their private-key blob best-effort deleted from
|
|
1068
|
+
* {@link CryptoUtils.KeyStore.IPrivateKeyStorage}; if the storage call fails,
|
|
1069
|
+
* a warning string is returned but the displacement still proceeds — the
|
|
1070
|
+
* orphaned blob is left for consumer-side GC. Without a configured provider,
|
|
1071
|
+
* asymmetric cleanup is silently skipped.
|
|
1072
|
+
* @returns A warning string if storage cleanup failed, otherwise undefined.
|
|
1073
|
+
*/
|
|
1074
|
+
async _releaseEntryResources(entry) {
|
|
1075
|
+
if (entry.type === 'asymmetric-keypair') {
|
|
1076
|
+
if (!this._privateKeyStorage) {
|
|
1077
|
+
return undefined;
|
|
1078
|
+
}
|
|
1079
|
+
const deleteResult = await this._privateKeyStorage.delete(entry.id);
|
|
1080
|
+
if (deleteResult.isFailure()) {
|
|
1081
|
+
return `Failed to delete prior storage blob for '${entry.name}' (id ${entry.id}): ${deleteResult.message}`;
|
|
1082
|
+
}
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
entry.key.fill(0);
|
|
1086
|
+
return undefined;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Mints a fresh UUID v4 storage handle using the crypto provider's
|
|
1090
|
+
* {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
|
|
1091
|
+
* Random-bytes failures propagate as Failure.
|
|
1092
|
+
*/
|
|
1093
|
+
_generateId() {
|
|
1094
|
+
return this._cryptoProvider.generateRandomBytes(16).onSuccess((bytes) => {
|
|
1095
|
+
// Per RFC 4122 §4.4: set version (4) and variant (10xx) bits.
|
|
1096
|
+
// eslint-disable-next-line no-bitwise
|
|
1097
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
1098
|
+
// eslint-disable-next-line no-bitwise
|
|
1099
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
1100
|
+
const hex = Array.from(bytes)
|
|
1101
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1102
|
+
.join('');
|
|
1103
|
+
return (0, ts_utils_1.succeed)(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
793
1106
|
}
|
|
794
1107
|
exports.KeyStore = KeyStore;
|
|
795
1108
|
//# sourceMappingURL=keyStore.js.map
|