@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.
Files changed (272) hide show
  1. package/dist/index.browser.js +2 -1
  2. package/dist/index.browser.js.map +1 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/packlets/ai-assist/apiClient.js +807 -67
  5. package/dist/packlets/ai-assist/apiClient.js.map +1 -0
  6. package/dist/packlets/ai-assist/chatRequestBuilders.js +180 -0
  7. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  8. package/dist/packlets/ai-assist/converters.js +2 -1
  9. package/dist/packlets/ai-assist/converters.js.map +1 -0
  10. package/dist/packlets/ai-assist/endpoint.js +78 -0
  11. package/dist/packlets/ai-assist/endpoint.js.map +1 -0
  12. package/dist/packlets/ai-assist/index.js +4 -3
  13. package/dist/packlets/ai-assist/index.js.map +1 -0
  14. package/dist/packlets/ai-assist/model.js +20 -3
  15. package/dist/packlets/ai-assist/model.js.map +1 -0
  16. package/dist/packlets/ai-assist/registry.js +137 -10
  17. package/dist/packlets/ai-assist/registry.js.map +1 -0
  18. package/dist/packlets/ai-assist/sseParser.js +122 -0
  19. package/dist/packlets/ai-assist/sseParser.js.map +1 -0
  20. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +192 -0
  21. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  22. package/dist/packlets/ai-assist/streamingAdapters/common.js +77 -0
  23. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  24. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +160 -0
  25. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  26. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +150 -0
  27. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  28. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +164 -0
  29. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  30. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +157 -0
  31. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  32. package/dist/packlets/ai-assist/streamingClient.js +94 -0
  33. package/dist/packlets/ai-assist/streamingClient.js.map +1 -0
  34. package/dist/packlets/ai-assist/toolFormats.js.map +1 -0
  35. package/dist/packlets/conversion/converters.js +34 -1
  36. package/dist/packlets/conversion/converters.js.map +1 -0
  37. package/dist/packlets/conversion/index.js.map +1 -0
  38. package/dist/packlets/crypto-utils/constants.js.map +1 -0
  39. package/dist/packlets/crypto-utils/converters.js.map +1 -0
  40. package/dist/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  41. package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -0
  42. package/dist/packlets/crypto-utils/index.browser.js +2 -0
  43. package/dist/packlets/crypto-utils/index.browser.js.map +1 -0
  44. package/dist/packlets/crypto-utils/index.js +2 -0
  45. package/dist/packlets/crypto-utils/index.js.map +1 -0
  46. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +63 -0
  47. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  48. package/dist/packlets/crypto-utils/keystore/converters.js +101 -9
  49. package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -0
  50. package/dist/packlets/crypto-utils/keystore/index.js +1 -0
  51. package/dist/packlets/crypto-utils/keystore/index.js.map +1 -0
  52. package/dist/packlets/crypto-utils/keystore/keyStore.js +431 -118
  53. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  54. package/dist/packlets/crypto-utils/keystore/model.js +22 -1
  55. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -0
  56. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
  57. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  58. package/dist/packlets/crypto-utils/model.js +10 -0
  59. package/dist/packlets/crypto-utils/model.js.map +1 -0
  60. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +163 -1
  61. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  62. package/dist/packlets/csv/csvFileHelpers.js.map +1 -0
  63. package/dist/packlets/csv/csvHelpers.js.map +1 -0
  64. package/dist/packlets/csv/index.browser.js.map +1 -0
  65. package/dist/packlets/csv/index.js.map +1 -0
  66. package/dist/packlets/experimental/extendedArray.js.map +1 -0
  67. package/dist/packlets/experimental/formatter.js.map +1 -0
  68. package/dist/packlets/experimental/index.js.map +1 -0
  69. package/dist/packlets/experimental/rangeOf.js.map +1 -0
  70. package/dist/packlets/hash/index.browser.js.map +1 -0
  71. package/dist/packlets/hash/index.js.map +1 -0
  72. package/dist/packlets/hash/index.node.js.map +1 -0
  73. package/dist/packlets/hash/md5Normalizer.browser.js.map +1 -0
  74. package/dist/packlets/hash/md5Normalizer.js.map +1 -0
  75. package/dist/packlets/mustache/index.js.map +1 -0
  76. package/dist/packlets/mustache/interfaces.js.map +1 -0
  77. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -0
  78. package/dist/packlets/record-jar/index.browser.js.map +1 -0
  79. package/dist/packlets/record-jar/index.js.map +1 -0
  80. package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  81. package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -0
  82. package/dist/packlets/yaml/converters.js.map +1 -0
  83. package/dist/packlets/yaml/index.js +1 -0
  84. package/dist/packlets/yaml/index.js.map +1 -0
  85. package/dist/packlets/yaml/serializers.js +48 -0
  86. package/dist/packlets/yaml/serializers.js.map +1 -0
  87. package/dist/packlets/zip-file-tree/index.js.map +1 -0
  88. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  89. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  90. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  91. package/dist/ts-extras.d.ts +1499 -41
  92. package/dist/tsdoc-metadata.json +1 -1
  93. package/lib/index.browser.d.ts +2 -1
  94. package/lib/index.browser.d.ts.map +1 -0
  95. package/lib/index.browser.js +3 -1
  96. package/lib/index.browser.js.map +1 -0
  97. package/lib/index.d.ts.map +1 -0
  98. package/lib/index.js.map +1 -0
  99. package/lib/packlets/ai-assist/apiClient.d.ts +140 -1
  100. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -0
  101. package/lib/packlets/ai-assist/apiClient.js +810 -66
  102. package/lib/packlets/ai-assist/apiClient.js.map +1 -0
  103. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +89 -0
  104. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -0
  105. package/lib/packlets/ai-assist/chatRequestBuilders.js +189 -0
  106. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  107. package/lib/packlets/ai-assist/converters.d.ts.map +1 -0
  108. package/lib/packlets/ai-assist/converters.js +2 -1
  109. package/lib/packlets/ai-assist/converters.js.map +1 -0
  110. package/lib/packlets/ai-assist/endpoint.d.ts +28 -0
  111. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
  112. package/lib/packlets/ai-assist/endpoint.js +82 -0
  113. package/lib/packlets/ai-assist/endpoint.js.map +1 -0
  114. package/lib/packlets/ai-assist/index.d.ts +4 -3
  115. package/lib/packlets/ai-assist/index.d.ts.map +1 -0
  116. package/lib/packlets/ai-assist/index.js +12 -1
  117. package/lib/packlets/ai-assist/index.js.map +1 -0
  118. package/lib/packlets/ai-assist/model.d.ts +341 -3
  119. package/lib/packlets/ai-assist/model.d.ts.map +1 -0
  120. package/lib/packlets/ai-assist/model.js +21 -3
  121. package/lib/packlets/ai-assist/model.js.map +1 -0
  122. package/lib/packlets/ai-assist/registry.d.ts +34 -1
  123. package/lib/packlets/ai-assist/registry.d.ts.map +1 -0
  124. package/lib/packlets/ai-assist/registry.js +140 -11
  125. package/lib/packlets/ai-assist/registry.js.map +1 -0
  126. package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
  127. package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -0
  128. package/lib/packlets/ai-assist/sseParser.js +127 -0
  129. package/lib/packlets/ai-assist/sseParser.js.map +1 -0
  130. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +18 -0
  131. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -0
  132. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +195 -0
  133. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  134. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +79 -0
  135. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -0
  136. package/lib/packlets/ai-assist/streamingAdapters/common.js +81 -0
  137. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  138. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +19 -0
  139. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -0
  140. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +163 -0
  141. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  142. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +18 -0
  143. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -0
  144. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +153 -0
  145. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  146. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +19 -0
  147. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -0
  148. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +167 -0
  149. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  150. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
  151. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -0
  152. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +160 -0
  153. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  154. package/lib/packlets/ai-assist/streamingClient.d.ts +33 -0
  155. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -0
  156. package/lib/packlets/ai-assist/streamingClient.js +99 -0
  157. package/lib/packlets/ai-assist/streamingClient.js.map +1 -0
  158. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -0
  159. package/lib/packlets/ai-assist/toolFormats.js.map +1 -0
  160. package/lib/packlets/conversion/converters.d.ts +8 -1
  161. package/lib/packlets/conversion/converters.d.ts.map +1 -0
  162. package/lib/packlets/conversion/converters.js +35 -2
  163. package/lib/packlets/conversion/converters.js.map +1 -0
  164. package/lib/packlets/conversion/index.d.ts.map +1 -0
  165. package/lib/packlets/conversion/index.js.map +1 -0
  166. package/lib/packlets/crypto-utils/constants.d.ts.map +1 -0
  167. package/lib/packlets/crypto-utils/constants.js.map +1 -0
  168. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -0
  169. package/lib/packlets/crypto-utils/converters.js.map +1 -0
  170. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts.map +1 -0
  171. package/lib/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  172. package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -0
  173. package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -0
  174. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
  175. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -0
  176. package/lib/packlets/crypto-utils/index.browser.js +4 -1
  177. package/lib/packlets/crypto-utils/index.browser.js.map +1 -0
  178. package/lib/packlets/crypto-utils/index.d.ts +1 -0
  179. package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
  180. package/lib/packlets/crypto-utils/index.js +4 -1
  181. package/lib/packlets/crypto-utils/index.js.map +1 -0
  182. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +50 -0
  183. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -0
  184. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +66 -0
  185. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  186. package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
  187. package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -0
  188. package/lib/packlets/crypto-utils/keystore/converters.js +100 -8
  189. package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -0
  190. package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
  191. package/lib/packlets/crypto-utils/keystore/index.d.ts.map +1 -0
  192. package/lib/packlets/crypto-utils/keystore/index.js +1 -0
  193. package/lib/packlets/crypto-utils/keystore/index.js.map +1 -0
  194. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +125 -12
  195. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -0
  196. package/lib/packlets/crypto-utils/keystore/keyStore.js +431 -118
  197. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  198. package/lib/packlets/crypto-utils/keystore/model.d.ts +248 -17
  199. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -0
  200. package/lib/packlets/crypto-utils/keystore/model.js +24 -2
  201. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -0
  202. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
  203. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts.map +1 -0
  204. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
  205. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  206. package/lib/packlets/crypto-utils/model.d.ts +145 -0
  207. package/lib/packlets/crypto-utils/model.d.ts.map +1 -0
  208. package/lib/packlets/crypto-utils/model.js +11 -1
  209. package/lib/packlets/crypto-utils/model.js.map +1 -0
  210. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +51 -1
  211. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -0
  212. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +162 -0
  213. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  214. package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -0
  215. package/lib/packlets/csv/csvFileHelpers.js.map +1 -0
  216. package/lib/packlets/csv/csvHelpers.d.ts.map +1 -0
  217. package/lib/packlets/csv/csvHelpers.js.map +1 -0
  218. package/lib/packlets/csv/index.browser.d.ts.map +1 -0
  219. package/lib/packlets/csv/index.browser.js.map +1 -0
  220. package/lib/packlets/csv/index.d.ts.map +1 -0
  221. package/lib/packlets/csv/index.js.map +1 -0
  222. package/lib/packlets/experimental/extendedArray.d.ts.map +1 -0
  223. package/lib/packlets/experimental/extendedArray.js.map +1 -0
  224. package/lib/packlets/experimental/formatter.d.ts.map +1 -0
  225. package/lib/packlets/experimental/formatter.js.map +1 -0
  226. package/lib/packlets/experimental/index.d.ts.map +1 -0
  227. package/lib/packlets/experimental/index.js.map +1 -0
  228. package/lib/packlets/experimental/rangeOf.d.ts.map +1 -0
  229. package/lib/packlets/experimental/rangeOf.js.map +1 -0
  230. package/lib/packlets/hash/index.browser.d.ts.map +1 -0
  231. package/lib/packlets/hash/index.browser.js.map +1 -0
  232. package/lib/packlets/hash/index.d.ts.map +1 -0
  233. package/lib/packlets/hash/index.js.map +1 -0
  234. package/lib/packlets/hash/index.node.d.ts.map +1 -0
  235. package/lib/packlets/hash/index.node.js.map +1 -0
  236. package/lib/packlets/hash/md5Normalizer.browser.d.ts.map +1 -0
  237. package/lib/packlets/hash/md5Normalizer.browser.js.map +1 -0
  238. package/lib/packlets/hash/md5Normalizer.d.ts.map +1 -0
  239. package/lib/packlets/hash/md5Normalizer.js.map +1 -0
  240. package/lib/packlets/mustache/index.d.ts.map +1 -0
  241. package/lib/packlets/mustache/index.js.map +1 -0
  242. package/lib/packlets/mustache/interfaces.d.ts.map +1 -0
  243. package/lib/packlets/mustache/interfaces.js.map +1 -0
  244. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -0
  245. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -0
  246. package/lib/packlets/record-jar/index.browser.d.ts.map +1 -0
  247. package/lib/packlets/record-jar/index.browser.js.map +1 -0
  248. package/lib/packlets/record-jar/index.d.ts.map +1 -0
  249. package/lib/packlets/record-jar/index.js.map +1 -0
  250. package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -0
  251. package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  252. package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -0
  253. package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -0
  254. package/lib/packlets/yaml/converters.d.ts.map +1 -0
  255. package/lib/packlets/yaml/converters.js.map +1 -0
  256. package/lib/packlets/yaml/index.d.ts +1 -0
  257. package/lib/packlets/yaml/index.d.ts.map +1 -0
  258. package/lib/packlets/yaml/index.js +1 -0
  259. package/lib/packlets/yaml/index.js.map +1 -0
  260. package/lib/packlets/yaml/serializers.d.ts +45 -0
  261. package/lib/packlets/yaml/serializers.d.ts.map +1 -0
  262. package/lib/packlets/yaml/serializers.js +84 -0
  263. package/lib/packlets/yaml/serializers.js.map +1 -0
  264. package/lib/packlets/zip-file-tree/index.d.ts.map +1 -0
  265. package/lib/packlets/zip-file-tree/index.js.map +1 -0
  266. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
  267. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -0
  268. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  269. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  270. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -0
  271. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  272. package/package.json +24 -23
@@ -63,8 +63,9 @@ function getCurrentTimestamp() {
63
63
  * @public
64
64
  */
65
65
  export class KeyStore {
66
- constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
66
+ constructor(cryptoProvider, iterations, keystoreFile, isNew, privateKeyStorage) {
67
67
  this._cryptoProvider = cryptoProvider;
68
+ this._privateKeyStorage = privateKeyStorage;
68
69
  this._iterations = iterations;
69
70
  this._keystoreFile = keystoreFile;
70
71
  this._state = 'locked';
@@ -87,7 +88,7 @@ export class KeyStore {
87
88
  if (iterations < 1) {
88
89
  return fail('Iterations must be at least 1');
89
90
  }
90
- return succeed(new KeyStore(params.cryptoProvider, iterations, undefined, true));
91
+ return succeed(new KeyStore(params.cryptoProvider, iterations, undefined, true, params.privateKeyStorage));
91
92
  }
92
93
  /**
93
94
  * Opens an existing encrypted key store.
@@ -103,7 +104,7 @@ export class KeyStore {
103
104
  return fail(`Invalid key store file: ${fileResult.message}`);
104
105
  }
105
106
  const iterations = fileResult.value.keyDerivation.iterations;
106
- return succeed(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
107
+ return succeed(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false, params.privateKeyStorage));
107
108
  }
108
109
  // ============================================================================
109
110
  // Lifecycle Methods
@@ -146,7 +147,6 @@ export class KeyStore {
146
147
  * @public
147
148
  */
148
149
  async unlock(password) {
149
- var _a;
150
150
  if (this._isNew) {
151
151
  return fail('Cannot unlock a new key store - use initialize() instead');
152
152
  }
@@ -170,57 +170,37 @@ export class KeyStore {
170
170
  if (keyResult.isFailure()) {
171
171
  return fail(`Key derivation failed: ${keyResult.message}`);
172
172
  }
173
- // Decrypt the vault
174
- const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
175
- const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
176
- const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
177
- /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
178
- if (ivResult.isFailure()) {
179
- return fail(`Invalid IV in key store file: ${ivResult.message}`);
180
- }
181
- if (authTagResult.isFailure()) {
182
- return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
183
- }
184
- if (encryptedDataResult.isFailure()) {
185
- return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
186
- }
187
- const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
188
- if (decryptResult.isFailure()) {
189
- return fail('Incorrect password or corrupted key store');
173
+ return this._decryptVault(keyResult.value);
174
+ }
175
+ /**
176
+ * Unlocks an existing key store with a pre-derived key, bypassing
177
+ * PBKDF2 key derivation. Use this when the derived key has been
178
+ * stored externally (e.g., in another key store) and the original
179
+ * password is no longer available.
180
+ *
181
+ * The supplied key must have been derived from the correct password
182
+ * using the key store file's own PBKDF2 parameters (salt and
183
+ * iteration count).
184
+ *
185
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
186
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
187
+ * @public
188
+ */
189
+ async unlockWithKey(derivedKey) {
190
+ if (this._isNew) {
191
+ return fail('Cannot unlock a new key store - use initialize() instead');
190
192
  }
191
- // Parse the vault contents
192
- const parseResult = captureResult(() => JSON.parse(decryptResult.value));
193
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
194
- if (parseResult.isFailure()) {
195
- return fail(`Failed to parse vault contents: ${parseResult.message}`);
193
+ if (this._state === 'unlocked') {
194
+ return fail('Key store is already unlocked');
196
195
  }
197
- const vaultResult = keystoreVaultContents.convert(parseResult.value);
198
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
199
- if (vaultResult.isFailure()) {
200
- return fail(`Invalid vault format: ${vaultResult.message}`);
196
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
197
+ return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
201
198
  }
202
- // Load secrets into memory
203
- this._salt = salt;
204
- this._secrets = new Map();
205
- for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
206
- const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
207
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
208
- if (keyBytesResult.isFailure()) {
209
- return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
210
- }
211
- const entry = {
212
- name,
213
- /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
214
- type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
215
- key: keyBytesResult.value,
216
- description: jsonEntry.description,
217
- createdAt: jsonEntry.createdAt
218
- };
219
- this._secrets.set(name, entry);
199
+ /* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
200
+ if (!this._keystoreFile) {
201
+ return fail('No key store file to unlock');
220
202
  }
221
- this._state = 'unlocked';
222
- this._dirty = false;
223
- return succeed(this);
203
+ return this._decryptVault(derivedKey);
224
204
  }
225
205
  /**
226
206
  * Locks the key store, clearing all secrets from memory.
@@ -238,7 +218,9 @@ export class KeyStore {
238
218
  // Clear secrets from memory (overwrite for security)
239
219
  if (this._secrets) {
240
220
  for (const entry of this._secrets.values()) {
241
- entry.key.fill(0);
221
+ if (entry.type !== 'asymmetric-keypair') {
222
+ entry.key.fill(0);
223
+ }
242
224
  }
243
225
  this._secrets.clear();
244
226
  this._secrets = undefined;
@@ -300,7 +282,9 @@ export class KeyStore {
300
282
  return succeed(Array.from(this._secrets.keys()));
301
283
  }
302
284
  /**
303
- * Gets a secret by name.
285
+ * Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
286
+ * — callers must check `entry.type` before accessing `key`/`id` since asymmetric
287
+ * entries carry no raw key material.
304
288
  * @param name - Name of the secret
305
289
  * @returns Success with secret entry, Failure if not found or locked
306
290
  * @public
@@ -315,6 +299,27 @@ export class KeyStore {
315
299
  }
316
300
  return succeed(entry);
317
301
  }
302
+ /**
303
+ * Returns the public-key JWK for an asymmetric-keypair entry.
304
+ * Available without {@link CryptoUtils.KeyStore.IPrivateKeyStorage} since the
305
+ * public key lives in the vault metadata directly.
306
+ * @param name - Name of the entry
307
+ * @returns Success with the JWK, Failure if not found, locked, or wrong type
308
+ * @public
309
+ */
310
+ getPublicKeyJwk(name) {
311
+ if (!this._secrets) {
312
+ return fail('Key store is locked');
313
+ }
314
+ const entry = this._secrets.get(name);
315
+ if (!entry) {
316
+ return fail(`Secret '${name}' not found`);
317
+ }
318
+ if (entry.type !== 'asymmetric-keypair') {
319
+ return fail(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
320
+ }
321
+ return succeed(entry.publicKeyJwk);
322
+ }
318
323
  /**
319
324
  * Checks if a secret exists.
320
325
  * @param name - Name of the secret
@@ -341,7 +346,6 @@ export class KeyStore {
341
346
  if (!name || name.length === 0) {
342
347
  return fail('Secret name cannot be empty');
343
348
  }
344
- const replaced = this._secrets.has(name);
345
349
  // Generate a new random key
346
350
  const keyResult = await this._cryptoProvider.generateKey();
347
351
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
@@ -355,19 +359,28 @@ export class KeyStore {
355
359
  description: options === null || options === void 0 ? void 0 : options.description,
356
360
  createdAt: getCurrentTimestamp()
357
361
  };
362
+ const existing = this._secrets.get(name);
363
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
358
364
  this._secrets.set(name, entry);
359
365
  this._dirty = true;
360
- return succeed({ entry, replaced });
366
+ return succeed({ entry, replaced: existing !== undefined, warning });
361
367
  }
362
368
  /**
363
- * Imports an existing secret key.
369
+ * Imports raw 32-byte key material into the vault.
370
+ *
371
+ * Always validates that the key is exactly 32 bytes (AES-256). The optional
372
+ * `type` field is a classification label stored with the entry; it does not
373
+ * change the validation rules. For importing UTF-8 API key strings (variable
374
+ * length), use {@link KeyStore.importApiKey} instead.
375
+ *
364
376
  * @param name - Unique name for the secret
365
- * @param key - The 32-byte AES-256 key
366
- * @param options - Optional description, whether to replace existing
377
+ * @param key - The 32-byte AES-256 key material
378
+ * @param options - Optional type classification, description, whether to replace existing
367
379
  * @returns Success with entry, Failure if locked, key invalid, or exists and !replace
368
380
  * @public
369
381
  */
370
- importSecret(name, key, options) {
382
+ async importSecret(name, key, options) {
383
+ var _a;
371
384
  if (!this._secrets) {
372
385
  return fail('Key store is locked');
373
386
  }
@@ -377,20 +390,21 @@ export class KeyStore {
377
390
  if (key.length !== Constants.AES_256_KEY_SIZE) {
378
391
  return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
379
392
  }
380
- const exists = this._secrets.has(name);
381
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
393
+ const existing = this._secrets.get(name);
394
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
382
395
  return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
383
396
  }
384
397
  const entry = {
385
398
  name,
386
- type: 'encryption-key',
399
+ type: (_a = options === null || options === void 0 ? void 0 : options.type) !== null && _a !== void 0 ? _a : 'encryption-key',
387
400
  key: new Uint8Array(key), // Copy to prevent external modification
388
401
  description: options === null || options === void 0 ? void 0 : options.description,
389
402
  createdAt: getCurrentTimestamp()
390
403
  };
404
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
391
405
  this._secrets.set(name, entry);
392
406
  this._dirty = true;
393
- return succeed({ entry, replaced: exists });
407
+ return succeed({ entry, replaced: existing !== undefined, warning });
394
408
  }
395
409
  /**
396
410
  * Adds a secret derived from a password using PBKDF2.
@@ -417,8 +431,8 @@ export class KeyStore {
417
431
  if (!password || password.length === 0) {
418
432
  return fail('Password cannot be empty');
419
433
  }
420
- const exists = this._secrets.has(name);
421
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
434
+ const existing = this._secrets.get(name);
435
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
422
436
  return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
423
437
  }
424
438
  const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : DEFAULT_SECRET_ITERATIONS;
@@ -441,11 +455,13 @@ export class KeyStore {
441
455
  description: options === null || options === void 0 ? void 0 : options.description,
442
456
  createdAt: getCurrentTimestamp()
443
457
  };
458
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
444
459
  this._secrets.set(name, entry);
445
460
  this._dirty = true;
446
461
  return succeed({
447
462
  entry,
448
- replaced: exists,
463
+ replaced: existing !== undefined,
464
+ warning,
449
465
  keyDerivation: {
450
466
  kdf: 'pbkdf2',
451
467
  salt: this._cryptoProvider.toBase64(saltResult.value),
@@ -454,12 +470,16 @@ export class KeyStore {
454
470
  });
455
471
  }
456
472
  /**
457
- * Removes a secret by name.
473
+ * Removes a secret by name. Vault-first: the in-memory vault entry is dropped
474
+ * before any storage cleanup runs. For asymmetric-keypair entries, best-effort
475
+ * calls {@link CryptoUtils.KeyStore.IPrivateKeyStorage}.delete on the entry's
476
+ * `id`; a failure is reported via `warning` on the result but does not roll
477
+ * back the vault removal.
458
478
  * @param name - Name of the secret to remove
459
- * @returns Success with removed entry, Failure if not found or locked
479
+ * @returns Success with removed entry (and optional warning), Failure if not found or locked
460
480
  * @public
461
481
  */
462
- removeSecret(name) {
482
+ async removeSecret(name) {
463
483
  if (!this._secrets) {
464
484
  return fail('Key store is locked');
465
485
  }
@@ -467,11 +487,12 @@ export class KeyStore {
467
487
  if (!entry) {
468
488
  return fail(`Secret '${name}' not found`);
469
489
  }
470
- // Clear the key before removing (security)
471
- entry.key.fill(0);
490
+ // Vault-first: drop the in-memory entry before touching storage so a
491
+ // storage failure cannot block removal.
472
492
  this._secrets.delete(name);
473
493
  this._dirty = true;
474
- return succeed(entry);
494
+ const warning = await this._releaseEntryResources(entry);
495
+ return succeed({ entry, warning });
475
496
  }
476
497
  /**
477
498
  * Imports an API key string into the vault.
@@ -482,7 +503,7 @@ export class KeyStore {
482
503
  * @returns Success with entry, Failure if locked, empty, or exists and !replace
483
504
  * @public
484
505
  */
485
- importApiKey(name, apiKey, options) {
506
+ async importApiKey(name, apiKey, options) {
486
507
  if (!this._secrets) {
487
508
  return fail('Key store is locked');
488
509
  }
@@ -492,8 +513,8 @@ export class KeyStore {
492
513
  if (!apiKey || apiKey.length === 0) {
493
514
  return fail('API key cannot be empty');
494
515
  }
495
- const exists = this._secrets.has(name);
496
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
516
+ const existing = this._secrets.get(name);
517
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
497
518
  return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
498
519
  }
499
520
  const encoder = new TextEncoder();
@@ -504,9 +525,10 @@ export class KeyStore {
504
525
  description: options === null || options === void 0 ? void 0 : options.description,
505
526
  createdAt: getCurrentTimestamp()
506
527
  };
528
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
507
529
  this._secrets.set(name, entry);
508
530
  this._dirty = true;
509
- return succeed({ entry, replaced: exists });
531
+ return succeed({ entry, replaced: existing !== undefined, warning });
510
532
  }
511
533
  /**
512
534
  * Retrieves an API key string by name.
@@ -529,6 +551,118 @@ export class KeyStore {
529
551
  const decoder = new TextDecoder();
530
552
  return succeed(decoder.decode(entry.key));
531
553
  }
554
+ // ============================================================================
555
+ // Asymmetric Keypair Management
556
+ // ============================================================================
557
+ /**
558
+ * Adds a new asymmetric keypair to the vault. Storage-first: the private key
559
+ * is stored under a freshly-minted `id` before the public-key vault entry is
560
+ * committed. If the storage call fails, no vault entry is written and the
561
+ * operation returns Failure.
562
+ *
563
+ * When `replace: true` displaces an existing entry (asymmetric or symmetric),
564
+ * a fresh `id` is minted; the displaced entry's resources are released
565
+ * best-effort. Failure of the storage delete is reported via `warning` on the
566
+ * result but does not roll back the replacement.
567
+ *
568
+ * Requires a {@link CryptoUtils.KeyStore.IPrivateKeyStorage} backend
569
+ * supplied at construction.
570
+ *
571
+ * @param name - Unique name for the entry
572
+ * @param options - Algorithm, optional description, replace flag
573
+ * @returns Success with the new entry, Failure if locked, no provider, or storage write failed
574
+ * @public
575
+ */
576
+ async addKeyPair(name, options) {
577
+ if (!this._secrets) {
578
+ return fail('Key store is locked');
579
+ }
580
+ if (!name || name.length === 0) {
581
+ return fail('Entry name cannot be empty');
582
+ }
583
+ if (!this._privateKeyStorage) {
584
+ return fail('No private key storage configured');
585
+ }
586
+ const existing = this._secrets.get(name);
587
+ if (existing && !options.replace) {
588
+ return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
589
+ }
590
+ // Generate the keypair before touching storage. extractable=true on backends
591
+ // that round-trip via JWK; extractable=false on backends that hold CryptoKey
592
+ // refs directly.
593
+ const extractable = !this._privateKeyStorage.supportsNonExtractable;
594
+ const keyPairResult = await this._cryptoProvider.generateKeyPair(options.algorithm, extractable);
595
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests; cannot be triggered here without mocking */
596
+ if (keyPairResult.isFailure()) {
597
+ return fail(`Failed to generate keypair for '${name}': ${keyPairResult.message}`);
598
+ }
599
+ const { publicKey, privateKey } = keyPairResult.value;
600
+ const jwkResult = await this._cryptoProvider.exportPublicKeyJwk(publicKey);
601
+ /* c8 ignore next 3 - export of an extractable freshly-generated public key is hard to fail */
602
+ if (jwkResult.isFailure()) {
603
+ return fail(`Failed to export public key for '${name}': ${jwkResult.message}`);
604
+ }
605
+ const idResult = this._generateId();
606
+ /* c8 ignore next 3 - random-bytes failure is hard to trigger with a healthy provider */
607
+ if (idResult.isFailure()) {
608
+ return fail(`Failed to mint storage id for '${name}': ${idResult.message}`);
609
+ }
610
+ const id = idResult.value;
611
+ // Storage-first: write the private key before committing the vault entry.
612
+ const storeResult = await this._privateKeyStorage.store(id, privateKey);
613
+ if (storeResult.isFailure()) {
614
+ return fail(`Failed to persist private key for '${name}': ${storeResult.message}`);
615
+ }
616
+ const entry = {
617
+ name,
618
+ type: 'asymmetric-keypair',
619
+ id,
620
+ algorithm: options.algorithm,
621
+ publicKeyJwk: jwkResult.value,
622
+ description: options.description,
623
+ createdAt: getCurrentTimestamp()
624
+ };
625
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
626
+ this._secrets.set(name, entry);
627
+ this._dirty = true;
628
+ return succeed({ entry, replaced: existing !== undefined, warning });
629
+ }
630
+ /**
631
+ * Retrieves the keypair for an asymmetric-keypair entry. The private key is
632
+ * loaded from {@link CryptoUtils.KeyStore.IPrivateKeyStorage} on every call —
633
+ * the keystore never caches private `CryptoKey` references between calls.
634
+ * The public key is re-imported from the vault's JWK so callers always
635
+ * receive a `CryptoKey` rather than the JWK form.
636
+ * @param name - Name of the entry
637
+ * @returns Success with `{ publicKey, privateKey }`, Failure if not found,
638
+ * locked, wrong type, no provider, or storage load failed.
639
+ * @public
640
+ */
641
+ async getKeyPair(name) {
642
+ if (!this._secrets) {
643
+ return fail('Key store is locked');
644
+ }
645
+ const entry = this._secrets.get(name);
646
+ if (!entry) {
647
+ return fail(`Secret '${name}' not found`);
648
+ }
649
+ if (entry.type !== 'asymmetric-keypair') {
650
+ return fail(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
651
+ }
652
+ if (!this._privateKeyStorage) {
653
+ return fail('No private key storage configured');
654
+ }
655
+ const privateResult = await this._privateKeyStorage.load(entry.id);
656
+ if (privateResult.isFailure()) {
657
+ return fail(`Failed to load private key for '${name}': ${privateResult.message}`);
658
+ }
659
+ const publicResult = await this._cryptoProvider.importPublicKeyJwk(entry.publicKeyJwk, entry.algorithm);
660
+ /* c8 ignore next 3 - vault JWKs that previously exported cleanly are extremely unlikely to fail re-import */
661
+ if (publicResult.isFailure()) {
662
+ return fail(`Failed to re-import public key for '${name}': ${publicResult.message}`);
663
+ }
664
+ return succeed({ publicKey: publicResult.value, privateKey: privateResult.value });
665
+ }
532
666
  /**
533
667
  * Lists secret names filtered by type.
534
668
  * @param type - The secret type to filter by
@@ -568,7 +702,8 @@ export class KeyStore {
568
702
  if (oldName !== newName && this._secrets.has(newName)) {
569
703
  return fail(`Secret '${newName}' already exists`);
570
704
  }
571
- // Create new entry with new name (preserve type)
705
+ // Create new entry with new name. For asymmetric entries the spread
706
+ // preserves `id` so the storage handle survives the rename.
572
707
  const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
573
708
  this._secrets.delete(oldName);
574
709
  this._secrets.set(newName, newEntry);
@@ -598,49 +733,29 @@ export class KeyStore {
598
733
  if (keyResult.isFailure()) {
599
734
  return fail(`Key derivation failed: ${keyResult.message}`);
600
735
  }
601
- // Build vault contents
602
- const secrets = {};
603
- for (const [name, entry] of this._secrets) {
604
- secrets[name] = {
605
- name: entry.name,
606
- type: entry.type,
607
- key: this._cryptoProvider.toBase64(entry.key),
608
- description: entry.description,
609
- createdAt: entry.createdAt
610
- };
611
- }
612
- const vaultContents = {
613
- version: KEYSTORE_FORMAT,
614
- secrets
615
- };
616
- // Serialize and encrypt
617
- const jsonResult = captureResult(() => JSON.stringify(vaultContents));
618
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
619
- if (jsonResult.isFailure()) {
620
- return fail(`Failed to serialize vault: ${jsonResult.message}`);
736
+ return this._encryptVault(keyResult.value);
737
+ }
738
+ /**
739
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
740
+ * derivation. Use this when the derived key has been stored externally
741
+ * (e.g., in another key store) and the original password is no longer
742
+ * available.
743
+ *
744
+ * The supplied key must be the same key that was (or would be) derived
745
+ * from the master password using the key store's PBKDF2 parameters.
746
+ *
747
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
748
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
749
+ * @public
750
+ */
751
+ async saveWithKey(derivedKey) {
752
+ if (!this._secrets || !this._salt) {
753
+ return fail('Key store is locked');
621
754
  }
622
- const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, keyResult.value);
623
- /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
624
- if (encryptResult.isFailure()) {
625
- return fail(`Encryption failed: ${encryptResult.message}`);
755
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
756
+ return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
626
757
  }
627
- const { iv, authTag, encryptedData } = encryptResult.value;
628
- const keystoreFileData = {
629
- format: KEYSTORE_FORMAT,
630
- algorithm: Constants.DEFAULT_ALGORITHM,
631
- iv: this._cryptoProvider.toBase64(iv),
632
- authTag: this._cryptoProvider.toBase64(authTag),
633
- encryptedData: this._cryptoProvider.toBase64(encryptedData),
634
- keyDerivation: {
635
- kdf: 'pbkdf2',
636
- salt: this._cryptoProvider.toBase64(this._salt),
637
- iterations: this._iterations
638
- }
639
- };
640
- this._keystoreFile = keystoreFileData;
641
- this._dirty = false;
642
- this._isNew = false;
643
- return succeed(keystoreFileData);
758
+ return this._encryptVault(derivedKey);
644
759
  }
645
760
  /**
646
761
  * Changes the master password.
@@ -708,6 +823,9 @@ export class KeyStore {
708
823
  if (secretResult.isFailure()) {
709
824
  return fail(`encryptByName: ${secretResult.message}`);
710
825
  }
826
+ if (secretResult.value.type === 'asymmetric-keypair') {
827
+ return fail(`encryptByName: secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
828
+ }
711
829
  return createEncryptedFile({
712
830
  content,
713
831
  secretName,
@@ -735,6 +853,9 @@ export class KeyStore {
735
853
  if (!entry) {
736
854
  return fail(`Secret '${secretName}' not found in key store`);
737
855
  }
856
+ if (entry.type === 'asymmetric-keypair') {
857
+ return fail(`Secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
858
+ }
738
859
  return succeed(entry.key);
739
860
  };
740
861
  return succeed(provider);
@@ -754,5 +875,197 @@ export class KeyStore {
754
875
  cryptoProvider: this._cryptoProvider
755
876
  });
756
877
  }
878
+ // ============================================================================
879
+ // Private: Vault Encryption / Decryption
880
+ // ============================================================================
881
+ /**
882
+ * Encrypts the vault with a derived key and returns the key store file.
883
+ * Shared by `save()` and `saveWithKey()`.
884
+ */
885
+ async _encryptVault(derivedKey) {
886
+ // _secrets and _salt are guaranteed non-undefined by callers
887
+ const secrets = this._secrets;
888
+ const salt = this._salt;
889
+ // Build vault contents
890
+ const secretEntries = {};
891
+ for (const [name, entry] of secrets) {
892
+ if (entry.type === 'asymmetric-keypair') {
893
+ secretEntries[name] = {
894
+ name: entry.name,
895
+ type: entry.type,
896
+ id: entry.id,
897
+ algorithm: entry.algorithm,
898
+ publicKeyJwk: entry.publicKeyJwk,
899
+ description: entry.description,
900
+ createdAt: entry.createdAt
901
+ };
902
+ }
903
+ else {
904
+ secretEntries[name] = {
905
+ name: entry.name,
906
+ type: entry.type,
907
+ key: this._cryptoProvider.toBase64(entry.key),
908
+ description: entry.description,
909
+ createdAt: entry.createdAt
910
+ };
911
+ }
912
+ }
913
+ const vaultContents = {
914
+ version: KEYSTORE_FORMAT,
915
+ secrets: secretEntries
916
+ };
917
+ // Serialize and encrypt
918
+ const jsonResult = captureResult(() => JSON.stringify(vaultContents));
919
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
920
+ if (jsonResult.isFailure()) {
921
+ return fail(`Failed to serialize vault: ${jsonResult.message}`);
922
+ }
923
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
924
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
925
+ if (encryptResult.isFailure()) {
926
+ return fail(`Encryption failed: ${encryptResult.message}`);
927
+ }
928
+ const { iv, authTag, encryptedData } = encryptResult.value;
929
+ const keystoreFileData = {
930
+ format: KEYSTORE_FORMAT,
931
+ algorithm: Constants.DEFAULT_ALGORITHM,
932
+ iv: this._cryptoProvider.toBase64(iv),
933
+ authTag: this._cryptoProvider.toBase64(authTag),
934
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
935
+ keyDerivation: {
936
+ kdf: 'pbkdf2',
937
+ salt: this._cryptoProvider.toBase64(salt),
938
+ iterations: this._iterations
939
+ }
940
+ };
941
+ this._keystoreFile = keystoreFileData;
942
+ this._dirty = false;
943
+ this._isNew = false;
944
+ return succeed(keystoreFileData);
945
+ }
946
+ /**
947
+ * Decrypts the vault with a derived key and loads secrets into memory.
948
+ * Shared by `unlock()` and `unlockWithKey()`.
949
+ */
950
+ async _decryptVault(derivedKey) {
951
+ const keystoreFile = this._keystoreFile;
952
+ if (keystoreFile === undefined) {
953
+ return fail('No key store file loaded');
954
+ }
955
+ const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
956
+ const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
957
+ const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
958
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
959
+ if (ivResult.isFailure()) {
960
+ return fail(`Invalid IV in key store file: ${ivResult.message}`);
961
+ }
962
+ if (authTagResult.isFailure()) {
963
+ return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
964
+ }
965
+ if (encryptedDataResult.isFailure()) {
966
+ return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
967
+ }
968
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
969
+ if (decryptResult.isFailure()) {
970
+ return fail('Incorrect password or corrupted key store');
971
+ }
972
+ // Parse the vault contents
973
+ const parseResult = captureResult(() => JSON.parse(decryptResult.value));
974
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
975
+ if (parseResult.isFailure()) {
976
+ return fail(`Failed to parse vault contents: ${parseResult.message}`);
977
+ }
978
+ const vaultResult = keystoreVaultContents.convert(parseResult.value);
979
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
980
+ if (vaultResult.isFailure()) {
981
+ return fail(`Invalid vault format: ${vaultResult.message}`);
982
+ }
983
+ // Build secrets into local variables to avoid partial state on failure
984
+ const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
985
+ if (saltResult.isFailure()) {
986
+ return fail(`Invalid salt in key store file: ${saltResult.message}`);
987
+ }
988
+ const secrets = new Map();
989
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
990
+ if (jsonEntry.type === 'asymmetric-keypair') {
991
+ const entry = {
992
+ name,
993
+ type: jsonEntry.type,
994
+ id: jsonEntry.id,
995
+ algorithm: jsonEntry.algorithm,
996
+ publicKeyJwk: jsonEntry.publicKeyJwk,
997
+ description: jsonEntry.description,
998
+ createdAt: jsonEntry.createdAt
999
+ };
1000
+ secrets.set(name, entry);
1001
+ }
1002
+ else {
1003
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
1004
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1005
+ if (keyBytesResult.isFailure()) {
1006
+ return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
1007
+ }
1008
+ const entry = {
1009
+ name,
1010
+ type: jsonEntry.type,
1011
+ key: keyBytesResult.value,
1012
+ description: jsonEntry.description,
1013
+ createdAt: jsonEntry.createdAt
1014
+ };
1015
+ secrets.set(name, entry);
1016
+ }
1017
+ }
1018
+ // All validation passed — commit state atomically
1019
+ this._salt = saltResult.value;
1020
+ this._secrets = secrets;
1021
+ this._state = 'unlocked';
1022
+ this._dirty = false;
1023
+ return succeed(this);
1024
+ }
1025
+ // ============================================================================
1026
+ // Private: Helpers for asymmetric flows
1027
+ // ============================================================================
1028
+ /**
1029
+ * Releases the resources held by an entry being displaced from the vault.
1030
+ * Symmetric entries get their key buffer zeroed in place. Asymmetric entries
1031
+ * have their private-key blob best-effort deleted from
1032
+ * {@link CryptoUtils.KeyStore.IPrivateKeyStorage}; if the storage call fails,
1033
+ * a warning string is returned but the displacement still proceeds — the
1034
+ * orphaned blob is left for consumer-side GC. Without a configured provider,
1035
+ * asymmetric cleanup is silently skipped.
1036
+ * @returns A warning string if storage cleanup failed, otherwise undefined.
1037
+ */
1038
+ async _releaseEntryResources(entry) {
1039
+ if (entry.type === 'asymmetric-keypair') {
1040
+ if (!this._privateKeyStorage) {
1041
+ return undefined;
1042
+ }
1043
+ const deleteResult = await this._privateKeyStorage.delete(entry.id);
1044
+ if (deleteResult.isFailure()) {
1045
+ return `Failed to delete prior storage blob for '${entry.name}' (id ${entry.id}): ${deleteResult.message}`;
1046
+ }
1047
+ return undefined;
1048
+ }
1049
+ entry.key.fill(0);
1050
+ return undefined;
1051
+ }
1052
+ /**
1053
+ * Mints a fresh UUID v4 storage handle using the crypto provider's
1054
+ * {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
1055
+ * Random-bytes failures propagate as Failure.
1056
+ */
1057
+ _generateId() {
1058
+ return this._cryptoProvider.generateRandomBytes(16).onSuccess((bytes) => {
1059
+ // Per RFC 4122 §4.4: set version (4) and variant (10xx) bits.
1060
+ // eslint-disable-next-line no-bitwise
1061
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1062
+ // eslint-disable-next-line no-bitwise
1063
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1064
+ const hex = Array.from(bytes)
1065
+ .map((b) => b.toString(16).padStart(2, '0'))
1066
+ .join('');
1067
+ return succeed(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`);
1068
+ });
1069
+ }
757
1070
  }
758
1071
  //# sourceMappingURL=keyStore.js.map