@fgv/ts-extras 5.1.0-3 → 5.1.0-31

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 (334) hide show
  1. package/dist/index.browser.js +4 -2
  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 +958 -131
  5. package/dist/packlets/ai-assist/apiClient.js.map +1 -0
  6. package/dist/packlets/ai-assist/chatRequestBuilders.js +186 -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/imageOptionsResolver.js +212 -0
  13. package/dist/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  14. package/dist/packlets/ai-assist/index.js +7 -3
  15. package/dist/packlets/ai-assist/index.js.map +1 -0
  16. package/dist/packlets/ai-assist/jsonCompletion.js +95 -0
  17. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -0
  18. package/dist/packlets/ai-assist/jsonResponse.js +149 -0
  19. package/dist/packlets/ai-assist/jsonResponse.js.map +1 -0
  20. package/dist/packlets/ai-assist/model.js +21 -4
  21. package/dist/packlets/ai-assist/model.js.map +1 -0
  22. package/dist/packlets/ai-assist/registry.js +235 -10
  23. package/dist/packlets/ai-assist/registry.js.map +1 -0
  24. package/dist/packlets/ai-assist/sseParser.js +123 -0
  25. package/dist/packlets/ai-assist/sseParser.js.map +1 -0
  26. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +197 -0
  27. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  28. package/dist/packlets/ai-assist/streamingAdapters/common.js +79 -0
  29. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  30. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +172 -0
  31. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  32. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +165 -0
  33. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  34. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +179 -0
  35. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  36. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +163 -0
  37. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  38. package/dist/packlets/ai-assist/streamingClient.js +116 -0
  39. package/dist/packlets/ai-assist/streamingClient.js.map +1 -0
  40. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +265 -0
  41. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  42. package/dist/packlets/ai-assist/toolFormats.js.map +1 -0
  43. package/dist/packlets/conversion/converters.js +35 -1
  44. package/dist/packlets/conversion/converters.js.map +1 -0
  45. package/dist/packlets/conversion/index.js.map +1 -0
  46. package/dist/packlets/crypto-utils/constants.js.map +1 -0
  47. package/dist/packlets/crypto-utils/converters.js +24 -4
  48. package/dist/packlets/crypto-utils/converters.js.map +1 -0
  49. package/dist/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  50. package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -0
  51. package/dist/packlets/crypto-utils/hpkeProvider.js +333 -0
  52. package/dist/packlets/crypto-utils/hpkeProvider.js.map +1 -0
  53. package/dist/packlets/crypto-utils/index.browser.js +7 -0
  54. package/dist/packlets/crypto-utils/index.browser.js.map +1 -0
  55. package/dist/packlets/crypto-utils/index.js +6 -0
  56. package/dist/packlets/crypto-utils/index.js.map +1 -0
  57. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +71 -0
  58. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  59. package/dist/packlets/crypto-utils/keystore/converters.js +103 -11
  60. package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -0
  61. package/dist/packlets/crypto-utils/keystore/index.js +1 -0
  62. package/dist/packlets/crypto-utils/keystore/index.js.map +1 -0
  63. package/dist/packlets/crypto-utils/keystore/keyStore.js +618 -118
  64. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  65. package/dist/packlets/crypto-utils/keystore/model.js +22 -1
  66. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -0
  67. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
  68. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  69. package/dist/packlets/crypto-utils/model.js +32 -0
  70. package/dist/packlets/crypto-utils/model.js.map +1 -0
  71. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +270 -1
  72. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  73. package/dist/packlets/crypto-utils/spkiHelpers.js +130 -0
  74. package/dist/packlets/crypto-utils/spkiHelpers.js.map +1 -0
  75. package/dist/packlets/csv/csvFileHelpers.js +0 -14
  76. package/dist/packlets/csv/csvFileHelpers.js.map +1 -0
  77. package/dist/packlets/csv/csvHelpers.js +14 -0
  78. package/dist/packlets/csv/csvHelpers.js.map +1 -0
  79. package/dist/packlets/csv/index.browser.js +1 -3
  80. package/dist/packlets/csv/index.browser.js.map +1 -0
  81. package/dist/packlets/csv/index.js.map +1 -0
  82. package/dist/packlets/experimental/extendedArray.js.map +1 -0
  83. package/dist/packlets/experimental/formatter.js.map +1 -0
  84. package/dist/packlets/experimental/index.js.map +1 -0
  85. package/dist/packlets/experimental/rangeOf.js.map +1 -0
  86. package/dist/packlets/hash/index.browser.js.map +1 -0
  87. package/dist/packlets/hash/index.js.map +1 -0
  88. package/dist/packlets/hash/index.node.js.map +1 -0
  89. package/dist/packlets/hash/md5Normalizer.browser.js.map +1 -0
  90. package/dist/packlets/hash/md5Normalizer.js.map +1 -0
  91. package/dist/packlets/mustache/index.js.map +1 -0
  92. package/dist/packlets/mustache/interfaces.js.map +1 -0
  93. package/dist/packlets/mustache/mustacheTemplate.js +42 -4
  94. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -0
  95. package/dist/packlets/record-jar/index.browser.js +1 -3
  96. package/dist/packlets/record-jar/index.browser.js.map +1 -0
  97. package/dist/packlets/record-jar/index.js.map +1 -0
  98. package/dist/packlets/record-jar/recordJarFileHelpers.js +0 -18
  99. package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  100. package/dist/packlets/record-jar/recordJarHelpers.js +18 -0
  101. package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -0
  102. package/dist/packlets/yaml/converters.js.map +1 -0
  103. package/dist/packlets/yaml/index.js +1 -0
  104. package/dist/packlets/yaml/index.js.map +1 -0
  105. package/dist/packlets/yaml/serializers.js +48 -0
  106. package/dist/packlets/yaml/serializers.js.map +1 -0
  107. package/dist/packlets/zip-file-tree/index.js.map +1 -0
  108. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  109. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  110. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  111. package/dist/ts-extras.d.ts +2869 -154
  112. package/dist/tsdoc-metadata.json +1 -1
  113. package/lib/index.browser.d.ts +4 -2
  114. package/lib/index.browser.d.ts.map +1 -0
  115. package/lib/index.browser.js +8 -3
  116. package/lib/index.browser.js.map +1 -0
  117. package/lib/index.d.ts.map +1 -0
  118. package/lib/index.js.map +1 -0
  119. package/lib/packlets/ai-assist/apiClient.d.ts +99 -16
  120. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -0
  121. package/lib/packlets/ai-assist/apiClient.js +961 -130
  122. package/lib/packlets/ai-assist/apiClient.js.map +1 -0
  123. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +89 -0
  124. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -0
  125. package/lib/packlets/ai-assist/chatRequestBuilders.js +195 -0
  126. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  127. package/lib/packlets/ai-assist/converters.d.ts.map +1 -0
  128. package/lib/packlets/ai-assist/converters.js +2 -1
  129. package/lib/packlets/ai-assist/converters.js.map +1 -0
  130. package/lib/packlets/ai-assist/endpoint.d.ts +28 -0
  131. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
  132. package/lib/packlets/ai-assist/endpoint.js +82 -0
  133. package/lib/packlets/ai-assist/endpoint.js.map +1 -0
  134. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +74 -0
  135. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -0
  136. package/lib/packlets/ai-assist/imageOptionsResolver.js +216 -0
  137. package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  138. package/lib/packlets/ai-assist/index.d.ts +7 -3
  139. package/lib/packlets/ai-assist/index.d.ts.map +1 -0
  140. package/lib/packlets/ai-assist/index.js +21 -1
  141. package/lib/packlets/ai-assist/index.js.map +1 -0
  142. package/lib/packlets/ai-assist/jsonCompletion.d.ts +93 -0
  143. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -0
  144. package/lib/packlets/ai-assist/jsonCompletion.js +99 -0
  145. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -0
  146. package/lib/packlets/ai-assist/jsonResponse.d.ts +91 -0
  147. package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -0
  148. package/lib/packlets/ai-assist/jsonResponse.js +154 -0
  149. package/lib/packlets/ai-assist/jsonResponse.js.map +1 -0
  150. package/lib/packlets/ai-assist/model.d.ts +720 -7
  151. package/lib/packlets/ai-assist/model.d.ts.map +1 -0
  152. package/lib/packlets/ai-assist/model.js +22 -4
  153. package/lib/packlets/ai-assist/model.js.map +1 -0
  154. package/lib/packlets/ai-assist/registry.d.ts +34 -1
  155. package/lib/packlets/ai-assist/registry.d.ts.map +1 -0
  156. package/lib/packlets/ai-assist/registry.js +238 -11
  157. package/lib/packlets/ai-assist/registry.js.map +1 -0
  158. package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
  159. package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -0
  160. package/lib/packlets/ai-assist/sseParser.js +128 -0
  161. package/lib/packlets/ai-assist/sseParser.js.map +1 -0
  162. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +19 -0
  163. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -0
  164. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +200 -0
  165. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  166. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +83 -0
  167. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -0
  168. package/lib/packlets/ai-assist/streamingAdapters/common.js +83 -0
  169. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  170. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +20 -0
  171. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -0
  172. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +175 -0
  173. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  174. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +19 -0
  175. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -0
  176. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +168 -0
  177. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  178. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +20 -0
  179. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -0
  180. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +182 -0
  181. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  182. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
  183. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -0
  184. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +166 -0
  185. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  186. package/lib/packlets/ai-assist/streamingClient.d.ts +33 -0
  187. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -0
  188. package/lib/packlets/ai-assist/streamingClient.js +121 -0
  189. package/lib/packlets/ai-assist/streamingClient.js.map +1 -0
  190. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +71 -0
  191. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -0
  192. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +270 -0
  193. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  194. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -0
  195. package/lib/packlets/ai-assist/toolFormats.js.map +1 -0
  196. package/lib/packlets/conversion/converters.d.ts +8 -1
  197. package/lib/packlets/conversion/converters.d.ts.map +1 -0
  198. package/lib/packlets/conversion/converters.js +36 -2
  199. package/lib/packlets/conversion/converters.js.map +1 -0
  200. package/lib/packlets/conversion/index.d.ts.map +1 -0
  201. package/lib/packlets/conversion/index.js.map +1 -0
  202. package/lib/packlets/crypto-utils/constants.d.ts.map +1 -0
  203. package/lib/packlets/crypto-utils/constants.js.map +1 -0
  204. package/lib/packlets/crypto-utils/converters.d.ts +12 -1
  205. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -0
  206. package/lib/packlets/crypto-utils/converters.js +25 -5
  207. package/lib/packlets/crypto-utils/converters.js.map +1 -0
  208. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts.map +1 -0
  209. package/lib/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  210. package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -0
  211. package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -0
  212. package/lib/packlets/crypto-utils/hpkeProvider.d.ts +142 -0
  213. package/lib/packlets/crypto-utils/hpkeProvider.d.ts.map +1 -0
  214. package/lib/packlets/crypto-utils/hpkeProvider.js +337 -0
  215. package/lib/packlets/crypto-utils/hpkeProvider.js.map +1 -0
  216. package/lib/packlets/crypto-utils/index.browser.d.ts +3 -0
  217. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -0
  218. package/lib/packlets/crypto-utils/index.browser.js +14 -1
  219. package/lib/packlets/crypto-utils/index.browser.js.map +1 -0
  220. package/lib/packlets/crypto-utils/index.d.ts +3 -0
  221. package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
  222. package/lib/packlets/crypto-utils/index.js +13 -1
  223. package/lib/packlets/crypto-utils/index.js.map +1 -0
  224. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +54 -0
  225. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -0
  226. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +74 -0
  227. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  228. package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
  229. package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -0
  230. package/lib/packlets/crypto-utils/keystore/converters.js +101 -9
  231. package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -0
  232. package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
  233. package/lib/packlets/crypto-utils/keystore/index.d.ts.map +1 -0
  234. package/lib/packlets/crypto-utils/keystore/index.js +1 -0
  235. package/lib/packlets/crypto-utils/keystore/index.js.map +1 -0
  236. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +198 -13
  237. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -0
  238. package/lib/packlets/crypto-utils/keystore/keyStore.js +624 -124
  239. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  240. package/lib/packlets/crypto-utils/keystore/model.d.ts +268 -19
  241. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -0
  242. package/lib/packlets/crypto-utils/keystore/model.js +24 -2
  243. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -0
  244. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
  245. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts.map +1 -0
  246. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
  247. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  248. package/lib/packlets/crypto-utils/model.d.ts +338 -10
  249. package/lib/packlets/crypto-utils/model.d.ts.map +1 -0
  250. package/lib/packlets/crypto-utils/model.js +33 -1
  251. package/lib/packlets/crypto-utils/model.js.map +1 -0
  252. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +110 -2
  253. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -0
  254. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +269 -0
  255. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  256. package/lib/packlets/crypto-utils/spkiHelpers.d.ts +53 -0
  257. package/lib/packlets/crypto-utils/spkiHelpers.d.ts.map +1 -0
  258. package/lib/packlets/crypto-utils/spkiHelpers.js +136 -0
  259. package/lib/packlets/crypto-utils/spkiHelpers.js.map +1 -0
  260. package/lib/packlets/csv/csvFileHelpers.d.ts +0 -10
  261. package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -0
  262. package/lib/packlets/csv/csvFileHelpers.js +0 -15
  263. package/lib/packlets/csv/csvFileHelpers.js.map +1 -0
  264. package/lib/packlets/csv/csvHelpers.d.ts +10 -0
  265. package/lib/packlets/csv/csvHelpers.d.ts.map +1 -0
  266. package/lib/packlets/csv/csvHelpers.js +15 -0
  267. package/lib/packlets/csv/csvHelpers.js.map +1 -0
  268. package/lib/packlets/csv/index.browser.d.ts +0 -1
  269. package/lib/packlets/csv/index.browser.d.ts.map +1 -0
  270. package/lib/packlets/csv/index.browser.js +1 -5
  271. package/lib/packlets/csv/index.browser.js.map +1 -0
  272. package/lib/packlets/csv/index.d.ts.map +1 -0
  273. package/lib/packlets/csv/index.js.map +1 -0
  274. package/lib/packlets/experimental/extendedArray.d.ts.map +1 -0
  275. package/lib/packlets/experimental/extendedArray.js.map +1 -0
  276. package/lib/packlets/experimental/formatter.d.ts.map +1 -0
  277. package/lib/packlets/experimental/formatter.js.map +1 -0
  278. package/lib/packlets/experimental/index.d.ts.map +1 -0
  279. package/lib/packlets/experimental/index.js.map +1 -0
  280. package/lib/packlets/experimental/rangeOf.d.ts.map +1 -0
  281. package/lib/packlets/experimental/rangeOf.js.map +1 -0
  282. package/lib/packlets/hash/index.browser.d.ts.map +1 -0
  283. package/lib/packlets/hash/index.browser.js.map +1 -0
  284. package/lib/packlets/hash/index.d.ts.map +1 -0
  285. package/lib/packlets/hash/index.js.map +1 -0
  286. package/lib/packlets/hash/index.node.d.ts.map +1 -0
  287. package/lib/packlets/hash/index.node.js.map +1 -0
  288. package/lib/packlets/hash/md5Normalizer.browser.d.ts.map +1 -0
  289. package/lib/packlets/hash/md5Normalizer.browser.js.map +1 -0
  290. package/lib/packlets/hash/md5Normalizer.d.ts.map +1 -0
  291. package/lib/packlets/hash/md5Normalizer.js.map +1 -0
  292. package/lib/packlets/mustache/index.d.ts +1 -1
  293. package/lib/packlets/mustache/index.d.ts.map +1 -0
  294. package/lib/packlets/mustache/index.js.map +1 -0
  295. package/lib/packlets/mustache/interfaces.d.ts +34 -0
  296. package/lib/packlets/mustache/interfaces.d.ts.map +1 -0
  297. package/lib/packlets/mustache/interfaces.js.map +1 -0
  298. package/lib/packlets/mustache/mustacheTemplate.d.ts +2 -0
  299. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -0
  300. package/lib/packlets/mustache/mustacheTemplate.js +42 -4
  301. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -0
  302. package/lib/packlets/record-jar/index.browser.d.ts +0 -1
  303. package/lib/packlets/record-jar/index.browser.d.ts.map +1 -0
  304. package/lib/packlets/record-jar/index.browser.js +1 -5
  305. package/lib/packlets/record-jar/index.browser.js.map +1 -0
  306. package/lib/packlets/record-jar/index.d.ts.map +1 -0
  307. package/lib/packlets/record-jar/index.js.map +1 -0
  308. package/lib/packlets/record-jar/recordJarFileHelpers.d.ts +0 -11
  309. package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -0
  310. package/lib/packlets/record-jar/recordJarFileHelpers.js +0 -19
  311. package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  312. package/lib/packlets/record-jar/recordJarHelpers.d.ts +11 -0
  313. package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -0
  314. package/lib/packlets/record-jar/recordJarHelpers.js +19 -0
  315. package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -0
  316. package/lib/packlets/yaml/converters.d.ts.map +1 -0
  317. package/lib/packlets/yaml/converters.js.map +1 -0
  318. package/lib/packlets/yaml/index.d.ts +1 -0
  319. package/lib/packlets/yaml/index.d.ts.map +1 -0
  320. package/lib/packlets/yaml/index.js +1 -0
  321. package/lib/packlets/yaml/index.js.map +1 -0
  322. package/lib/packlets/yaml/serializers.d.ts +45 -0
  323. package/lib/packlets/yaml/serializers.d.ts.map +1 -0
  324. package/lib/packlets/yaml/serializers.js +84 -0
  325. package/lib/packlets/yaml/serializers.js.map +1 -0
  326. package/lib/packlets/zip-file-tree/index.d.ts.map +1 -0
  327. package/lib/packlets/zip-file-tree/index.js.map +1 -0
  328. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
  329. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -0
  330. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  331. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  332. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -0
  333. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  334. package/package.json +16 -15
@@ -56,7 +56,8 @@ exports.KeyStore = void 0;
56
56
  const ts_utils_1 = require("@fgv/ts-utils");
57
57
  const Constants = __importStar(require("../constants"));
58
58
  const encryptedFile_1 = require("../encryptedFile");
59
- const model_1 = require("./model");
59
+ const model_1 = require("../model");
60
+ const model_2 = require("./model");
60
61
  const converters_1 = require("./converters");
61
62
  /**
62
63
  * Gets the current ISO timestamp.
@@ -99,8 +100,9 @@ function getCurrentTimestamp() {
99
100
  * @public
100
101
  */
101
102
  class KeyStore {
102
- constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
103
+ constructor(cryptoProvider, iterations, keystoreFile, isNew, privateKeyStorage) {
103
104
  this._cryptoProvider = cryptoProvider;
105
+ this._privateKeyStorage = privateKeyStorage;
104
106
  this._iterations = iterations;
105
107
  this._keystoreFile = keystoreFile;
106
108
  this._state = 'locked';
@@ -119,11 +121,11 @@ class KeyStore {
119
121
  */
120
122
  static create(params) {
121
123
  var _a;
122
- const iterations = (_a = params.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_KEYSTORE_ITERATIONS;
124
+ const iterations = (_a = params.iterations) !== null && _a !== void 0 ? _a : model_2.DEFAULT_KEYSTORE_ITERATIONS;
123
125
  if (iterations < 1) {
124
126
  return (0, ts_utils_1.fail)('Iterations must be at least 1');
125
127
  }
126
- return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true));
128
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true, params.privateKeyStorage));
127
129
  }
128
130
  /**
129
131
  * Opens an existing encrypted key store.
@@ -139,7 +141,7 @@ class KeyStore {
139
141
  return (0, ts_utils_1.fail)(`Invalid key store file: ${fileResult.message}`);
140
142
  }
141
143
  const iterations = fileResult.value.keyDerivation.iterations;
142
- return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
144
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false, params.privateKeyStorage));
143
145
  }
144
146
  // ============================================================================
145
147
  // Lifecycle Methods
@@ -163,7 +165,7 @@ class KeyStore {
163
165
  return (0, ts_utils_1.fail)('Password cannot be empty');
164
166
  }
165
167
  // Generate salt for this key store using crypto provider
166
- const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
168
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
167
169
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
168
170
  if (saltResult.isFailure()) {
169
171
  return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
@@ -182,7 +184,6 @@ class KeyStore {
182
184
  * @public
183
185
  */
184
186
  async unlock(password) {
185
- var _a;
186
187
  if (this._isNew) {
187
188
  return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
188
189
  }
@@ -206,57 +207,37 @@ class KeyStore {
206
207
  if (keyResult.isFailure()) {
207
208
  return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
208
209
  }
209
- // Decrypt the vault
210
- const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
211
- const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
212
- const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
213
- /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
214
- if (ivResult.isFailure()) {
215
- return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
216
- }
217
- if (authTagResult.isFailure()) {
218
- return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
219
- }
220
- if (encryptedDataResult.isFailure()) {
221
- return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
222
- }
223
- const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
224
- if (decryptResult.isFailure()) {
225
- return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
210
+ return this._decryptVault(keyResult.value);
211
+ }
212
+ /**
213
+ * Unlocks an existing key store with a pre-derived key, bypassing
214
+ * PBKDF2 key derivation. Use this when the derived key has been
215
+ * stored externally (e.g., in another key store) and the original
216
+ * password is no longer available.
217
+ *
218
+ * The supplied key must have been derived from the correct password
219
+ * using the key store file's own PBKDF2 parameters (salt and
220
+ * iteration count).
221
+ *
222
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
223
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
224
+ * @public
225
+ */
226
+ async unlockWithKey(derivedKey) {
227
+ if (this._isNew) {
228
+ return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
226
229
  }
227
- // Parse the vault contents
228
- const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
229
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
230
- if (parseResult.isFailure()) {
231
- return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
230
+ if (this._state === 'unlocked') {
231
+ return (0, ts_utils_1.fail)('Key store is already unlocked');
232
232
  }
233
- const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
234
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
235
- if (vaultResult.isFailure()) {
236
- return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
233
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
234
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
237
235
  }
238
- // Load secrets into memory
239
- this._salt = salt;
240
- this._secrets = new Map();
241
- for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
242
- const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
243
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
244
- if (keyBytesResult.isFailure()) {
245
- return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
246
- }
247
- const entry = {
248
- name,
249
- /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
250
- type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
251
- key: keyBytesResult.value,
252
- description: jsonEntry.description,
253
- createdAt: jsonEntry.createdAt
254
- };
255
- this._secrets.set(name, entry);
236
+ /* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
237
+ if (!this._keystoreFile) {
238
+ return (0, ts_utils_1.fail)('No key store file to unlock');
256
239
  }
257
- this._state = 'unlocked';
258
- this._dirty = false;
259
- return (0, ts_utils_1.succeed)(this);
240
+ return this._decryptVault(derivedKey);
260
241
  }
261
242
  /**
262
243
  * Locks the key store, clearing all secrets from memory.
@@ -274,7 +255,9 @@ class KeyStore {
274
255
  // Clear secrets from memory (overwrite for security)
275
256
  if (this._secrets) {
276
257
  for (const entry of this._secrets.values()) {
277
- entry.key.fill(0);
258
+ if (entry.type !== 'asymmetric-keypair') {
259
+ entry.key.fill(0);
260
+ }
278
261
  }
279
262
  this._secrets.clear();
280
263
  this._secrets = undefined;
@@ -336,7 +319,9 @@ class KeyStore {
336
319
  return (0, ts_utils_1.succeed)(Array.from(this._secrets.keys()));
337
320
  }
338
321
  /**
339
- * Gets a secret by name.
322
+ * Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
323
+ * — callers must check `entry.type` before accessing `key`/`id` since asymmetric
324
+ * entries carry no raw key material.
340
325
  * @param name - Name of the secret
341
326
  * @returns Success with secret entry, Failure if not found or locked
342
327
  * @public
@@ -351,6 +336,27 @@ class KeyStore {
351
336
  }
352
337
  return (0, ts_utils_1.succeed)(entry);
353
338
  }
339
+ /**
340
+ * Returns the public-key JWK for an asymmetric-keypair entry.
341
+ * Available without {@link CryptoUtils.KeyStore.IPrivateKeyStorage} since the
342
+ * public key lives in the vault metadata directly.
343
+ * @param name - Name of the entry
344
+ * @returns Success with the JWK, Failure if not found, locked, or wrong type
345
+ * @public
346
+ */
347
+ getPublicKeyJwk(name) {
348
+ if (!this._secrets) {
349
+ return (0, ts_utils_1.fail)('Key store is locked');
350
+ }
351
+ const entry = this._secrets.get(name);
352
+ if (!entry) {
353
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
354
+ }
355
+ if (entry.type !== 'asymmetric-keypair') {
356
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
357
+ }
358
+ return (0, ts_utils_1.succeed)(entry.publicKeyJwk);
359
+ }
354
360
  /**
355
361
  * Checks if a secret exists.
356
362
  * @param name - Name of the secret
@@ -377,7 +383,6 @@ class KeyStore {
377
383
  if (!name || name.length === 0) {
378
384
  return (0, ts_utils_1.fail)('Secret name cannot be empty');
379
385
  }
380
- const replaced = this._secrets.has(name);
381
386
  // Generate a new random key
382
387
  const keyResult = await this._cryptoProvider.generateKey();
383
388
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
@@ -391,19 +396,28 @@ class KeyStore {
391
396
  description: options === null || options === void 0 ? void 0 : options.description,
392
397
  createdAt: getCurrentTimestamp()
393
398
  };
399
+ const existing = this._secrets.get(name);
400
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
394
401
  this._secrets.set(name, entry);
395
402
  this._dirty = true;
396
- return (0, ts_utils_1.succeed)({ entry, replaced });
403
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
397
404
  }
398
405
  /**
399
- * Imports an existing secret key.
406
+ * Imports raw 32-byte key material into the vault.
407
+ *
408
+ * Always validates that the key is exactly 32 bytes (AES-256). The optional
409
+ * `type` field is a classification label stored with the entry; it does not
410
+ * change the validation rules. For importing UTF-8 API key strings (variable
411
+ * length), use {@link KeyStore.importApiKey} instead.
412
+ *
400
413
  * @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
414
+ * @param key - The 32-byte AES-256 key material
415
+ * @param options - Optional type classification, description, whether to replace existing
403
416
  * @returns Success with entry, Failure if locked, key invalid, or exists and !replace
404
417
  * @public
405
418
  */
406
- importSecret(name, key, options) {
419
+ async importSecret(name, key, options) {
420
+ var _a;
407
421
  if (!this._secrets) {
408
422
  return (0, ts_utils_1.fail)('Key store is locked');
409
423
  }
@@ -413,20 +427,21 @@ class KeyStore {
413
427
  if (key.length !== Constants.AES_256_KEY_SIZE) {
414
428
  return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
415
429
  }
416
- const exists = this._secrets.has(name);
417
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
430
+ const existing = this._secrets.get(name);
431
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
418
432
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
419
433
  }
420
434
  const entry = {
421
435
  name,
422
- type: 'encryption-key',
436
+ type: (_a = options === null || options === void 0 ? void 0 : options.type) !== null && _a !== void 0 ? _a : 'encryption-key',
423
437
  key: new Uint8Array(key), // Copy to prevent external modification
424
438
  description: options === null || options === void 0 ? void 0 : options.description,
425
439
  createdAt: getCurrentTimestamp()
426
440
  };
441
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
427
442
  this._secrets.set(name, entry);
428
443
  this._dirty = true;
429
- return (0, ts_utils_1.succeed)({ entry, replaced: exists });
444
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
430
445
  }
431
446
  /**
432
447
  * Adds a secret derived from a password using PBKDF2.
@@ -453,13 +468,13 @@ class KeyStore {
453
468
  if (!password || password.length === 0) {
454
469
  return (0, ts_utils_1.fail)('Password cannot be empty');
455
470
  }
456
- const exists = this._secrets.has(name);
457
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
471
+ const existing = this._secrets.get(name);
472
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
458
473
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
459
474
  }
460
- const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_SECRET_ITERATIONS;
475
+ const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_2.DEFAULT_SECRET_ITERATIONS;
461
476
  // Generate a random salt for this secret's key derivation
462
- const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
477
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
463
478
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
464
479
  if (saltResult.isFailure()) {
465
480
  return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
@@ -477,11 +492,13 @@ class KeyStore {
477
492
  description: options === null || options === void 0 ? void 0 : options.description,
478
493
  createdAt: getCurrentTimestamp()
479
494
  };
495
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
480
496
  this._secrets.set(name, entry);
481
497
  this._dirty = true;
482
498
  return (0, ts_utils_1.succeed)({
483
499
  entry,
484
- replaced: exists,
500
+ replaced: existing !== undefined,
501
+ warning,
485
502
  keyDerivation: {
486
503
  kdf: 'pbkdf2',
487
504
  salt: this._cryptoProvider.toBase64(saltResult.value),
@@ -490,12 +507,183 @@ class KeyStore {
490
507
  });
491
508
  }
492
509
  /**
493
- * Removes a secret by name.
510
+ * Verifies that a candidate password derives the same key material currently
511
+ * stored under `name`, using the supplied
512
+ * {@link CryptoUtils.IKeyDerivationParams | key derivation parameters}.
513
+ *
514
+ * The keystore does not persist per-slot key derivation parameters with the
515
+ * entry — callers receive them from `addSecretFromPassword` and store them
516
+ * alongside the encrypted artifact (or wherever else makes sense). Pass
517
+ * those same parameters here for verification.
518
+ *
519
+ * Re-derives a key from `password` + `keyDerivation`, then compares it to
520
+ * the stored key material in constant time. Restricted to entries of type
521
+ * `'encryption-key'` — the type produced by `addSecretFromPassword`. Other
522
+ * symmetric types (`'api-key'`) and asymmetric entries are rejected so
523
+ * the boolean result reflects "this slot accepts this password" rather
524
+ * than an incidental byte-equality match against unrelated material.
525
+ *
526
+ * Note: the keystore does not currently flag whether an `'encryption-key'`
527
+ * entry was actually password-derived (vs. random via `addSecret` or raw
528
+ * via `importSecret`). A `true` result therefore means "the candidate
529
+ * password produces the same 32 bytes currently stored", which is what
530
+ * the equivalent consumer-side helper (`verifyGatePassword`) already
531
+ * implies for entries it manages.
532
+ *
533
+ * @param name - Name of the secret to verify against
534
+ * @param password - Candidate password to test
535
+ * @param keyDerivation - The key derivation parameters returned by
536
+ * `addSecretFromPassword` when the secret was created. Only
537
+ * `kdf: 'pbkdf2'` is supported.
538
+ * @returns Success(true) when the candidate matches the stored key,
539
+ * Success(false) when it does not, Failure if locked, secret missing,
540
+ * wrong type, unsupported `kdf`, or key derivation fails
541
+ * @public
542
+ */
543
+ async verifySecretFromPassword(name, password, keyDerivation) {
544
+ if (!this._secrets) {
545
+ return (0, ts_utils_1.fail)('Key store is locked');
546
+ }
547
+ if (!password || password.length === 0) {
548
+ return (0, ts_utils_1.fail)('Password cannot be empty');
549
+ }
550
+ if (keyDerivation.kdf !== 'pbkdf2') {
551
+ return (0, ts_utils_1.fail)(`Unsupported kdf '${keyDerivation.kdf}' (expected 'pbkdf2')`);
552
+ }
553
+ const entry = this._secrets.get(name);
554
+ if (!entry) {
555
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
556
+ }
557
+ if (entry.type !== 'encryption-key') {
558
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
559
+ }
560
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
561
+ if (saltResult.isFailure()) {
562
+ return (0, ts_utils_1.fail)(`Invalid salt: ${saltResult.message}`);
563
+ }
564
+ const derivedResult = await this._cryptoProvider.deriveKey(password, saltResult.value, keyDerivation.iterations);
565
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests */
566
+ if (derivedResult.isFailure()) {
567
+ return (0, ts_utils_1.fail)(`Key derivation failed: ${derivedResult.message}`);
568
+ }
569
+ return (0, ts_utils_1.succeed)(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
570
+ }
571
+ /**
572
+ * Adds a secret derived from a password using Argon2id (RFC 9106).
573
+ *
574
+ * The Argon2id provider must be supplied explicitly; the KeyStore does not
575
+ * hold one by default (consumers opt in by depending on the argon2 package).
576
+ *
577
+ * Returns the key derivation parameters so callers can store them alongside
578
+ * encrypted artifacts, enabling future re-derivation and verification.
579
+ *
580
+ * @param name - Unique name for the secret
581
+ * @param password - Password or passphrase
582
+ * @param argon2idProvider - Argon2id provider (Node or Browser implementation)
583
+ * @param options - Optional: Argon2id params (defaults to ARGON2ID_OWASP_MIN), description, replace flag
584
+ * @returns Success with entry and keyDerivation params, Failure if locked or invalid
585
+ * @public
586
+ */
587
+ async addSecretFromPasswordArgon2id(name, password, argon2idProvider, options) {
588
+ var _a;
589
+ if (!this._secrets) {
590
+ return (0, ts_utils_1.fail)('Key store is locked');
591
+ }
592
+ if (!name || name.length === 0) {
593
+ return (0, ts_utils_1.fail)('Secret name cannot be empty');
594
+ }
595
+ if (!password || password.length === 0) {
596
+ return (0, ts_utils_1.fail)('Password cannot be empty');
597
+ }
598
+ const existing = this._secrets.get(name);
599
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
600
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
601
+ }
602
+ const params = (_a = options === null || options === void 0 ? void 0 : options.params) !== null && _a !== void 0 ? _a : model_1.ARGON2ID_OWASP_MIN;
603
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
604
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
605
+ if (saltResult.isFailure()) {
606
+ return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
607
+ }
608
+ const keyResult = await argon2idProvider.argon2id(password, saltResult.value, params);
609
+ if (keyResult.isFailure()) {
610
+ return (0, ts_utils_1.fail)(`Argon2id key derivation failed: ${keyResult.message}`);
611
+ }
612
+ if (keyResult.value.length !== Constants.AES_256_KEY_SIZE) {
613
+ return (0, ts_utils_1.fail)(`Argon2id outputBytes must be ${Constants.AES_256_KEY_SIZE} for KeyStore secrets, got ${keyResult.value.length}`);
614
+ }
615
+ const entry = {
616
+ name,
617
+ type: 'encryption-key',
618
+ key: keyResult.value,
619
+ description: options === null || options === void 0 ? void 0 : options.description,
620
+ createdAt: getCurrentTimestamp()
621
+ };
622
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
623
+ this._secrets.set(name, entry);
624
+ this._dirty = true;
625
+ const keyDerivation = {
626
+ kdf: 'argon2id',
627
+ salt: this._cryptoProvider.toBase64(saltResult.value),
628
+ memoryKiB: params.memoryKiB,
629
+ iterations: params.iterations,
630
+ parallelism: params.parallelism
631
+ };
632
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning, keyDerivation });
633
+ }
634
+ /**
635
+ * Verifies a candidate password against an Argon2id-derived entry using the
636
+ * supplied key derivation parameters. Constant-time comparison.
637
+ *
638
+ * @param name - Name of the secret to verify against
639
+ * @param password - Candidate password to test
640
+ * @param argon2idProvider - Argon2id provider (must produce bit-identical output for identical inputs)
641
+ * @param keyDerivation - The Argon2id key derivation parameters returned by `addSecretFromPasswordArgon2id`
642
+ * @returns Success(true) if candidate matches stored key, Success(false) if not,
643
+ * Failure if locked, secret missing, wrong type, or derivation fails
644
+ * @public
645
+ */
646
+ async verifySecretFromPasswordArgon2id(name, password, argon2idProvider, keyDerivation) {
647
+ if (!this._secrets) {
648
+ return (0, ts_utils_1.fail)('Key store is locked');
649
+ }
650
+ if (!password || password.length === 0) {
651
+ return (0, ts_utils_1.fail)('Password cannot be empty');
652
+ }
653
+ const entry = this._secrets.get(name);
654
+ if (!entry) {
655
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
656
+ }
657
+ if (entry.type !== 'encryption-key') {
658
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
659
+ }
660
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
661
+ if (saltResult.isFailure()) {
662
+ return (0, ts_utils_1.fail)(`Invalid salt: ${saltResult.message}`);
663
+ }
664
+ const params = {
665
+ memoryKiB: keyDerivation.memoryKiB,
666
+ iterations: keyDerivation.iterations,
667
+ parallelism: keyDerivation.parallelism,
668
+ outputBytes: entry.key.length
669
+ };
670
+ const derivedResult = await argon2idProvider.argon2id(password, saltResult.value, params);
671
+ if (derivedResult.isFailure()) {
672
+ return (0, ts_utils_1.fail)(`Argon2id key derivation failed: ${derivedResult.message}`);
673
+ }
674
+ return (0, ts_utils_1.succeed)(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
675
+ }
676
+ /**
677
+ * Removes a secret by name. Vault-first: the in-memory vault entry is dropped
678
+ * before any storage cleanup runs. For asymmetric-keypair entries, best-effort
679
+ * calls {@link CryptoUtils.KeyStore.IPrivateKeyStorage}.delete on the entry's
680
+ * `id`; a failure is reported via `warning` on the result but does not roll
681
+ * back the vault removal.
494
682
  * @param name - Name of the secret to remove
495
- * @returns Success with removed entry, Failure if not found or locked
683
+ * @returns Success with removed entry (and optional warning), Failure if not found or locked
496
684
  * @public
497
685
  */
498
- removeSecret(name) {
686
+ async removeSecret(name) {
499
687
  if (!this._secrets) {
500
688
  return (0, ts_utils_1.fail)('Key store is locked');
501
689
  }
@@ -503,11 +691,12 @@ class KeyStore {
503
691
  if (!entry) {
504
692
  return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
505
693
  }
506
- // Clear the key before removing (security)
507
- entry.key.fill(0);
694
+ // Vault-first: drop the in-memory entry before touching storage so a
695
+ // storage failure cannot block removal.
508
696
  this._secrets.delete(name);
509
697
  this._dirty = true;
510
- return (0, ts_utils_1.succeed)(entry);
698
+ const warning = await this._releaseEntryResources(entry);
699
+ return (0, ts_utils_1.succeed)({ entry, warning });
511
700
  }
512
701
  /**
513
702
  * Imports an API key string into the vault.
@@ -518,7 +707,7 @@ class KeyStore {
518
707
  * @returns Success with entry, Failure if locked, empty, or exists and !replace
519
708
  * @public
520
709
  */
521
- importApiKey(name, apiKey, options) {
710
+ async importApiKey(name, apiKey, options) {
522
711
  if (!this._secrets) {
523
712
  return (0, ts_utils_1.fail)('Key store is locked');
524
713
  }
@@ -528,8 +717,8 @@ class KeyStore {
528
717
  if (!apiKey || apiKey.length === 0) {
529
718
  return (0, ts_utils_1.fail)('API key cannot be empty');
530
719
  }
531
- const exists = this._secrets.has(name);
532
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
720
+ const existing = this._secrets.get(name);
721
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
533
722
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
534
723
  }
535
724
  const encoder = new TextEncoder();
@@ -540,9 +729,10 @@ class KeyStore {
540
729
  description: options === null || options === void 0 ? void 0 : options.description,
541
730
  createdAt: getCurrentTimestamp()
542
731
  };
732
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
543
733
  this._secrets.set(name, entry);
544
734
  this._dirty = true;
545
- return (0, ts_utils_1.succeed)({ entry, replaced: exists });
735
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
546
736
  }
547
737
  /**
548
738
  * Retrieves an API key string by name.
@@ -565,6 +755,118 @@ class KeyStore {
565
755
  const decoder = new TextDecoder();
566
756
  return (0, ts_utils_1.succeed)(decoder.decode(entry.key));
567
757
  }
758
+ // ============================================================================
759
+ // Asymmetric Keypair Management
760
+ // ============================================================================
761
+ /**
762
+ * Adds a new asymmetric keypair to the vault. Storage-first: the private key
763
+ * is stored under a freshly-minted `id` before the public-key vault entry is
764
+ * committed. If the storage call fails, no vault entry is written and the
765
+ * operation returns Failure.
766
+ *
767
+ * When `replace: true` displaces an existing entry (asymmetric or symmetric),
768
+ * a fresh `id` is minted; the displaced entry's resources are released
769
+ * best-effort. Failure of the storage delete is reported via `warning` on the
770
+ * result but does not roll back the replacement.
771
+ *
772
+ * Requires a {@link CryptoUtils.KeyStore.IPrivateKeyStorage} backend
773
+ * supplied at construction.
774
+ *
775
+ * @param name - Unique name for the entry
776
+ * @param options - Algorithm, optional description, replace flag
777
+ * @returns Success with the new entry, Failure if locked, no provider, or storage write failed
778
+ * @public
779
+ */
780
+ async addKeyPair(name, options) {
781
+ if (!this._secrets) {
782
+ return (0, ts_utils_1.fail)('Key store is locked');
783
+ }
784
+ if (!name || name.length === 0) {
785
+ return (0, ts_utils_1.fail)('Entry name cannot be empty');
786
+ }
787
+ if (!this._privateKeyStorage) {
788
+ return (0, ts_utils_1.fail)('No private key storage configured');
789
+ }
790
+ const existing = this._secrets.get(name);
791
+ if (existing && !options.replace) {
792
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
793
+ }
794
+ // Generate the keypair before touching storage. extractable=true on backends
795
+ // that round-trip via JWK; extractable=false on backends that hold CryptoKey
796
+ // refs directly.
797
+ const extractable = !this._privateKeyStorage.supportsNonExtractable;
798
+ const keyPairResult = await this._cryptoProvider.generateKeyPair(options.algorithm, extractable);
799
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests; cannot be triggered here without mocking */
800
+ if (keyPairResult.isFailure()) {
801
+ return (0, ts_utils_1.fail)(`Failed to generate keypair for '${name}': ${keyPairResult.message}`);
802
+ }
803
+ const { publicKey, privateKey } = keyPairResult.value;
804
+ const jwkResult = await this._cryptoProvider.exportPublicKeyJwk(publicKey);
805
+ /* c8 ignore next 3 - export of an extractable freshly-generated public key is hard to fail */
806
+ if (jwkResult.isFailure()) {
807
+ return (0, ts_utils_1.fail)(`Failed to export public key for '${name}': ${jwkResult.message}`);
808
+ }
809
+ const idResult = this._generateId();
810
+ /* c8 ignore next 3 - random-bytes failure is hard to trigger with a healthy provider */
811
+ if (idResult.isFailure()) {
812
+ return (0, ts_utils_1.fail)(`Failed to mint storage id for '${name}': ${idResult.message}`);
813
+ }
814
+ const id = idResult.value;
815
+ // Storage-first: write the private key before committing the vault entry.
816
+ const storeResult = await this._privateKeyStorage.store(id, privateKey);
817
+ if (storeResult.isFailure()) {
818
+ return (0, ts_utils_1.fail)(`Failed to persist private key for '${name}': ${storeResult.message}`);
819
+ }
820
+ const entry = {
821
+ name,
822
+ type: 'asymmetric-keypair',
823
+ id,
824
+ algorithm: options.algorithm,
825
+ publicKeyJwk: jwkResult.value,
826
+ description: options.description,
827
+ createdAt: getCurrentTimestamp()
828
+ };
829
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
830
+ this._secrets.set(name, entry);
831
+ this._dirty = true;
832
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
833
+ }
834
+ /**
835
+ * Retrieves the keypair for an asymmetric-keypair entry. The private key is
836
+ * loaded from {@link CryptoUtils.KeyStore.IPrivateKeyStorage} on every call —
837
+ * the keystore never caches private `CryptoKey` references between calls.
838
+ * The public key is re-imported from the vault's JWK so callers always
839
+ * receive a `CryptoKey` rather than the JWK form.
840
+ * @param name - Name of the entry
841
+ * @returns Success with `{ publicKey, privateKey }`, Failure if not found,
842
+ * locked, wrong type, no provider, or storage load failed.
843
+ * @public
844
+ */
845
+ async getKeyPair(name) {
846
+ if (!this._secrets) {
847
+ return (0, ts_utils_1.fail)('Key store is locked');
848
+ }
849
+ const entry = this._secrets.get(name);
850
+ if (!entry) {
851
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
852
+ }
853
+ if (entry.type !== 'asymmetric-keypair') {
854
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
855
+ }
856
+ if (!this._privateKeyStorage) {
857
+ return (0, ts_utils_1.fail)('No private key storage configured');
858
+ }
859
+ const privateResult = await this._privateKeyStorage.load(entry.id);
860
+ if (privateResult.isFailure()) {
861
+ return (0, ts_utils_1.fail)(`Failed to load private key for '${name}': ${privateResult.message}`);
862
+ }
863
+ const publicResult = await this._cryptoProvider.importPublicKeyJwk(entry.publicKeyJwk, entry.algorithm);
864
+ /* c8 ignore next 3 - vault JWKs that previously exported cleanly are extremely unlikely to fail re-import */
865
+ if (publicResult.isFailure()) {
866
+ return (0, ts_utils_1.fail)(`Failed to re-import public key for '${name}': ${publicResult.message}`);
867
+ }
868
+ return (0, ts_utils_1.succeed)({ publicKey: publicResult.value, privateKey: privateResult.value });
869
+ }
568
870
  /**
569
871
  * Lists secret names filtered by type.
570
872
  * @param type - The secret type to filter by
@@ -604,7 +906,8 @@ class KeyStore {
604
906
  if (oldName !== newName && this._secrets.has(newName)) {
605
907
  return (0, ts_utils_1.fail)(`Secret '${newName}' already exists`);
606
908
  }
607
- // Create new entry with new name (preserve type)
909
+ // Create new entry with new name. For asymmetric entries the spread
910
+ // preserves `id` so the storage handle survives the rename.
608
911
  const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
609
912
  this._secrets.delete(oldName);
610
913
  this._secrets.set(newName, newEntry);
@@ -634,49 +937,29 @@ class KeyStore {
634
937
  if (keyResult.isFailure()) {
635
938
  return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
636
939
  }
637
- // Build vault contents
638
- const secrets = {};
639
- for (const [name, entry] of this._secrets) {
640
- secrets[name] = {
641
- name: entry.name,
642
- type: entry.type,
643
- key: this._cryptoProvider.toBase64(entry.key),
644
- description: entry.description,
645
- createdAt: entry.createdAt
646
- };
647
- }
648
- const vaultContents = {
649
- version: model_1.KEYSTORE_FORMAT,
650
- secrets
651
- };
652
- // Serialize and encrypt
653
- const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
654
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
655
- if (jsonResult.isFailure()) {
656
- return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
940
+ return this._encryptVault(keyResult.value);
941
+ }
942
+ /**
943
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
944
+ * derivation. Use this when the derived key has been stored externally
945
+ * (e.g., in another key store) and the original password is no longer
946
+ * available.
947
+ *
948
+ * The supplied key must be the same key that was (or would be) derived
949
+ * from the master password using the key store's PBKDF2 parameters.
950
+ *
951
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
952
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
953
+ * @public
954
+ */
955
+ async saveWithKey(derivedKey) {
956
+ if (!this._secrets || !this._salt) {
957
+ return (0, ts_utils_1.fail)('Key store is locked');
657
958
  }
658
- const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, keyResult.value);
659
- /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
660
- if (encryptResult.isFailure()) {
661
- return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
959
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
960
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
662
961
  }
663
- const { iv, authTag, encryptedData } = encryptResult.value;
664
- const keystoreFileData = {
665
- format: model_1.KEYSTORE_FORMAT,
666
- algorithm: Constants.DEFAULT_ALGORITHM,
667
- iv: this._cryptoProvider.toBase64(iv),
668
- authTag: this._cryptoProvider.toBase64(authTag),
669
- encryptedData: this._cryptoProvider.toBase64(encryptedData),
670
- keyDerivation: {
671
- kdf: 'pbkdf2',
672
- salt: this._cryptoProvider.toBase64(this._salt),
673
- iterations: this._iterations
674
- }
675
- };
676
- this._keystoreFile = keystoreFileData;
677
- this._dirty = false;
678
- this._isNew = false;
679
- return (0, ts_utils_1.succeed)(keystoreFileData);
962
+ return this._encryptVault(derivedKey);
680
963
  }
681
964
  /**
682
965
  * Changes the master password.
@@ -720,7 +1003,7 @@ class KeyStore {
720
1003
  }
721
1004
  }
722
1005
  // Generate new salt for the new password using crypto provider
723
- const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
1006
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
724
1007
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
725
1008
  if (saltResult.isFailure()) {
726
1009
  return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
@@ -744,6 +1027,9 @@ class KeyStore {
744
1027
  if (secretResult.isFailure()) {
745
1028
  return (0, ts_utils_1.fail)(`encryptByName: ${secretResult.message}`);
746
1029
  }
1030
+ if (secretResult.value.type === 'asymmetric-keypair') {
1031
+ return (0, ts_utils_1.fail)(`encryptByName: secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
1032
+ }
747
1033
  return (0, encryptedFile_1.createEncryptedFile)({
748
1034
  content,
749
1035
  secretName,
@@ -771,6 +1057,9 @@ class KeyStore {
771
1057
  if (!entry) {
772
1058
  return (0, ts_utils_1.fail)(`Secret '${secretName}' not found in key store`);
773
1059
  }
1060
+ if (entry.type === 'asymmetric-keypair') {
1061
+ return (0, ts_utils_1.fail)(`Secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
1062
+ }
774
1063
  return (0, ts_utils_1.succeed)(entry.key);
775
1064
  };
776
1065
  return (0, ts_utils_1.succeed)(provider);
@@ -790,6 +1079,217 @@ class KeyStore {
790
1079
  cryptoProvider: this._cryptoProvider
791
1080
  });
792
1081
  }
1082
+ // ============================================================================
1083
+ // Private: Vault Encryption / Decryption
1084
+ // ============================================================================
1085
+ /**
1086
+ * Encrypts the vault with a derived key and returns the key store file.
1087
+ * Shared by `save()` and `saveWithKey()`.
1088
+ */
1089
+ async _encryptVault(derivedKey) {
1090
+ // _secrets and _salt are guaranteed non-undefined by callers
1091
+ const secrets = this._secrets;
1092
+ const salt = this._salt;
1093
+ // Build vault contents
1094
+ const secretEntries = {};
1095
+ for (const [name, entry] of secrets) {
1096
+ if (entry.type === 'asymmetric-keypair') {
1097
+ secretEntries[name] = {
1098
+ name: entry.name,
1099
+ type: entry.type,
1100
+ id: entry.id,
1101
+ algorithm: entry.algorithm,
1102
+ publicKeyJwk: entry.publicKeyJwk,
1103
+ description: entry.description,
1104
+ createdAt: entry.createdAt
1105
+ };
1106
+ }
1107
+ else {
1108
+ secretEntries[name] = {
1109
+ name: entry.name,
1110
+ type: entry.type,
1111
+ key: this._cryptoProvider.toBase64(entry.key),
1112
+ description: entry.description,
1113
+ createdAt: entry.createdAt
1114
+ };
1115
+ }
1116
+ }
1117
+ const vaultContents = {
1118
+ version: model_2.KEYSTORE_FORMAT,
1119
+ secrets: secretEntries
1120
+ };
1121
+ // Serialize and encrypt
1122
+ const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
1123
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1124
+ if (jsonResult.isFailure()) {
1125
+ return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
1126
+ }
1127
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
1128
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
1129
+ if (encryptResult.isFailure()) {
1130
+ return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
1131
+ }
1132
+ const { iv, authTag, encryptedData } = encryptResult.value;
1133
+ const keystoreFileData = {
1134
+ format: model_2.KEYSTORE_FORMAT,
1135
+ algorithm: Constants.DEFAULT_ALGORITHM,
1136
+ iv: this._cryptoProvider.toBase64(iv),
1137
+ authTag: this._cryptoProvider.toBase64(authTag),
1138
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
1139
+ keyDerivation: {
1140
+ kdf: 'pbkdf2',
1141
+ salt: this._cryptoProvider.toBase64(salt),
1142
+ iterations: this._iterations
1143
+ }
1144
+ };
1145
+ this._keystoreFile = keystoreFileData;
1146
+ this._dirty = false;
1147
+ this._isNew = false;
1148
+ return (0, ts_utils_1.succeed)(keystoreFileData);
1149
+ }
1150
+ /**
1151
+ * Decrypts the vault with a derived key and loads secrets into memory.
1152
+ * Shared by `unlock()` and `unlockWithKey()`.
1153
+ */
1154
+ async _decryptVault(derivedKey) {
1155
+ const keystoreFile = this._keystoreFile;
1156
+ /* c8 ignore next 3 - defensive: _decryptVault is only called after a successful open() or create() */
1157
+ if (keystoreFile === undefined) {
1158
+ return (0, ts_utils_1.fail)('No key store file loaded');
1159
+ }
1160
+ const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
1161
+ const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
1162
+ const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
1163
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
1164
+ if (ivResult.isFailure()) {
1165
+ return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
1166
+ }
1167
+ if (authTagResult.isFailure()) {
1168
+ return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
1169
+ }
1170
+ if (encryptedDataResult.isFailure()) {
1171
+ return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
1172
+ }
1173
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
1174
+ if (decryptResult.isFailure()) {
1175
+ return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
1176
+ }
1177
+ // Parse the vault contents
1178
+ const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
1179
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1180
+ if (parseResult.isFailure()) {
1181
+ return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
1182
+ }
1183
+ const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
1184
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1185
+ if (vaultResult.isFailure()) {
1186
+ return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
1187
+ }
1188
+ // Build secrets into local variables to avoid partial state on failure
1189
+ const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
1190
+ if (saltResult.isFailure()) {
1191
+ return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
1192
+ }
1193
+ const secrets = new Map();
1194
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
1195
+ if (jsonEntry.type === 'asymmetric-keypair') {
1196
+ const entry = {
1197
+ name,
1198
+ type: jsonEntry.type,
1199
+ id: jsonEntry.id,
1200
+ algorithm: jsonEntry.algorithm,
1201
+ publicKeyJwk: jsonEntry.publicKeyJwk,
1202
+ description: jsonEntry.description,
1203
+ createdAt: jsonEntry.createdAt
1204
+ };
1205
+ secrets.set(name, entry);
1206
+ }
1207
+ else {
1208
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
1209
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1210
+ if (keyBytesResult.isFailure()) {
1211
+ return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
1212
+ }
1213
+ const entry = {
1214
+ name,
1215
+ type: jsonEntry.type,
1216
+ key: keyBytesResult.value,
1217
+ description: jsonEntry.description,
1218
+ createdAt: jsonEntry.createdAt
1219
+ };
1220
+ secrets.set(name, entry);
1221
+ }
1222
+ }
1223
+ // All validation passed — commit state atomically
1224
+ this._salt = saltResult.value;
1225
+ this._secrets = secrets;
1226
+ this._state = 'unlocked';
1227
+ this._dirty = false;
1228
+ return (0, ts_utils_1.succeed)(this);
1229
+ }
1230
+ // ============================================================================
1231
+ // Private: Helpers for asymmetric flows
1232
+ // ============================================================================
1233
+ /**
1234
+ * Releases the resources held by an entry being displaced from the vault.
1235
+ * Symmetric entries get their key buffer zeroed in place. Asymmetric entries
1236
+ * have their private-key blob best-effort deleted from
1237
+ * {@link CryptoUtils.KeyStore.IPrivateKeyStorage}; if the storage call fails,
1238
+ * a warning string is returned but the displacement still proceeds — the
1239
+ * orphaned blob is left for consumer-side GC. Without a configured provider,
1240
+ * asymmetric cleanup is silently skipped.
1241
+ * @returns A warning string if storage cleanup failed, otherwise undefined.
1242
+ */
1243
+ async _releaseEntryResources(entry) {
1244
+ if (entry.type === 'asymmetric-keypair') {
1245
+ if (!this._privateKeyStorage) {
1246
+ return undefined;
1247
+ }
1248
+ const deleteResult = await this._privateKeyStorage.delete(entry.id);
1249
+ if (deleteResult.isFailure()) {
1250
+ return `Failed to delete prior storage blob for '${entry.name}' (id ${entry.id}): ${deleteResult.message}`;
1251
+ }
1252
+ return undefined;
1253
+ }
1254
+ entry.key.fill(0);
1255
+ return undefined;
1256
+ }
1257
+ /**
1258
+ * Constant-time byte comparison. Returns false immediately for length
1259
+ * mismatch (length is not secret); for equal-length inputs, walks the full
1260
+ * buffer accumulating differences via XOR so the running time does not leak
1261
+ * the position of the first differing byte.
1262
+ */
1263
+ static _timingSafeEqual(a, b) {
1264
+ /* c8 ignore next 3 - defensive: callers compare equal-length 32-byte PBKDF2 keys */
1265
+ if (a.length !== b.length) {
1266
+ return false;
1267
+ }
1268
+ let diff = 0;
1269
+ for (let i = 0; i < a.length; i++) {
1270
+ // eslint-disable-next-line no-bitwise
1271
+ diff |= a[i] ^ b[i];
1272
+ }
1273
+ return diff === 0;
1274
+ }
1275
+ /**
1276
+ * Mints a fresh UUID v4 storage handle using the crypto provider's
1277
+ * {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
1278
+ * Random-bytes failures propagate as Failure.
1279
+ */
1280
+ _generateId() {
1281
+ return this._cryptoProvider.generateRandomBytes(16).onSuccess((bytes) => {
1282
+ // Per RFC 4122 §4.4: set version (4) and variant (10xx) bits.
1283
+ // eslint-disable-next-line no-bitwise
1284
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1285
+ // eslint-disable-next-line no-bitwise
1286
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1287
+ const hex = Array.from(bytes)
1288
+ .map((b) => b.toString(16).padStart(2, '0'))
1289
+ .join('');
1290
+ 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)}`);
1291
+ });
1292
+ }
793
1293
  }
794
1294
  exports.KeyStore = KeyStore;
795
1295
  //# sourceMappingURL=keyStore.js.map