@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
@@ -20,6 +20,7 @@
20
20
  import { captureResult, fail, succeed } from '@fgv/ts-utils';
21
21
  import * as Constants from '../constants';
22
22
  import { createEncryptedFile } from '../encryptedFile';
23
+ import { ARGON2ID_OWASP_MIN } from '../model';
23
24
  import { DEFAULT_KEYSTORE_ITERATIONS, DEFAULT_SECRET_ITERATIONS, KEYSTORE_FORMAT, MIN_SALT_LENGTH } from './model';
24
25
  import { keystoreFile, keystoreVaultContents } from './converters';
25
26
  /**
@@ -63,8 +64,9 @@ function getCurrentTimestamp() {
63
64
  * @public
64
65
  */
65
66
  export class KeyStore {
66
- constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
67
+ constructor(cryptoProvider, iterations, keystoreFile, isNew, privateKeyStorage) {
67
68
  this._cryptoProvider = cryptoProvider;
69
+ this._privateKeyStorage = privateKeyStorage;
68
70
  this._iterations = iterations;
69
71
  this._keystoreFile = keystoreFile;
70
72
  this._state = 'locked';
@@ -87,7 +89,7 @@ export class KeyStore {
87
89
  if (iterations < 1) {
88
90
  return fail('Iterations must be at least 1');
89
91
  }
90
- return succeed(new KeyStore(params.cryptoProvider, iterations, undefined, true));
92
+ return succeed(new KeyStore(params.cryptoProvider, iterations, undefined, true, params.privateKeyStorage));
91
93
  }
92
94
  /**
93
95
  * Opens an existing encrypted key store.
@@ -103,7 +105,7 @@ export class KeyStore {
103
105
  return fail(`Invalid key store file: ${fileResult.message}`);
104
106
  }
105
107
  const iterations = fileResult.value.keyDerivation.iterations;
106
- return succeed(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
108
+ return succeed(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false, params.privateKeyStorage));
107
109
  }
108
110
  // ============================================================================
109
111
  // Lifecycle Methods
@@ -146,7 +148,6 @@ export class KeyStore {
146
148
  * @public
147
149
  */
148
150
  async unlock(password) {
149
- var _a;
150
151
  if (this._isNew) {
151
152
  return fail('Cannot unlock a new key store - use initialize() instead');
152
153
  }
@@ -170,57 +171,37 @@ export class KeyStore {
170
171
  if (keyResult.isFailure()) {
171
172
  return fail(`Key derivation failed: ${keyResult.message}`);
172
173
  }
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');
174
+ return this._decryptVault(keyResult.value);
175
+ }
176
+ /**
177
+ * Unlocks an existing key store with a pre-derived key, bypassing
178
+ * PBKDF2 key derivation. Use this when the derived key has been
179
+ * stored externally (e.g., in another key store) and the original
180
+ * password is no longer available.
181
+ *
182
+ * The supplied key must have been derived from the correct password
183
+ * using the key store file's own PBKDF2 parameters (salt and
184
+ * iteration count).
185
+ *
186
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
187
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
188
+ * @public
189
+ */
190
+ async unlockWithKey(derivedKey) {
191
+ if (this._isNew) {
192
+ return fail('Cannot unlock a new key store - use initialize() instead');
190
193
  }
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}`);
194
+ if (this._state === 'unlocked') {
195
+ return fail('Key store is already unlocked');
196
196
  }
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}`);
197
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
198
+ return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
201
199
  }
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);
200
+ /* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
201
+ if (!this._keystoreFile) {
202
+ return fail('No key store file to unlock');
220
203
  }
221
- this._state = 'unlocked';
222
- this._dirty = false;
223
- return succeed(this);
204
+ return this._decryptVault(derivedKey);
224
205
  }
225
206
  /**
226
207
  * Locks the key store, clearing all secrets from memory.
@@ -238,7 +219,9 @@ export class KeyStore {
238
219
  // Clear secrets from memory (overwrite for security)
239
220
  if (this._secrets) {
240
221
  for (const entry of this._secrets.values()) {
241
- entry.key.fill(0);
222
+ if (entry.type !== 'asymmetric-keypair') {
223
+ entry.key.fill(0);
224
+ }
242
225
  }
243
226
  this._secrets.clear();
244
227
  this._secrets = undefined;
@@ -300,7 +283,9 @@ export class KeyStore {
300
283
  return succeed(Array.from(this._secrets.keys()));
301
284
  }
302
285
  /**
303
- * Gets a secret by name.
286
+ * Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
287
+ * — callers must check `entry.type` before accessing `key`/`id` since asymmetric
288
+ * entries carry no raw key material.
304
289
  * @param name - Name of the secret
305
290
  * @returns Success with secret entry, Failure if not found or locked
306
291
  * @public
@@ -315,6 +300,27 @@ export class KeyStore {
315
300
  }
316
301
  return succeed(entry);
317
302
  }
303
+ /**
304
+ * Returns the public-key JWK for an asymmetric-keypair entry.
305
+ * Available without {@link CryptoUtils.KeyStore.IPrivateKeyStorage} since the
306
+ * public key lives in the vault metadata directly.
307
+ * @param name - Name of the entry
308
+ * @returns Success with the JWK, Failure if not found, locked, or wrong type
309
+ * @public
310
+ */
311
+ getPublicKeyJwk(name) {
312
+ if (!this._secrets) {
313
+ return fail('Key store is locked');
314
+ }
315
+ const entry = this._secrets.get(name);
316
+ if (!entry) {
317
+ return fail(`Secret '${name}' not found`);
318
+ }
319
+ if (entry.type !== 'asymmetric-keypair') {
320
+ return fail(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
321
+ }
322
+ return succeed(entry.publicKeyJwk);
323
+ }
318
324
  /**
319
325
  * Checks if a secret exists.
320
326
  * @param name - Name of the secret
@@ -341,7 +347,6 @@ export class KeyStore {
341
347
  if (!name || name.length === 0) {
342
348
  return fail('Secret name cannot be empty');
343
349
  }
344
- const replaced = this._secrets.has(name);
345
350
  // Generate a new random key
346
351
  const keyResult = await this._cryptoProvider.generateKey();
347
352
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
@@ -355,19 +360,28 @@ export class KeyStore {
355
360
  description: options === null || options === void 0 ? void 0 : options.description,
356
361
  createdAt: getCurrentTimestamp()
357
362
  };
363
+ const existing = this._secrets.get(name);
364
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
358
365
  this._secrets.set(name, entry);
359
366
  this._dirty = true;
360
- return succeed({ entry, replaced });
367
+ return succeed({ entry, replaced: existing !== undefined, warning });
361
368
  }
362
369
  /**
363
- * Imports an existing secret key.
370
+ * Imports raw 32-byte key material into the vault.
371
+ *
372
+ * Always validates that the key is exactly 32 bytes (AES-256). The optional
373
+ * `type` field is a classification label stored with the entry; it does not
374
+ * change the validation rules. For importing UTF-8 API key strings (variable
375
+ * length), use {@link KeyStore.importApiKey} instead.
376
+ *
364
377
  * @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
378
+ * @param key - The 32-byte AES-256 key material
379
+ * @param options - Optional type classification, description, whether to replace existing
367
380
  * @returns Success with entry, Failure if locked, key invalid, or exists and !replace
368
381
  * @public
369
382
  */
370
- importSecret(name, key, options) {
383
+ async importSecret(name, key, options) {
384
+ var _a;
371
385
  if (!this._secrets) {
372
386
  return fail('Key store is locked');
373
387
  }
@@ -377,20 +391,21 @@ export class KeyStore {
377
391
  if (key.length !== Constants.AES_256_KEY_SIZE) {
378
392
  return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
379
393
  }
380
- const exists = this._secrets.has(name);
381
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
394
+ const existing = this._secrets.get(name);
395
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
382
396
  return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
383
397
  }
384
398
  const entry = {
385
399
  name,
386
- type: 'encryption-key',
400
+ type: (_a = options === null || options === void 0 ? void 0 : options.type) !== null && _a !== void 0 ? _a : 'encryption-key',
387
401
  key: new Uint8Array(key), // Copy to prevent external modification
388
402
  description: options === null || options === void 0 ? void 0 : options.description,
389
403
  createdAt: getCurrentTimestamp()
390
404
  };
405
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
391
406
  this._secrets.set(name, entry);
392
407
  this._dirty = true;
393
- return succeed({ entry, replaced: exists });
408
+ return succeed({ entry, replaced: existing !== undefined, warning });
394
409
  }
395
410
  /**
396
411
  * Adds a secret derived from a password using PBKDF2.
@@ -417,8 +432,8 @@ export class KeyStore {
417
432
  if (!password || password.length === 0) {
418
433
  return fail('Password cannot be empty');
419
434
  }
420
- const exists = this._secrets.has(name);
421
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
435
+ const existing = this._secrets.get(name);
436
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
422
437
  return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
423
438
  }
424
439
  const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : DEFAULT_SECRET_ITERATIONS;
@@ -441,11 +456,13 @@ export class KeyStore {
441
456
  description: options === null || options === void 0 ? void 0 : options.description,
442
457
  createdAt: getCurrentTimestamp()
443
458
  };
459
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
444
460
  this._secrets.set(name, entry);
445
461
  this._dirty = true;
446
462
  return succeed({
447
463
  entry,
448
- replaced: exists,
464
+ replaced: existing !== undefined,
465
+ warning,
449
466
  keyDerivation: {
450
467
  kdf: 'pbkdf2',
451
468
  salt: this._cryptoProvider.toBase64(saltResult.value),
@@ -454,12 +471,183 @@ export class KeyStore {
454
471
  });
455
472
  }
456
473
  /**
457
- * Removes a secret by name.
474
+ * Verifies that a candidate password derives the same key material currently
475
+ * stored under `name`, using the supplied
476
+ * {@link CryptoUtils.IKeyDerivationParams | key derivation parameters}.
477
+ *
478
+ * The keystore does not persist per-slot key derivation parameters with the
479
+ * entry — callers receive them from `addSecretFromPassword` and store them
480
+ * alongside the encrypted artifact (or wherever else makes sense). Pass
481
+ * those same parameters here for verification.
482
+ *
483
+ * Re-derives a key from `password` + `keyDerivation`, then compares it to
484
+ * the stored key material in constant time. Restricted to entries of type
485
+ * `'encryption-key'` — the type produced by `addSecretFromPassword`. Other
486
+ * symmetric types (`'api-key'`) and asymmetric entries are rejected so
487
+ * the boolean result reflects "this slot accepts this password" rather
488
+ * than an incidental byte-equality match against unrelated material.
489
+ *
490
+ * Note: the keystore does not currently flag whether an `'encryption-key'`
491
+ * entry was actually password-derived (vs. random via `addSecret` or raw
492
+ * via `importSecret`). A `true` result therefore means "the candidate
493
+ * password produces the same 32 bytes currently stored", which is what
494
+ * the equivalent consumer-side helper (`verifyGatePassword`) already
495
+ * implies for entries it manages.
496
+ *
497
+ * @param name - Name of the secret to verify against
498
+ * @param password - Candidate password to test
499
+ * @param keyDerivation - The key derivation parameters returned by
500
+ * `addSecretFromPassword` when the secret was created. Only
501
+ * `kdf: 'pbkdf2'` is supported.
502
+ * @returns Success(true) when the candidate matches the stored key,
503
+ * Success(false) when it does not, Failure if locked, secret missing,
504
+ * wrong type, unsupported `kdf`, or key derivation fails
505
+ * @public
506
+ */
507
+ async verifySecretFromPassword(name, password, keyDerivation) {
508
+ if (!this._secrets) {
509
+ return fail('Key store is locked');
510
+ }
511
+ if (!password || password.length === 0) {
512
+ return fail('Password cannot be empty');
513
+ }
514
+ if (keyDerivation.kdf !== 'pbkdf2') {
515
+ return fail(`Unsupported kdf '${keyDerivation.kdf}' (expected 'pbkdf2')`);
516
+ }
517
+ const entry = this._secrets.get(name);
518
+ if (!entry) {
519
+ return fail(`Secret '${name}' not found`);
520
+ }
521
+ if (entry.type !== 'encryption-key') {
522
+ return fail(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
523
+ }
524
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
525
+ if (saltResult.isFailure()) {
526
+ return fail(`Invalid salt: ${saltResult.message}`);
527
+ }
528
+ const derivedResult = await this._cryptoProvider.deriveKey(password, saltResult.value, keyDerivation.iterations);
529
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests */
530
+ if (derivedResult.isFailure()) {
531
+ return fail(`Key derivation failed: ${derivedResult.message}`);
532
+ }
533
+ return succeed(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
534
+ }
535
+ /**
536
+ * Adds a secret derived from a password using Argon2id (RFC 9106).
537
+ *
538
+ * The Argon2id provider must be supplied explicitly; the KeyStore does not
539
+ * hold one by default (consumers opt in by depending on the argon2 package).
540
+ *
541
+ * Returns the key derivation parameters so callers can store them alongside
542
+ * encrypted artifacts, enabling future re-derivation and verification.
543
+ *
544
+ * @param name - Unique name for the secret
545
+ * @param password - Password or passphrase
546
+ * @param argon2idProvider - Argon2id provider (Node or Browser implementation)
547
+ * @param options - Optional: Argon2id params (defaults to ARGON2ID_OWASP_MIN), description, replace flag
548
+ * @returns Success with entry and keyDerivation params, Failure if locked or invalid
549
+ * @public
550
+ */
551
+ async addSecretFromPasswordArgon2id(name, password, argon2idProvider, options) {
552
+ var _a;
553
+ if (!this._secrets) {
554
+ return fail('Key store is locked');
555
+ }
556
+ if (!name || name.length === 0) {
557
+ return fail('Secret name cannot be empty');
558
+ }
559
+ if (!password || password.length === 0) {
560
+ return fail('Password cannot be empty');
561
+ }
562
+ const existing = this._secrets.get(name);
563
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
564
+ return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
565
+ }
566
+ const params = (_a = options === null || options === void 0 ? void 0 : options.params) !== null && _a !== void 0 ? _a : ARGON2ID_OWASP_MIN;
567
+ const saltResult = this._cryptoProvider.generateRandomBytes(MIN_SALT_LENGTH);
568
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
569
+ if (saltResult.isFailure()) {
570
+ return fail(`Failed to generate salt: ${saltResult.message}`);
571
+ }
572
+ const keyResult = await argon2idProvider.argon2id(password, saltResult.value, params);
573
+ if (keyResult.isFailure()) {
574
+ return fail(`Argon2id key derivation failed: ${keyResult.message}`);
575
+ }
576
+ if (keyResult.value.length !== Constants.AES_256_KEY_SIZE) {
577
+ return fail(`Argon2id outputBytes must be ${Constants.AES_256_KEY_SIZE} for KeyStore secrets, got ${keyResult.value.length}`);
578
+ }
579
+ const entry = {
580
+ name,
581
+ type: 'encryption-key',
582
+ key: keyResult.value,
583
+ description: options === null || options === void 0 ? void 0 : options.description,
584
+ createdAt: getCurrentTimestamp()
585
+ };
586
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
587
+ this._secrets.set(name, entry);
588
+ this._dirty = true;
589
+ const keyDerivation = {
590
+ kdf: 'argon2id',
591
+ salt: this._cryptoProvider.toBase64(saltResult.value),
592
+ memoryKiB: params.memoryKiB,
593
+ iterations: params.iterations,
594
+ parallelism: params.parallelism
595
+ };
596
+ return succeed({ entry, replaced: existing !== undefined, warning, keyDerivation });
597
+ }
598
+ /**
599
+ * Verifies a candidate password against an Argon2id-derived entry using the
600
+ * supplied key derivation parameters. Constant-time comparison.
601
+ *
602
+ * @param name - Name of the secret to verify against
603
+ * @param password - Candidate password to test
604
+ * @param argon2idProvider - Argon2id provider (must produce bit-identical output for identical inputs)
605
+ * @param keyDerivation - The Argon2id key derivation parameters returned by `addSecretFromPasswordArgon2id`
606
+ * @returns Success(true) if candidate matches stored key, Success(false) if not,
607
+ * Failure if locked, secret missing, wrong type, or derivation fails
608
+ * @public
609
+ */
610
+ async verifySecretFromPasswordArgon2id(name, password, argon2idProvider, keyDerivation) {
611
+ if (!this._secrets) {
612
+ return fail('Key store is locked');
613
+ }
614
+ if (!password || password.length === 0) {
615
+ return fail('Password cannot be empty');
616
+ }
617
+ const entry = this._secrets.get(name);
618
+ if (!entry) {
619
+ return fail(`Secret '${name}' not found`);
620
+ }
621
+ if (entry.type !== 'encryption-key') {
622
+ return fail(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
623
+ }
624
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
625
+ if (saltResult.isFailure()) {
626
+ return fail(`Invalid salt: ${saltResult.message}`);
627
+ }
628
+ const params = {
629
+ memoryKiB: keyDerivation.memoryKiB,
630
+ iterations: keyDerivation.iterations,
631
+ parallelism: keyDerivation.parallelism,
632
+ outputBytes: entry.key.length
633
+ };
634
+ const derivedResult = await argon2idProvider.argon2id(password, saltResult.value, params);
635
+ if (derivedResult.isFailure()) {
636
+ return fail(`Argon2id key derivation failed: ${derivedResult.message}`);
637
+ }
638
+ return succeed(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
639
+ }
640
+ /**
641
+ * Removes a secret by name. Vault-first: the in-memory vault entry is dropped
642
+ * before any storage cleanup runs. For asymmetric-keypair entries, best-effort
643
+ * calls {@link CryptoUtils.KeyStore.IPrivateKeyStorage}.delete on the entry's
644
+ * `id`; a failure is reported via `warning` on the result but does not roll
645
+ * back the vault removal.
458
646
  * @param name - Name of the secret to remove
459
- * @returns Success with removed entry, Failure if not found or locked
647
+ * @returns Success with removed entry (and optional warning), Failure if not found or locked
460
648
  * @public
461
649
  */
462
- removeSecret(name) {
650
+ async removeSecret(name) {
463
651
  if (!this._secrets) {
464
652
  return fail('Key store is locked');
465
653
  }
@@ -467,11 +655,12 @@ export class KeyStore {
467
655
  if (!entry) {
468
656
  return fail(`Secret '${name}' not found`);
469
657
  }
470
- // Clear the key before removing (security)
471
- entry.key.fill(0);
658
+ // Vault-first: drop the in-memory entry before touching storage so a
659
+ // storage failure cannot block removal.
472
660
  this._secrets.delete(name);
473
661
  this._dirty = true;
474
- return succeed(entry);
662
+ const warning = await this._releaseEntryResources(entry);
663
+ return succeed({ entry, warning });
475
664
  }
476
665
  /**
477
666
  * Imports an API key string into the vault.
@@ -482,7 +671,7 @@ export class KeyStore {
482
671
  * @returns Success with entry, Failure if locked, empty, or exists and !replace
483
672
  * @public
484
673
  */
485
- importApiKey(name, apiKey, options) {
674
+ async importApiKey(name, apiKey, options) {
486
675
  if (!this._secrets) {
487
676
  return fail('Key store is locked');
488
677
  }
@@ -492,8 +681,8 @@ export class KeyStore {
492
681
  if (!apiKey || apiKey.length === 0) {
493
682
  return fail('API key cannot be empty');
494
683
  }
495
- const exists = this._secrets.has(name);
496
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
684
+ const existing = this._secrets.get(name);
685
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
497
686
  return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
498
687
  }
499
688
  const encoder = new TextEncoder();
@@ -504,9 +693,10 @@ export class KeyStore {
504
693
  description: options === null || options === void 0 ? void 0 : options.description,
505
694
  createdAt: getCurrentTimestamp()
506
695
  };
696
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
507
697
  this._secrets.set(name, entry);
508
698
  this._dirty = true;
509
- return succeed({ entry, replaced: exists });
699
+ return succeed({ entry, replaced: existing !== undefined, warning });
510
700
  }
511
701
  /**
512
702
  * Retrieves an API key string by name.
@@ -529,6 +719,118 @@ export class KeyStore {
529
719
  const decoder = new TextDecoder();
530
720
  return succeed(decoder.decode(entry.key));
531
721
  }
722
+ // ============================================================================
723
+ // Asymmetric Keypair Management
724
+ // ============================================================================
725
+ /**
726
+ * Adds a new asymmetric keypair to the vault. Storage-first: the private key
727
+ * is stored under a freshly-minted `id` before the public-key vault entry is
728
+ * committed. If the storage call fails, no vault entry is written and the
729
+ * operation returns Failure.
730
+ *
731
+ * When `replace: true` displaces an existing entry (asymmetric or symmetric),
732
+ * a fresh `id` is minted; the displaced entry's resources are released
733
+ * best-effort. Failure of the storage delete is reported via `warning` on the
734
+ * result but does not roll back the replacement.
735
+ *
736
+ * Requires a {@link CryptoUtils.KeyStore.IPrivateKeyStorage} backend
737
+ * supplied at construction.
738
+ *
739
+ * @param name - Unique name for the entry
740
+ * @param options - Algorithm, optional description, replace flag
741
+ * @returns Success with the new entry, Failure if locked, no provider, or storage write failed
742
+ * @public
743
+ */
744
+ async addKeyPair(name, options) {
745
+ if (!this._secrets) {
746
+ return fail('Key store is locked');
747
+ }
748
+ if (!name || name.length === 0) {
749
+ return fail('Entry name cannot be empty');
750
+ }
751
+ if (!this._privateKeyStorage) {
752
+ return fail('No private key storage configured');
753
+ }
754
+ const existing = this._secrets.get(name);
755
+ if (existing && !options.replace) {
756
+ return fail(`Secret '${name}' already exists - use replace=true to overwrite`);
757
+ }
758
+ // Generate the keypair before touching storage. extractable=true on backends
759
+ // that round-trip via JWK; extractable=false on backends that hold CryptoKey
760
+ // refs directly.
761
+ const extractable = !this._privateKeyStorage.supportsNonExtractable;
762
+ const keyPairResult = await this._cryptoProvider.generateKeyPair(options.algorithm, extractable);
763
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests; cannot be triggered here without mocking */
764
+ if (keyPairResult.isFailure()) {
765
+ return fail(`Failed to generate keypair for '${name}': ${keyPairResult.message}`);
766
+ }
767
+ const { publicKey, privateKey } = keyPairResult.value;
768
+ const jwkResult = await this._cryptoProvider.exportPublicKeyJwk(publicKey);
769
+ /* c8 ignore next 3 - export of an extractable freshly-generated public key is hard to fail */
770
+ if (jwkResult.isFailure()) {
771
+ return fail(`Failed to export public key for '${name}': ${jwkResult.message}`);
772
+ }
773
+ const idResult = this._generateId();
774
+ /* c8 ignore next 3 - random-bytes failure is hard to trigger with a healthy provider */
775
+ if (idResult.isFailure()) {
776
+ return fail(`Failed to mint storage id for '${name}': ${idResult.message}`);
777
+ }
778
+ const id = idResult.value;
779
+ // Storage-first: write the private key before committing the vault entry.
780
+ const storeResult = await this._privateKeyStorage.store(id, privateKey);
781
+ if (storeResult.isFailure()) {
782
+ return fail(`Failed to persist private key for '${name}': ${storeResult.message}`);
783
+ }
784
+ const entry = {
785
+ name,
786
+ type: 'asymmetric-keypair',
787
+ id,
788
+ algorithm: options.algorithm,
789
+ publicKeyJwk: jwkResult.value,
790
+ description: options.description,
791
+ createdAt: getCurrentTimestamp()
792
+ };
793
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
794
+ this._secrets.set(name, entry);
795
+ this._dirty = true;
796
+ return succeed({ entry, replaced: existing !== undefined, warning });
797
+ }
798
+ /**
799
+ * Retrieves the keypair for an asymmetric-keypair entry. The private key is
800
+ * loaded from {@link CryptoUtils.KeyStore.IPrivateKeyStorage} on every call —
801
+ * the keystore never caches private `CryptoKey` references between calls.
802
+ * The public key is re-imported from the vault's JWK so callers always
803
+ * receive a `CryptoKey` rather than the JWK form.
804
+ * @param name - Name of the entry
805
+ * @returns Success with `{ publicKey, privateKey }`, Failure if not found,
806
+ * locked, wrong type, no provider, or storage load failed.
807
+ * @public
808
+ */
809
+ async getKeyPair(name) {
810
+ if (!this._secrets) {
811
+ return fail('Key store is locked');
812
+ }
813
+ const entry = this._secrets.get(name);
814
+ if (!entry) {
815
+ return fail(`Secret '${name}' not found`);
816
+ }
817
+ if (entry.type !== 'asymmetric-keypair') {
818
+ return fail(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
819
+ }
820
+ if (!this._privateKeyStorage) {
821
+ return fail('No private key storage configured');
822
+ }
823
+ const privateResult = await this._privateKeyStorage.load(entry.id);
824
+ if (privateResult.isFailure()) {
825
+ return fail(`Failed to load private key for '${name}': ${privateResult.message}`);
826
+ }
827
+ const publicResult = await this._cryptoProvider.importPublicKeyJwk(entry.publicKeyJwk, entry.algorithm);
828
+ /* c8 ignore next 3 - vault JWKs that previously exported cleanly are extremely unlikely to fail re-import */
829
+ if (publicResult.isFailure()) {
830
+ return fail(`Failed to re-import public key for '${name}': ${publicResult.message}`);
831
+ }
832
+ return succeed({ publicKey: publicResult.value, privateKey: privateResult.value });
833
+ }
532
834
  /**
533
835
  * Lists secret names filtered by type.
534
836
  * @param type - The secret type to filter by
@@ -568,7 +870,8 @@ export class KeyStore {
568
870
  if (oldName !== newName && this._secrets.has(newName)) {
569
871
  return fail(`Secret '${newName}' already exists`);
570
872
  }
571
- // Create new entry with new name (preserve type)
873
+ // Create new entry with new name. For asymmetric entries the spread
874
+ // preserves `id` so the storage handle survives the rename.
572
875
  const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
573
876
  this._secrets.delete(oldName);
574
877
  this._secrets.set(newName, newEntry);
@@ -598,49 +901,29 @@ export class KeyStore {
598
901
  if (keyResult.isFailure()) {
599
902
  return fail(`Key derivation failed: ${keyResult.message}`);
600
903
  }
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}`);
904
+ return this._encryptVault(keyResult.value);
905
+ }
906
+ /**
907
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
908
+ * derivation. Use this when the derived key has been stored externally
909
+ * (e.g., in another key store) and the original password is no longer
910
+ * available.
911
+ *
912
+ * The supplied key must be the same key that was (or would be) derived
913
+ * from the master password using the key store's PBKDF2 parameters.
914
+ *
915
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
916
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
917
+ * @public
918
+ */
919
+ async saveWithKey(derivedKey) {
920
+ if (!this._secrets || !this._salt) {
921
+ return fail('Key store is locked');
621
922
  }
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}`);
923
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
924
+ return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
626
925
  }
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);
926
+ return this._encryptVault(derivedKey);
644
927
  }
645
928
  /**
646
929
  * Changes the master password.
@@ -708,6 +991,9 @@ export class KeyStore {
708
991
  if (secretResult.isFailure()) {
709
992
  return fail(`encryptByName: ${secretResult.message}`);
710
993
  }
994
+ if (secretResult.value.type === 'asymmetric-keypair') {
995
+ return fail(`encryptByName: secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
996
+ }
711
997
  return createEncryptedFile({
712
998
  content,
713
999
  secretName,
@@ -735,6 +1021,9 @@ export class KeyStore {
735
1021
  if (!entry) {
736
1022
  return fail(`Secret '${secretName}' not found in key store`);
737
1023
  }
1024
+ if (entry.type === 'asymmetric-keypair') {
1025
+ return fail(`Secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
1026
+ }
738
1027
  return succeed(entry.key);
739
1028
  };
740
1029
  return succeed(provider);
@@ -754,5 +1043,216 @@ export class KeyStore {
754
1043
  cryptoProvider: this._cryptoProvider
755
1044
  });
756
1045
  }
1046
+ // ============================================================================
1047
+ // Private: Vault Encryption / Decryption
1048
+ // ============================================================================
1049
+ /**
1050
+ * Encrypts the vault with a derived key and returns the key store file.
1051
+ * Shared by `save()` and `saveWithKey()`.
1052
+ */
1053
+ async _encryptVault(derivedKey) {
1054
+ // _secrets and _salt are guaranteed non-undefined by callers
1055
+ const secrets = this._secrets;
1056
+ const salt = this._salt;
1057
+ // Build vault contents
1058
+ const secretEntries = {};
1059
+ for (const [name, entry] of secrets) {
1060
+ if (entry.type === 'asymmetric-keypair') {
1061
+ secretEntries[name] = {
1062
+ name: entry.name,
1063
+ type: entry.type,
1064
+ id: entry.id,
1065
+ algorithm: entry.algorithm,
1066
+ publicKeyJwk: entry.publicKeyJwk,
1067
+ description: entry.description,
1068
+ createdAt: entry.createdAt
1069
+ };
1070
+ }
1071
+ else {
1072
+ secretEntries[name] = {
1073
+ name: entry.name,
1074
+ type: entry.type,
1075
+ key: this._cryptoProvider.toBase64(entry.key),
1076
+ description: entry.description,
1077
+ createdAt: entry.createdAt
1078
+ };
1079
+ }
1080
+ }
1081
+ const vaultContents = {
1082
+ version: KEYSTORE_FORMAT,
1083
+ secrets: secretEntries
1084
+ };
1085
+ // Serialize and encrypt
1086
+ const jsonResult = captureResult(() => JSON.stringify(vaultContents));
1087
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1088
+ if (jsonResult.isFailure()) {
1089
+ return fail(`Failed to serialize vault: ${jsonResult.message}`);
1090
+ }
1091
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
1092
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
1093
+ if (encryptResult.isFailure()) {
1094
+ return fail(`Encryption failed: ${encryptResult.message}`);
1095
+ }
1096
+ const { iv, authTag, encryptedData } = encryptResult.value;
1097
+ const keystoreFileData = {
1098
+ format: KEYSTORE_FORMAT,
1099
+ algorithm: Constants.DEFAULT_ALGORITHM,
1100
+ iv: this._cryptoProvider.toBase64(iv),
1101
+ authTag: this._cryptoProvider.toBase64(authTag),
1102
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
1103
+ keyDerivation: {
1104
+ kdf: 'pbkdf2',
1105
+ salt: this._cryptoProvider.toBase64(salt),
1106
+ iterations: this._iterations
1107
+ }
1108
+ };
1109
+ this._keystoreFile = keystoreFileData;
1110
+ this._dirty = false;
1111
+ this._isNew = false;
1112
+ return succeed(keystoreFileData);
1113
+ }
1114
+ /**
1115
+ * Decrypts the vault with a derived key and loads secrets into memory.
1116
+ * Shared by `unlock()` and `unlockWithKey()`.
1117
+ */
1118
+ async _decryptVault(derivedKey) {
1119
+ const keystoreFile = this._keystoreFile;
1120
+ /* c8 ignore next 3 - defensive: _decryptVault is only called after a successful open() or create() */
1121
+ if (keystoreFile === undefined) {
1122
+ return fail('No key store file loaded');
1123
+ }
1124
+ const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
1125
+ const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
1126
+ const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
1127
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
1128
+ if (ivResult.isFailure()) {
1129
+ return fail(`Invalid IV in key store file: ${ivResult.message}`);
1130
+ }
1131
+ if (authTagResult.isFailure()) {
1132
+ return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
1133
+ }
1134
+ if (encryptedDataResult.isFailure()) {
1135
+ return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
1136
+ }
1137
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
1138
+ if (decryptResult.isFailure()) {
1139
+ return fail('Incorrect password or corrupted key store');
1140
+ }
1141
+ // Parse the vault contents
1142
+ const parseResult = captureResult(() => JSON.parse(decryptResult.value));
1143
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1144
+ if (parseResult.isFailure()) {
1145
+ return fail(`Failed to parse vault contents: ${parseResult.message}`);
1146
+ }
1147
+ const vaultResult = keystoreVaultContents.convert(parseResult.value);
1148
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1149
+ if (vaultResult.isFailure()) {
1150
+ return fail(`Invalid vault format: ${vaultResult.message}`);
1151
+ }
1152
+ // Build secrets into local variables to avoid partial state on failure
1153
+ const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
1154
+ if (saltResult.isFailure()) {
1155
+ return fail(`Invalid salt in key store file: ${saltResult.message}`);
1156
+ }
1157
+ const secrets = new Map();
1158
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
1159
+ if (jsonEntry.type === 'asymmetric-keypair') {
1160
+ const entry = {
1161
+ name,
1162
+ type: jsonEntry.type,
1163
+ id: jsonEntry.id,
1164
+ algorithm: jsonEntry.algorithm,
1165
+ publicKeyJwk: jsonEntry.publicKeyJwk,
1166
+ description: jsonEntry.description,
1167
+ createdAt: jsonEntry.createdAt
1168
+ };
1169
+ secrets.set(name, entry);
1170
+ }
1171
+ else {
1172
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
1173
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1174
+ if (keyBytesResult.isFailure()) {
1175
+ return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
1176
+ }
1177
+ const entry = {
1178
+ name,
1179
+ type: jsonEntry.type,
1180
+ key: keyBytesResult.value,
1181
+ description: jsonEntry.description,
1182
+ createdAt: jsonEntry.createdAt
1183
+ };
1184
+ secrets.set(name, entry);
1185
+ }
1186
+ }
1187
+ // All validation passed — commit state atomically
1188
+ this._salt = saltResult.value;
1189
+ this._secrets = secrets;
1190
+ this._state = 'unlocked';
1191
+ this._dirty = false;
1192
+ return succeed(this);
1193
+ }
1194
+ // ============================================================================
1195
+ // Private: Helpers for asymmetric flows
1196
+ // ============================================================================
1197
+ /**
1198
+ * Releases the resources held by an entry being displaced from the vault.
1199
+ * Symmetric entries get their key buffer zeroed in place. Asymmetric entries
1200
+ * have their private-key blob best-effort deleted from
1201
+ * {@link CryptoUtils.KeyStore.IPrivateKeyStorage}; if the storage call fails,
1202
+ * a warning string is returned but the displacement still proceeds — the
1203
+ * orphaned blob is left for consumer-side GC. Without a configured provider,
1204
+ * asymmetric cleanup is silently skipped.
1205
+ * @returns A warning string if storage cleanup failed, otherwise undefined.
1206
+ */
1207
+ async _releaseEntryResources(entry) {
1208
+ if (entry.type === 'asymmetric-keypair') {
1209
+ if (!this._privateKeyStorage) {
1210
+ return undefined;
1211
+ }
1212
+ const deleteResult = await this._privateKeyStorage.delete(entry.id);
1213
+ if (deleteResult.isFailure()) {
1214
+ return `Failed to delete prior storage blob for '${entry.name}' (id ${entry.id}): ${deleteResult.message}`;
1215
+ }
1216
+ return undefined;
1217
+ }
1218
+ entry.key.fill(0);
1219
+ return undefined;
1220
+ }
1221
+ /**
1222
+ * Constant-time byte comparison. Returns false immediately for length
1223
+ * mismatch (length is not secret); for equal-length inputs, walks the full
1224
+ * buffer accumulating differences via XOR so the running time does not leak
1225
+ * the position of the first differing byte.
1226
+ */
1227
+ static _timingSafeEqual(a, b) {
1228
+ /* c8 ignore next 3 - defensive: callers compare equal-length 32-byte PBKDF2 keys */
1229
+ if (a.length !== b.length) {
1230
+ return false;
1231
+ }
1232
+ let diff = 0;
1233
+ for (let i = 0; i < a.length; i++) {
1234
+ // eslint-disable-next-line no-bitwise
1235
+ diff |= a[i] ^ b[i];
1236
+ }
1237
+ return diff === 0;
1238
+ }
1239
+ /**
1240
+ * Mints a fresh UUID v4 storage handle using the crypto provider's
1241
+ * {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
1242
+ * Random-bytes failures propagate as Failure.
1243
+ */
1244
+ _generateId() {
1245
+ return this._cryptoProvider.generateRandomBytes(16).onSuccess((bytes) => {
1246
+ // Per RFC 4122 §4.4: set version (4) and variant (10xx) bits.
1247
+ // eslint-disable-next-line no-bitwise
1248
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1249
+ // eslint-disable-next-line no-bitwise
1250
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1251
+ const hex = Array.from(bytes)
1252
+ .map((b) => b.toString(16).padStart(2, '0'))
1253
+ .join('');
1254
+ return succeed(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`);
1255
+ });
1256
+ }
757
1257
  }
758
1258
  //# sourceMappingURL=keyStore.js.map