@fgv/ts-extras 5.1.0-4 → 5.1.0-41

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 (368) 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 +886 -158
  5. package/dist/packlets/ai-assist/apiClient.js.map +1 -0
  6. package/dist/packlets/ai-assist/chatRequestBuilders.js +282 -0
  7. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  8. package/dist/packlets/ai-assist/converters.js +47 -3
  9. package/dist/packlets/ai-assist/converters.js.map +1 -0
  10. package/dist/packlets/ai-assist/embeddingClient.js +345 -0
  11. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -0
  12. package/dist/packlets/ai-assist/endpoint.js +78 -0
  13. package/dist/packlets/ai-assist/endpoint.js.map +1 -0
  14. package/dist/packlets/ai-assist/http.js +75 -0
  15. package/dist/packlets/ai-assist/http.js.map +1 -0
  16. package/dist/packlets/ai-assist/imageOptionsResolver.js +193 -0
  17. package/dist/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  18. package/dist/packlets/ai-assist/index.js +10 -4
  19. package/dist/packlets/ai-assist/index.js.map +1 -0
  20. package/dist/packlets/ai-assist/jsonCompletion.js +93 -0
  21. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -0
  22. package/dist/packlets/ai-assist/jsonResponse.js +149 -0
  23. package/dist/packlets/ai-assist/jsonResponse.js.map +1 -0
  24. package/dist/packlets/ai-assist/model.js +193 -9
  25. package/dist/packlets/ai-assist/model.js.map +1 -0
  26. package/dist/packlets/ai-assist/registry.js +314 -12
  27. package/dist/packlets/ai-assist/registry.js.map +1 -0
  28. package/dist/packlets/ai-assist/sseParser.js +123 -0
  29. package/dist/packlets/ai-assist/sseParser.js.map +1 -0
  30. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +368 -0
  31. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  32. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +687 -0
  33. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
  34. package/dist/packlets/ai-assist/streamingAdapters/common.js +187 -0
  35. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  36. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +204 -0
  37. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  38. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +170 -0
  39. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  40. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +386 -0
  41. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  42. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +173 -0
  43. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  44. package/dist/packlets/ai-assist/streamingClient.js +140 -0
  45. package/dist/packlets/ai-assist/streamingClient.js.map +1 -0
  46. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +288 -0
  47. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  48. package/dist/packlets/ai-assist/toolFormats.js +106 -10
  49. package/dist/packlets/ai-assist/toolFormats.js.map +1 -0
  50. package/dist/packlets/conversion/converters.js +35 -1
  51. package/dist/packlets/conversion/converters.js.map +1 -0
  52. package/dist/packlets/conversion/index.js.map +1 -0
  53. package/dist/packlets/crypto-utils/constants.js.map +1 -0
  54. package/dist/packlets/crypto-utils/converters.js +42 -4
  55. package/dist/packlets/crypto-utils/converters.js.map +1 -0
  56. package/dist/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  57. package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -0
  58. package/dist/packlets/crypto-utils/hpkeProvider.js +355 -0
  59. package/dist/packlets/crypto-utils/hpkeProvider.js.map +1 -0
  60. package/dist/packlets/crypto-utils/index.browser.js +10 -2
  61. package/dist/packlets/crypto-utils/index.browser.js.map +1 -0
  62. package/dist/packlets/crypto-utils/index.js +6 -0
  63. package/dist/packlets/crypto-utils/index.js.map +1 -0
  64. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +71 -0
  65. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  66. package/dist/packlets/crypto-utils/keystore/converters.js +105 -14
  67. package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -0
  68. package/dist/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js +287 -0
  69. package/dist/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js.map +1 -0
  70. package/dist/packlets/crypto-utils/keystore/index.browser.js +36 -0
  71. package/dist/packlets/crypto-utils/keystore/index.browser.js.map +1 -0
  72. package/dist/packlets/crypto-utils/keystore/index.js +3 -0
  73. package/dist/packlets/crypto-utils/keystore/index.js.map +1 -0
  74. package/dist/packlets/crypto-utils/keystore/keyStore.js +773 -119
  75. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  76. package/dist/packlets/crypto-utils/keystore/model.js +30 -4
  77. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -0
  78. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
  79. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  80. package/dist/packlets/crypto-utils/model.js +32 -0
  81. package/dist/packlets/crypto-utils/model.js.map +1 -0
  82. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +336 -1
  83. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  84. package/dist/packlets/crypto-utils/spkiHelpers.js +240 -0
  85. package/dist/packlets/crypto-utils/spkiHelpers.js.map +1 -0
  86. package/dist/packlets/csv/csvFileHelpers.js +0 -14
  87. package/dist/packlets/csv/csvFileHelpers.js.map +1 -0
  88. package/dist/packlets/csv/csvHelpers.js +14 -0
  89. package/dist/packlets/csv/csvHelpers.js.map +1 -0
  90. package/dist/packlets/csv/index.browser.js +1 -3
  91. package/dist/packlets/csv/index.browser.js.map +1 -0
  92. package/dist/packlets/csv/index.js.map +1 -0
  93. package/dist/packlets/experimental/extendedArray.js.map +1 -0
  94. package/dist/packlets/experimental/formatter.js.map +1 -0
  95. package/dist/packlets/experimental/index.js.map +1 -0
  96. package/dist/packlets/experimental/rangeOf.js.map +1 -0
  97. package/dist/packlets/hash/index.browser.js.map +1 -0
  98. package/dist/packlets/hash/index.js.map +1 -0
  99. package/dist/packlets/hash/index.node.js.map +1 -0
  100. package/dist/packlets/hash/md5Normalizer.browser.js.map +1 -0
  101. package/dist/packlets/hash/md5Normalizer.js.map +1 -0
  102. package/dist/packlets/mustache/index.js.map +1 -0
  103. package/dist/packlets/mustache/interfaces.js.map +1 -0
  104. package/dist/packlets/mustache/mustacheTemplate.js +42 -4
  105. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -0
  106. package/dist/packlets/record-jar/index.browser.js +1 -3
  107. package/dist/packlets/record-jar/index.browser.js.map +1 -0
  108. package/dist/packlets/record-jar/index.js.map +1 -0
  109. package/dist/packlets/record-jar/recordJarFileHelpers.js +0 -18
  110. package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  111. package/dist/packlets/record-jar/recordJarHelpers.js +18 -0
  112. package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -0
  113. package/dist/packlets/yaml/converters.js.map +1 -0
  114. package/dist/packlets/yaml/index.js +1 -0
  115. package/dist/packlets/yaml/index.js.map +1 -0
  116. package/dist/packlets/yaml/serializers.js +48 -0
  117. package/dist/packlets/yaml/serializers.js.map +1 -0
  118. package/dist/packlets/zip-file-tree/index.js.map +1 -0
  119. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  120. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  121. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  122. package/dist/ts-extras.d.ts +4342 -285
  123. package/dist/tsdoc-metadata.json +1 -1
  124. package/lib/index.browser.d.ts +4 -2
  125. package/lib/index.browser.d.ts.map +1 -0
  126. package/lib/index.browser.js +8 -3
  127. package/lib/index.browser.js.map +1 -0
  128. package/lib/index.d.ts.map +1 -0
  129. package/lib/index.js.map +1 -0
  130. package/lib/packlets/ai-assist/apiClient.d.ts +115 -32
  131. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -0
  132. package/lib/packlets/ai-assist/apiClient.js +888 -156
  133. package/lib/packlets/ai-assist/apiClient.js.map +1 -0
  134. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +149 -0
  135. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -0
  136. package/lib/packlets/ai-assist/chatRequestBuilders.js +293 -0
  137. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  138. package/lib/packlets/ai-assist/converters.d.ts +15 -1
  139. package/lib/packlets/ai-assist/converters.d.ts.map +1 -0
  140. package/lib/packlets/ai-assist/converters.js +47 -3
  141. package/lib/packlets/ai-assist/converters.js.map +1 -0
  142. package/lib/packlets/ai-assist/embeddingClient.d.ts +69 -0
  143. package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -0
  144. package/lib/packlets/ai-assist/embeddingClient.js +349 -0
  145. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -0
  146. package/lib/packlets/ai-assist/endpoint.d.ts +28 -0
  147. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
  148. package/lib/packlets/ai-assist/endpoint.js +82 -0
  149. package/lib/packlets/ai-assist/endpoint.js.map +1 -0
  150. package/lib/packlets/ai-assist/http.d.ts +24 -0
  151. package/lib/packlets/ai-assist/http.d.ts.map +1 -0
  152. package/lib/packlets/ai-assist/http.js +78 -0
  153. package/lib/packlets/ai-assist/http.js.map +1 -0
  154. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +66 -0
  155. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -0
  156. package/lib/packlets/ai-assist/imageOptionsResolver.js +197 -0
  157. package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
  158. package/lib/packlets/ai-assist/index.d.ts +10 -4
  159. package/lib/packlets/ai-assist/index.d.ts.map +1 -0
  160. package/lib/packlets/ai-assist/index.js +36 -1
  161. package/lib/packlets/ai-assist/index.js.map +1 -0
  162. package/lib/packlets/ai-assist/jsonCompletion.d.ts +93 -0
  163. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -0
  164. package/lib/packlets/ai-assist/jsonCompletion.js +97 -0
  165. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -0
  166. package/lib/packlets/ai-assist/jsonResponse.d.ts +91 -0
  167. package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -0
  168. package/lib/packlets/ai-assist/jsonResponse.js +154 -0
  169. package/lib/packlets/ai-assist/jsonResponse.js.map +1 -0
  170. package/lib/packlets/ai-assist/model.d.ts +1202 -12
  171. package/lib/packlets/ai-assist/model.d.ts.map +1 -0
  172. package/lib/packlets/ai-assist/model.js +198 -10
  173. package/lib/packlets/ai-assist/model.js.map +1 -0
  174. package/lib/packlets/ai-assist/registry.d.ts +56 -1
  175. package/lib/packlets/ai-assist/registry.d.ts.map +1 -0
  176. package/lib/packlets/ai-assist/registry.js +319 -13
  177. package/lib/packlets/ai-assist/registry.js.map +1 -0
  178. package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
  179. package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -0
  180. package/lib/packlets/ai-assist/sseParser.js +128 -0
  181. package/lib/packlets/ai-assist/sseParser.js.map +1 -0
  182. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +72 -0
  183. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -0
  184. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +371 -0
  185. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  186. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +260 -0
  187. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -0
  188. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +694 -0
  189. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
  190. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +154 -0
  191. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -0
  192. package/lib/packlets/ai-assist/streamingAdapters/common.js +193 -0
  193. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  194. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +43 -0
  195. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -0
  196. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +207 -0
  197. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  198. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +19 -0
  199. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -0
  200. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +173 -0
  201. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  202. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +33 -0
  203. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -0
  204. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +389 -0
  205. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  206. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
  207. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -0
  208. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +176 -0
  209. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  210. package/lib/packlets/ai-assist/streamingClient.d.ts +50 -0
  211. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -0
  212. package/lib/packlets/ai-assist/streamingClient.js +146 -0
  213. package/lib/packlets/ai-assist/streamingClient.js.map +1 -0
  214. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +87 -0
  215. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -0
  216. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +294 -0
  217. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
  218. package/lib/packlets/ai-assist/toolFormats.d.ts +40 -9
  219. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -0
  220. package/lib/packlets/ai-assist/toolFormats.js +107 -10
  221. package/lib/packlets/ai-assist/toolFormats.js.map +1 -0
  222. package/lib/packlets/conversion/converters.d.ts +8 -1
  223. package/lib/packlets/conversion/converters.d.ts.map +1 -0
  224. package/lib/packlets/conversion/converters.js +36 -2
  225. package/lib/packlets/conversion/converters.js.map +1 -0
  226. package/lib/packlets/conversion/index.d.ts.map +1 -0
  227. package/lib/packlets/conversion/index.js.map +1 -0
  228. package/lib/packlets/crypto-utils/constants.d.ts.map +1 -0
  229. package/lib/packlets/crypto-utils/constants.js.map +1 -0
  230. package/lib/packlets/crypto-utils/converters.d.ts +20 -1
  231. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -0
  232. package/lib/packlets/crypto-utils/converters.js +43 -5
  233. package/lib/packlets/crypto-utils/converters.js.map +1 -0
  234. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts.map +1 -0
  235. package/lib/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  236. package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -0
  237. package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -0
  238. package/lib/packlets/crypto-utils/hpkeProvider.d.ts +150 -0
  239. package/lib/packlets/crypto-utils/hpkeProvider.d.ts.map +1 -0
  240. package/lib/packlets/crypto-utils/hpkeProvider.js +359 -0
  241. package/lib/packlets/crypto-utils/hpkeProvider.js.map +1 -0
  242. package/lib/packlets/crypto-utils/index.browser.d.ts +4 -1
  243. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -0
  244. package/lib/packlets/crypto-utils/index.browser.js +22 -3
  245. package/lib/packlets/crypto-utils/index.browser.js.map +1 -0
  246. package/lib/packlets/crypto-utils/index.d.ts +3 -0
  247. package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
  248. package/lib/packlets/crypto-utils/index.js +18 -1
  249. package/lib/packlets/crypto-utils/index.js.map +1 -0
  250. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +54 -0
  251. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -0
  252. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +74 -0
  253. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  254. package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
  255. package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -0
  256. package/lib/packlets/crypto-utils/keystore/converters.js +103 -12
  257. package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -0
  258. package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.d.ts +148 -0
  259. package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.d.ts.map +1 -0
  260. package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js +324 -0
  261. package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js.map +1 -0
  262. package/lib/packlets/crypto-utils/keystore/index.browser.d.ts +10 -0
  263. package/lib/packlets/crypto-utils/keystore/index.browser.d.ts.map +1 -0
  264. package/lib/packlets/crypto-utils/keystore/index.browser.js +76 -0
  265. package/lib/packlets/crypto-utils/keystore/index.browser.js.map +1 -0
  266. package/lib/packlets/crypto-utils/keystore/index.d.ts +2 -0
  267. package/lib/packlets/crypto-utils/keystore/index.d.ts.map +1 -0
  268. package/lib/packlets/crypto-utils/keystore/index.js +5 -1
  269. package/lib/packlets/crypto-utils/keystore/index.js.map +1 -0
  270. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +270 -13
  271. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -0
  272. package/lib/packlets/crypto-utils/keystore/keyStore.js +778 -124
  273. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  274. package/lib/packlets/crypto-utils/keystore/model.d.ts +350 -21
  275. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -0
  276. package/lib/packlets/crypto-utils/keystore/model.js +32 -5
  277. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -0
  278. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +53 -0
  279. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts.map +1 -0
  280. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
  281. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  282. package/lib/packlets/crypto-utils/model.d.ts +437 -10
  283. package/lib/packlets/crypto-utils/model.d.ts.map +1 -0
  284. package/lib/packlets/crypto-utils/model.js +33 -1
  285. package/lib/packlets/crypto-utils/model.js.map +1 -0
  286. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +134 -2
  287. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -0
  288. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +335 -0
  289. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  290. package/lib/packlets/crypto-utils/spkiHelpers.d.ts +125 -0
  291. package/lib/packlets/crypto-utils/spkiHelpers.d.ts.map +1 -0
  292. package/lib/packlets/crypto-utils/spkiHelpers.js +251 -0
  293. package/lib/packlets/crypto-utils/spkiHelpers.js.map +1 -0
  294. package/lib/packlets/csv/csvFileHelpers.d.ts +0 -10
  295. package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -0
  296. package/lib/packlets/csv/csvFileHelpers.js +0 -15
  297. package/lib/packlets/csv/csvFileHelpers.js.map +1 -0
  298. package/lib/packlets/csv/csvHelpers.d.ts +10 -0
  299. package/lib/packlets/csv/csvHelpers.d.ts.map +1 -0
  300. package/lib/packlets/csv/csvHelpers.js +15 -0
  301. package/lib/packlets/csv/csvHelpers.js.map +1 -0
  302. package/lib/packlets/csv/index.browser.d.ts +0 -1
  303. package/lib/packlets/csv/index.browser.d.ts.map +1 -0
  304. package/lib/packlets/csv/index.browser.js +1 -5
  305. package/lib/packlets/csv/index.browser.js.map +1 -0
  306. package/lib/packlets/csv/index.d.ts.map +1 -0
  307. package/lib/packlets/csv/index.js.map +1 -0
  308. package/lib/packlets/experimental/extendedArray.d.ts.map +1 -0
  309. package/lib/packlets/experimental/extendedArray.js.map +1 -0
  310. package/lib/packlets/experimental/formatter.d.ts.map +1 -0
  311. package/lib/packlets/experimental/formatter.js.map +1 -0
  312. package/lib/packlets/experimental/index.d.ts.map +1 -0
  313. package/lib/packlets/experimental/index.js.map +1 -0
  314. package/lib/packlets/experimental/rangeOf.d.ts.map +1 -0
  315. package/lib/packlets/experimental/rangeOf.js.map +1 -0
  316. package/lib/packlets/hash/index.browser.d.ts.map +1 -0
  317. package/lib/packlets/hash/index.browser.js.map +1 -0
  318. package/lib/packlets/hash/index.d.ts.map +1 -0
  319. package/lib/packlets/hash/index.js.map +1 -0
  320. package/lib/packlets/hash/index.node.d.ts.map +1 -0
  321. package/lib/packlets/hash/index.node.js.map +1 -0
  322. package/lib/packlets/hash/md5Normalizer.browser.d.ts.map +1 -0
  323. package/lib/packlets/hash/md5Normalizer.browser.js.map +1 -0
  324. package/lib/packlets/hash/md5Normalizer.d.ts.map +1 -0
  325. package/lib/packlets/hash/md5Normalizer.js.map +1 -0
  326. package/lib/packlets/mustache/index.d.ts +1 -1
  327. package/lib/packlets/mustache/index.d.ts.map +1 -0
  328. package/lib/packlets/mustache/index.js.map +1 -0
  329. package/lib/packlets/mustache/interfaces.d.ts +34 -0
  330. package/lib/packlets/mustache/interfaces.d.ts.map +1 -0
  331. package/lib/packlets/mustache/interfaces.js.map +1 -0
  332. package/lib/packlets/mustache/mustacheTemplate.d.ts +2 -0
  333. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -0
  334. package/lib/packlets/mustache/mustacheTemplate.js +42 -4
  335. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -0
  336. package/lib/packlets/record-jar/index.browser.d.ts +0 -1
  337. package/lib/packlets/record-jar/index.browser.d.ts.map +1 -0
  338. package/lib/packlets/record-jar/index.browser.js +1 -5
  339. package/lib/packlets/record-jar/index.browser.js.map +1 -0
  340. package/lib/packlets/record-jar/index.d.ts.map +1 -0
  341. package/lib/packlets/record-jar/index.js.map +1 -0
  342. package/lib/packlets/record-jar/recordJarFileHelpers.d.ts +0 -11
  343. package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -0
  344. package/lib/packlets/record-jar/recordJarFileHelpers.js +0 -19
  345. package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  346. package/lib/packlets/record-jar/recordJarHelpers.d.ts +11 -0
  347. package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -0
  348. package/lib/packlets/record-jar/recordJarHelpers.js +19 -0
  349. package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -0
  350. package/lib/packlets/yaml/converters.d.ts.map +1 -0
  351. package/lib/packlets/yaml/converters.js.map +1 -0
  352. package/lib/packlets/yaml/index.d.ts +1 -0
  353. package/lib/packlets/yaml/index.d.ts.map +1 -0
  354. package/lib/packlets/yaml/index.js +1 -0
  355. package/lib/packlets/yaml/index.js.map +1 -0
  356. package/lib/packlets/yaml/serializers.d.ts +45 -0
  357. package/lib/packlets/yaml/serializers.d.ts.map +1 -0
  358. package/lib/packlets/yaml/serializers.js +84 -0
  359. package/lib/packlets/yaml/serializers.js.map +1 -0
  360. package/lib/packlets/zip-file-tree/index.d.ts.map +1 -0
  361. package/lib/packlets/zip-file-tree/index.js.map +1 -0
  362. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
  363. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -0
  364. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  365. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  366. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -0
  367. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  368. package/package.json +20 -15
@@ -56,7 +56,9 @@ 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 keyPairAlgorithmParams_1 = require("../keyPairAlgorithmParams");
60
+ const model_1 = require("../model");
61
+ const model_2 = require("./model");
60
62
  const converters_1 = require("./converters");
61
63
  /**
62
64
  * Gets the current ISO timestamp.
@@ -64,6 +66,11 @@ const converters_1 = require("./converters");
64
66
  function getCurrentTimestamp() {
65
67
  return new Date().toISOString();
66
68
  }
69
+ // WebCrypto key usages that only ever apply to a public key. Filtering these
70
+ // out of the keypair usages yields the usages valid for the private half, which
71
+ // `crypto.subtle.importKey` requires when re-importing a private JWK. Mirrors
72
+ // the same list in `EncryptedFilePrivateKeyStorage`.
73
+ const PUBLIC_ONLY_USAGES = ['verify', 'encrypt', 'wrapKey'];
67
74
  // ============================================================================
68
75
  // KeyStore Class
69
76
  // ============================================================================
@@ -96,11 +103,26 @@ function getCurrentTimestamp() {
96
103
  * const encryptionConfig = keystore2.getEncryptionConfig().orThrow();
97
104
  * ```
98
105
  *
106
+ * @remarks
107
+ * SECURITY — private-key escrow. By default an asymmetric keypair's private
108
+ * key lives only in the per-device {@link CryptoUtils.KeyStore.IPrivateKeyStorage}
109
+ * backend; the vault carries only the public JWK, so a vault recovered on a new
110
+ * device reconstitutes with no private keys (a lost device is a lost identity).
111
+ * `addKeyPair(name, { escrow: true })` opts into carrying an encrypted private-key
112
+ * copy inside the vault ciphertext, so the vault file plus the master password can
113
+ * recover the identity on a fresh device via `getKeyPair(name, { rehydrate: true })`.
114
+ *
115
+ * When escrow is used, **the master password becomes the sole gate** protecting a
116
+ * private signing/decryption key that was previously unrecoverable from the vault.
117
+ * Configure escrow-bearing stores with a strong KDF — an Argon2id-derived master
118
+ * key or a high-iteration PBKDF2 count — and treat the vault file accordingly.
119
+ *
99
120
  * @public
100
121
  */
101
122
  class KeyStore {
102
- constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
123
+ constructor(cryptoProvider, iterations, keystoreFile, isNew, privateKeyStorage) {
103
124
  this._cryptoProvider = cryptoProvider;
125
+ this._privateKeyStorage = privateKeyStorage;
104
126
  this._iterations = iterations;
105
127
  this._keystoreFile = keystoreFile;
106
128
  this._state = 'locked';
@@ -119,11 +141,11 @@ class KeyStore {
119
141
  */
120
142
  static create(params) {
121
143
  var _a;
122
- const iterations = (_a = params.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_KEYSTORE_ITERATIONS;
144
+ const iterations = (_a = params.iterations) !== null && _a !== void 0 ? _a : model_2.DEFAULT_KEYSTORE_ITERATIONS;
123
145
  if (iterations < 1) {
124
146
  return (0, ts_utils_1.fail)('Iterations must be at least 1');
125
147
  }
126
- return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true));
148
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true, params.privateKeyStorage));
127
149
  }
128
150
  /**
129
151
  * Opens an existing encrypted key store.
@@ -139,7 +161,7 @@ class KeyStore {
139
161
  return (0, ts_utils_1.fail)(`Invalid key store file: ${fileResult.message}`);
140
162
  }
141
163
  const iterations = fileResult.value.keyDerivation.iterations;
142
- return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
164
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false, params.privateKeyStorage));
143
165
  }
144
166
  // ============================================================================
145
167
  // Lifecycle Methods
@@ -163,7 +185,7 @@ class KeyStore {
163
185
  return (0, ts_utils_1.fail)('Password cannot be empty');
164
186
  }
165
187
  // Generate salt for this key store using crypto provider
166
- const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
188
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
167
189
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
168
190
  if (saltResult.isFailure()) {
169
191
  return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
@@ -182,7 +204,6 @@ class KeyStore {
182
204
  * @public
183
205
  */
184
206
  async unlock(password) {
185
- var _a;
186
207
  if (this._isNew) {
187
208
  return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
188
209
  }
@@ -206,57 +227,37 @@ class KeyStore {
206
227
  if (keyResult.isFailure()) {
207
228
  return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
208
229
  }
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');
230
+ return this._decryptVault(keyResult.value);
231
+ }
232
+ /**
233
+ * Unlocks an existing key store with a pre-derived key, bypassing
234
+ * PBKDF2 key derivation. Use this when the derived key has been
235
+ * stored externally (e.g., in another key store) and the original
236
+ * password is no longer available.
237
+ *
238
+ * The supplied key must have been derived from the correct password
239
+ * using the key store file's own PBKDF2 parameters (salt and
240
+ * iteration count).
241
+ *
242
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
243
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
244
+ * @public
245
+ */
246
+ async unlockWithKey(derivedKey) {
247
+ if (this._isNew) {
248
+ return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
226
249
  }
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}`);
250
+ if (this._state === 'unlocked') {
251
+ return (0, ts_utils_1.fail)('Key store is already unlocked');
232
252
  }
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}`);
253
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
254
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
237
255
  }
238
- // Load secrets into memory
239
- this._salt = salt;
240
- this._secrets = new Map();
241
- for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
242
- const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
243
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
244
- if (keyBytesResult.isFailure()) {
245
- return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
246
- }
247
- const entry = {
248
- name,
249
- /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
250
- type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
251
- key: keyBytesResult.value,
252
- description: jsonEntry.description,
253
- createdAt: jsonEntry.createdAt
254
- };
255
- this._secrets.set(name, entry);
256
+ /* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
257
+ if (!this._keystoreFile) {
258
+ return (0, ts_utils_1.fail)('No key store file to unlock');
256
259
  }
257
- this._state = 'unlocked';
258
- this._dirty = false;
259
- return (0, ts_utils_1.succeed)(this);
260
+ return this._decryptVault(derivedKey);
260
261
  }
261
262
  /**
262
263
  * Locks the key store, clearing all secrets from memory.
@@ -274,7 +275,9 @@ class KeyStore {
274
275
  // Clear secrets from memory (overwrite for security)
275
276
  if (this._secrets) {
276
277
  for (const entry of this._secrets.values()) {
277
- entry.key.fill(0);
278
+ if (entry.type !== 'asymmetric-keypair') {
279
+ entry.key.fill(0);
280
+ }
278
281
  }
279
282
  this._secrets.clear();
280
283
  this._secrets = undefined;
@@ -336,7 +339,15 @@ class KeyStore {
336
339
  return (0, ts_utils_1.succeed)(Array.from(this._secrets.keys()));
337
340
  }
338
341
  /**
339
- * Gets a secret by name.
342
+ * Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
343
+ * — callers must check `entry.type` before accessing `key`/`id` since asymmetric
344
+ * entries carry no raw key material.
345
+ *
346
+ * SECURITY: for an escrow-enabled asymmetric-keypair entry the returned object
347
+ * carries `escrowedPrivateKeyJwk` in cleartext (same custody class as
348
+ * `publicKeyJwk`, gated by the same unlock) — do not log or serialize a
349
+ * `getSecret()` result casually.
350
+ *
340
351
  * @param name - Name of the secret
341
352
  * @returns Success with secret entry, Failure if not found or locked
342
353
  * @public
@@ -351,6 +362,27 @@ class KeyStore {
351
362
  }
352
363
  return (0, ts_utils_1.succeed)(entry);
353
364
  }
365
+ /**
366
+ * Returns the public-key JWK for an asymmetric-keypair entry.
367
+ * Available without {@link CryptoUtils.KeyStore.IPrivateKeyStorage} since the
368
+ * public key lives in the vault metadata directly.
369
+ * @param name - Name of the entry
370
+ * @returns Success with the JWK, Failure if not found, locked, or wrong type
371
+ * @public
372
+ */
373
+ getPublicKeyJwk(name) {
374
+ if (!this._secrets) {
375
+ return (0, ts_utils_1.fail)('Key store is locked');
376
+ }
377
+ const entry = this._secrets.get(name);
378
+ if (!entry) {
379
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
380
+ }
381
+ if (entry.type !== 'asymmetric-keypair') {
382
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
383
+ }
384
+ return (0, ts_utils_1.succeed)(entry.publicKeyJwk);
385
+ }
354
386
  /**
355
387
  * Checks if a secret exists.
356
388
  * @param name - Name of the secret
@@ -377,7 +409,6 @@ class KeyStore {
377
409
  if (!name || name.length === 0) {
378
410
  return (0, ts_utils_1.fail)('Secret name cannot be empty');
379
411
  }
380
- const replaced = this._secrets.has(name);
381
412
  // Generate a new random key
382
413
  const keyResult = await this._cryptoProvider.generateKey();
383
414
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
@@ -391,19 +422,28 @@ class KeyStore {
391
422
  description: options === null || options === void 0 ? void 0 : options.description,
392
423
  createdAt: getCurrentTimestamp()
393
424
  };
425
+ const existing = this._secrets.get(name);
426
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
394
427
  this._secrets.set(name, entry);
395
428
  this._dirty = true;
396
- return (0, ts_utils_1.succeed)({ entry, replaced });
429
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
397
430
  }
398
431
  /**
399
- * Imports an existing secret key.
432
+ * Imports raw 32-byte key material into the vault.
433
+ *
434
+ * Always validates that the key is exactly 32 bytes (AES-256). The optional
435
+ * `type` field is a classification label stored with the entry; it does not
436
+ * change the validation rules. For importing UTF-8 API key strings (variable
437
+ * length), use {@link KeyStore.importApiKey} instead.
438
+ *
400
439
  * @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
440
+ * @param key - The 32-byte AES-256 key material
441
+ * @param options - Optional type classification, description, whether to replace existing
403
442
  * @returns Success with entry, Failure if locked, key invalid, or exists and !replace
404
443
  * @public
405
444
  */
406
- importSecret(name, key, options) {
445
+ async importSecret(name, key, options) {
446
+ var _a;
407
447
  if (!this._secrets) {
408
448
  return (0, ts_utils_1.fail)('Key store is locked');
409
449
  }
@@ -413,20 +453,21 @@ class KeyStore {
413
453
  if (key.length !== Constants.AES_256_KEY_SIZE) {
414
454
  return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
415
455
  }
416
- const exists = this._secrets.has(name);
417
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
456
+ const existing = this._secrets.get(name);
457
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
418
458
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
419
459
  }
420
460
  const entry = {
421
461
  name,
422
- type: 'encryption-key',
462
+ type: (_a = options === null || options === void 0 ? void 0 : options.type) !== null && _a !== void 0 ? _a : 'encryption-key',
423
463
  key: new Uint8Array(key), // Copy to prevent external modification
424
464
  description: options === null || options === void 0 ? void 0 : options.description,
425
465
  createdAt: getCurrentTimestamp()
426
466
  };
467
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
427
468
  this._secrets.set(name, entry);
428
469
  this._dirty = true;
429
- return (0, ts_utils_1.succeed)({ entry, replaced: exists });
470
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
430
471
  }
431
472
  /**
432
473
  * Adds a secret derived from a password using PBKDF2.
@@ -453,13 +494,13 @@ class KeyStore {
453
494
  if (!password || password.length === 0) {
454
495
  return (0, ts_utils_1.fail)('Password cannot be empty');
455
496
  }
456
- const exists = this._secrets.has(name);
457
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
497
+ const existing = this._secrets.get(name);
498
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
458
499
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
459
500
  }
460
- const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_SECRET_ITERATIONS;
501
+ const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_2.DEFAULT_SECRET_ITERATIONS;
461
502
  // Generate a random salt for this secret's key derivation
462
- const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
503
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
463
504
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
464
505
  if (saltResult.isFailure()) {
465
506
  return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
@@ -477,11 +518,13 @@ class KeyStore {
477
518
  description: options === null || options === void 0 ? void 0 : options.description,
478
519
  createdAt: getCurrentTimestamp()
479
520
  };
521
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
480
522
  this._secrets.set(name, entry);
481
523
  this._dirty = true;
482
524
  return (0, ts_utils_1.succeed)({
483
525
  entry,
484
- replaced: exists,
526
+ replaced: existing !== undefined,
527
+ warning,
485
528
  keyDerivation: {
486
529
  kdf: 'pbkdf2',
487
530
  salt: this._cryptoProvider.toBase64(saltResult.value),
@@ -490,12 +533,183 @@ class KeyStore {
490
533
  });
491
534
  }
492
535
  /**
493
- * Removes a secret by name.
536
+ * Verifies that a candidate password derives the same key material currently
537
+ * stored under `name`, using the supplied
538
+ * {@link CryptoUtils.IKeyDerivationParams | key derivation parameters}.
539
+ *
540
+ * The keystore does not persist per-slot key derivation parameters with the
541
+ * entry — callers receive them from `addSecretFromPassword` and store them
542
+ * alongside the encrypted artifact (or wherever else makes sense). Pass
543
+ * those same parameters here for verification.
544
+ *
545
+ * Re-derives a key from `password` + `keyDerivation`, then compares it to
546
+ * the stored key material in constant time. Restricted to entries of type
547
+ * `'encryption-key'` — the type produced by `addSecretFromPassword`. Other
548
+ * symmetric types (`'api-key'`) and asymmetric entries are rejected so
549
+ * the boolean result reflects "this slot accepts this password" rather
550
+ * than an incidental byte-equality match against unrelated material.
551
+ *
552
+ * Note: the keystore does not currently flag whether an `'encryption-key'`
553
+ * entry was actually password-derived (vs. random via `addSecret` or raw
554
+ * via `importSecret`). A `true` result therefore means "the candidate
555
+ * password produces the same 32 bytes currently stored", which is what
556
+ * the equivalent consumer-side helper (`verifyGatePassword`) already
557
+ * implies for entries it manages.
558
+ *
559
+ * @param name - Name of the secret to verify against
560
+ * @param password - Candidate password to test
561
+ * @param keyDerivation - The key derivation parameters returned by
562
+ * `addSecretFromPassword` when the secret was created. Only
563
+ * `kdf: 'pbkdf2'` is supported.
564
+ * @returns Success(true) when the candidate matches the stored key,
565
+ * Success(false) when it does not, Failure if locked, secret missing,
566
+ * wrong type, unsupported `kdf`, or key derivation fails
567
+ * @public
568
+ */
569
+ async verifySecretFromPassword(name, password, keyDerivation) {
570
+ if (!this._secrets) {
571
+ return (0, ts_utils_1.fail)('Key store is locked');
572
+ }
573
+ if (!password || password.length === 0) {
574
+ return (0, ts_utils_1.fail)('Password cannot be empty');
575
+ }
576
+ if (keyDerivation.kdf !== 'pbkdf2') {
577
+ return (0, ts_utils_1.fail)(`Unsupported kdf '${keyDerivation.kdf}' (expected 'pbkdf2')`);
578
+ }
579
+ const entry = this._secrets.get(name);
580
+ if (!entry) {
581
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
582
+ }
583
+ if (entry.type !== 'encryption-key') {
584
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
585
+ }
586
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
587
+ if (saltResult.isFailure()) {
588
+ return (0, ts_utils_1.fail)(`Invalid salt: ${saltResult.message}`);
589
+ }
590
+ const derivedResult = await this._cryptoProvider.deriveKey(password, saltResult.value, keyDerivation.iterations);
591
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests */
592
+ if (derivedResult.isFailure()) {
593
+ return (0, ts_utils_1.fail)(`Key derivation failed: ${derivedResult.message}`);
594
+ }
595
+ return (0, ts_utils_1.succeed)(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
596
+ }
597
+ /**
598
+ * Adds a secret derived from a password using Argon2id (RFC 9106).
599
+ *
600
+ * The Argon2id provider must be supplied explicitly; the KeyStore does not
601
+ * hold one by default (consumers opt in by depending on the argon2 package).
602
+ *
603
+ * Returns the key derivation parameters so callers can store them alongside
604
+ * encrypted artifacts, enabling future re-derivation and verification.
605
+ *
606
+ * @param name - Unique name for the secret
607
+ * @param password - Password or passphrase
608
+ * @param argon2idProvider - Argon2id provider (Node or Browser implementation)
609
+ * @param options - Optional: Argon2id params (defaults to ARGON2ID_OWASP_MIN), description, replace flag
610
+ * @returns Success with entry and keyDerivation params, Failure if locked or invalid
611
+ * @public
612
+ */
613
+ async addSecretFromPasswordArgon2id(name, password, argon2idProvider, options) {
614
+ var _a;
615
+ if (!this._secrets) {
616
+ return (0, ts_utils_1.fail)('Key store is locked');
617
+ }
618
+ if (!name || name.length === 0) {
619
+ return (0, ts_utils_1.fail)('Secret name cannot be empty');
620
+ }
621
+ if (!password || password.length === 0) {
622
+ return (0, ts_utils_1.fail)('Password cannot be empty');
623
+ }
624
+ const existing = this._secrets.get(name);
625
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
626
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
627
+ }
628
+ const params = (_a = options === null || options === void 0 ? void 0 : options.params) !== null && _a !== void 0 ? _a : model_1.ARGON2ID_OWASP_MIN;
629
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
630
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
631
+ if (saltResult.isFailure()) {
632
+ return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
633
+ }
634
+ const keyResult = await argon2idProvider.argon2id(password, saltResult.value, params);
635
+ if (keyResult.isFailure()) {
636
+ return (0, ts_utils_1.fail)(`Argon2id key derivation failed: ${keyResult.message}`);
637
+ }
638
+ if (keyResult.value.length !== Constants.AES_256_KEY_SIZE) {
639
+ return (0, ts_utils_1.fail)(`Argon2id outputBytes must be ${Constants.AES_256_KEY_SIZE} for KeyStore secrets, got ${keyResult.value.length}`);
640
+ }
641
+ const entry = {
642
+ name,
643
+ type: 'encryption-key',
644
+ key: keyResult.value,
645
+ description: options === null || options === void 0 ? void 0 : options.description,
646
+ createdAt: getCurrentTimestamp()
647
+ };
648
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
649
+ this._secrets.set(name, entry);
650
+ this._dirty = true;
651
+ const keyDerivation = {
652
+ kdf: 'argon2id',
653
+ salt: this._cryptoProvider.toBase64(saltResult.value),
654
+ memoryKiB: params.memoryKiB,
655
+ iterations: params.iterations,
656
+ parallelism: params.parallelism
657
+ };
658
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning, keyDerivation });
659
+ }
660
+ /**
661
+ * Verifies a candidate password against an Argon2id-derived entry using the
662
+ * supplied key derivation parameters. Constant-time comparison.
663
+ *
664
+ * @param name - Name of the secret to verify against
665
+ * @param password - Candidate password to test
666
+ * @param argon2idProvider - Argon2id provider (must produce bit-identical output for identical inputs)
667
+ * @param keyDerivation - The Argon2id key derivation parameters returned by `addSecretFromPasswordArgon2id`
668
+ * @returns Success(true) if candidate matches stored key, Success(false) if not,
669
+ * Failure if locked, secret missing, wrong type, or derivation fails
670
+ * @public
671
+ */
672
+ async verifySecretFromPasswordArgon2id(name, password, argon2idProvider, keyDerivation) {
673
+ if (!this._secrets) {
674
+ return (0, ts_utils_1.fail)('Key store is locked');
675
+ }
676
+ if (!password || password.length === 0) {
677
+ return (0, ts_utils_1.fail)('Password cannot be empty');
678
+ }
679
+ const entry = this._secrets.get(name);
680
+ if (!entry) {
681
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
682
+ }
683
+ if (entry.type !== 'encryption-key') {
684
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
685
+ }
686
+ const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
687
+ if (saltResult.isFailure()) {
688
+ return (0, ts_utils_1.fail)(`Invalid salt: ${saltResult.message}`);
689
+ }
690
+ const params = {
691
+ memoryKiB: keyDerivation.memoryKiB,
692
+ iterations: keyDerivation.iterations,
693
+ parallelism: keyDerivation.parallelism,
694
+ outputBytes: entry.key.length
695
+ };
696
+ const derivedResult = await argon2idProvider.argon2id(password, saltResult.value, params);
697
+ if (derivedResult.isFailure()) {
698
+ return (0, ts_utils_1.fail)(`Argon2id key derivation failed: ${derivedResult.message}`);
699
+ }
700
+ return (0, ts_utils_1.succeed)(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
701
+ }
702
+ /**
703
+ * Removes a secret by name. Vault-first: the in-memory vault entry is dropped
704
+ * before any storage cleanup runs. For asymmetric-keypair entries, best-effort
705
+ * calls {@link CryptoUtils.KeyStore.IPrivateKeyStorage}.delete on the entry's
706
+ * `id`; a failure is reported via `warning` on the result but does not roll
707
+ * back the vault removal.
494
708
  * @param name - Name of the secret to remove
495
- * @returns Success with removed entry, Failure if not found or locked
709
+ * @returns Success with removed entry (and optional warning), Failure if not found or locked
496
710
  * @public
497
711
  */
498
- removeSecret(name) {
712
+ async removeSecret(name) {
499
713
  if (!this._secrets) {
500
714
  return (0, ts_utils_1.fail)('Key store is locked');
501
715
  }
@@ -503,11 +717,12 @@ class KeyStore {
503
717
  if (!entry) {
504
718
  return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
505
719
  }
506
- // Clear the key before removing (security)
507
- entry.key.fill(0);
720
+ // Vault-first: drop the in-memory entry before touching storage so a
721
+ // storage failure cannot block removal.
508
722
  this._secrets.delete(name);
509
723
  this._dirty = true;
510
- return (0, ts_utils_1.succeed)(entry);
724
+ const warning = await this._releaseEntryResources(entry);
725
+ return (0, ts_utils_1.succeed)({ entry, warning });
511
726
  }
512
727
  /**
513
728
  * Imports an API key string into the vault.
@@ -518,7 +733,7 @@ class KeyStore {
518
733
  * @returns Success with entry, Failure if locked, empty, or exists and !replace
519
734
  * @public
520
735
  */
521
- importApiKey(name, apiKey, options) {
736
+ async importApiKey(name, apiKey, options) {
522
737
  if (!this._secrets) {
523
738
  return (0, ts_utils_1.fail)('Key store is locked');
524
739
  }
@@ -528,8 +743,8 @@ class KeyStore {
528
743
  if (!apiKey || apiKey.length === 0) {
529
744
  return (0, ts_utils_1.fail)('API key cannot be empty');
530
745
  }
531
- const exists = this._secrets.has(name);
532
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
746
+ const existing = this._secrets.get(name);
747
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
533
748
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
534
749
  }
535
750
  const encoder = new TextEncoder();
@@ -540,9 +755,10 @@ class KeyStore {
540
755
  description: options === null || options === void 0 ? void 0 : options.description,
541
756
  createdAt: getCurrentTimestamp()
542
757
  };
758
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
543
759
  this._secrets.set(name, entry);
544
760
  this._dirty = true;
545
- return (0, ts_utils_1.succeed)({ entry, replaced: exists });
761
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
546
762
  }
547
763
  /**
548
764
  * Retrieves an API key string by name.
@@ -565,6 +781,174 @@ class KeyStore {
565
781
  const decoder = new TextDecoder();
566
782
  return (0, ts_utils_1.succeed)(decoder.decode(entry.key));
567
783
  }
784
+ // ============================================================================
785
+ // Asymmetric Keypair Management
786
+ // ============================================================================
787
+ /**
788
+ * Adds a new asymmetric keypair to the vault. Storage-first: the private key
789
+ * is stored under a freshly-minted `id` before the public-key vault entry is
790
+ * committed. If the storage call fails, no vault entry is written and the
791
+ * operation returns Failure.
792
+ *
793
+ * When `replace: true` displaces an existing entry (asymmetric or symmetric),
794
+ * a fresh `id` is minted; the displaced entry's resources are released
795
+ * best-effort. Failure of the storage delete is reported via `warning` on the
796
+ * result but does not roll back the replacement.
797
+ *
798
+ * Requires a {@link CryptoUtils.KeyStore.IPrivateKeyStorage} backend
799
+ * supplied at construction.
800
+ *
801
+ * Extractability of the generated private key defaults to the storage
802
+ * backend's {@link CryptoUtils.KeyStore.IPrivateKeyStorage.supportsNonExtractable}.
803
+ * `options.extractable` overrides that default for this key only; requesting
804
+ * `extractable: false` against a backend that cannot store non-extractable
805
+ * keys fails loudly rather than silently generating an extractable key.
806
+ *
807
+ * @param name - Unique name for the entry
808
+ * @param options - Algorithm, optional description, replace flag, extractable override
809
+ * @returns Success with the new entry, Failure if locked, no provider, storage write failed,
810
+ * or the requested extractability cannot be honored by the backend
811
+ * @public
812
+ */
813
+ async addKeyPair(name, options) {
814
+ var _a;
815
+ if (!this._secrets) {
816
+ return (0, ts_utils_1.fail)('Key store is locked');
817
+ }
818
+ if (!name || name.length === 0) {
819
+ return (0, ts_utils_1.fail)('Entry name cannot be empty');
820
+ }
821
+ if (!this._privateKeyStorage) {
822
+ return (0, ts_utils_1.fail)('No private key storage configured');
823
+ }
824
+ const existing = this._secrets.get(name);
825
+ if (existing && !options.replace) {
826
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
827
+ }
828
+ // Generate the keypair before touching storage. extractable=true on backends
829
+ // that round-trip via JWK; extractable=false on backends that hold CryptoKey
830
+ // refs directly. `options.extractable` overrides the backend-derived default
831
+ // for this key only; a caller-requested non-extractable key against a
832
+ // backend that cannot store one fails loudly rather than silently
833
+ // upgrading to extractable.
834
+ const backendExtractable = !this._privateKeyStorage.supportsNonExtractable;
835
+ if (options.extractable === false && backendExtractable) {
836
+ return (0, ts_utils_1.fail)(`Cannot create non-extractable keypair for '${name}': storage backend does not support non-extractable keys`);
837
+ }
838
+ // Extractability of the LIVE stored key. Escrow is orthogonal — it never
839
+ // changes this; the escrow copy is captured separately from a transient
840
+ // extractable key.
841
+ const liveExtractable = (_a = options.extractable) !== null && _a !== void 0 ? _a : backendExtractable;
842
+ // For escrow we must be able to export the private key to JWK, which only
843
+ // works on an extractable key, so the transient keypair is generated
844
+ // extractable regardless of `liveExtractable`. Without escrow it is
845
+ // generated directly at the live setting.
846
+ const generateExtractable = options.escrow === true ? true : liveExtractable;
847
+ const keyPairResult = await this._cryptoProvider.generateKeyPair(options.algorithm, generateExtractable);
848
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests; cannot be triggered here without mocking */
849
+ if (keyPairResult.isFailure()) {
850
+ return (0, ts_utils_1.fail)(`Failed to generate keypair for '${name}': ${keyPairResult.message}`);
851
+ }
852
+ const { publicKey, privateKey } = keyPairResult.value;
853
+ const jwkResult = await this._cryptoProvider.exportPublicKeyJwk(publicKey);
854
+ /* c8 ignore next 3 - export of an extractable freshly-generated public key is hard to fail */
855
+ if (jwkResult.isFailure()) {
856
+ return (0, ts_utils_1.fail)(`Failed to export public key for '${name}': ${jwkResult.message}`);
857
+ }
858
+ // Escrow (opt-in). The bare private key exists only as three local
859
+ // artifacts, none of which is returned or logged: (1) the transient
860
+ // extractable `privateKey` above; (2) the exported `escrowedPrivateKeyJwk`,
861
+ // which is carried in the vault entry (same custody class as the public
862
+ // JWK); (3) a freshly re-imported non-extractable key handed to `store()`
863
+ // when the live copy must be non-extractable. When the live copy is
864
+ // extractable the transient key IS the live copy and is stored directly.
865
+ let escrowedPrivateKeyJwk;
866
+ let keyToStore = privateKey;
867
+ if (options.escrow === true) {
868
+ // Chain the export → (conditional) non-extractable re-import so the
869
+ // failure dispatch lives in ts-utils rather than in local branch nodes.
870
+ // When the live copy is extractable the transient key IS the live copy;
871
+ // otherwise a freshly re-imported non-extractable key is stored instead.
872
+ const escrowPrepResult = await (await this._exportPrivateKeyJwk(privateKey, name)).thenOnSuccess(async (jwk) => {
873
+ if (liveExtractable) {
874
+ return (0, ts_utils_1.succeed)({ jwk, keyToStore: privateKey });
875
+ }
876
+ return (await this._importEscrowedPrivateKey(jwk, options.algorithm, false))
877
+ .withErrorFormat((msg) => `Failed to prepare non-extractable key for '${name}': ${msg}`)
878
+ .onSuccess((reimported) => (0, ts_utils_1.succeed)({ jwk, keyToStore: reimported }));
879
+ });
880
+ /* c8 ignore next 3 - escrowPrepResult only fails if exporting or re-importing a freshly-generated valid key fails, which a healthy provider cannot do (same untestable class as the keyPairResult/jwkResult/idResult guards); the import-failure path itself is covered via getKeyPair rehydration of a malformed escrow JWK */
881
+ if (escrowPrepResult.isFailure()) {
882
+ return (0, ts_utils_1.fail)(escrowPrepResult.message);
883
+ }
884
+ ({ jwk: escrowedPrivateKeyJwk, keyToStore } = escrowPrepResult.value);
885
+ }
886
+ const idResult = this._generateId();
887
+ /* c8 ignore next 3 - random-bytes failure is hard to trigger with a healthy provider */
888
+ if (idResult.isFailure()) {
889
+ return (0, ts_utils_1.fail)(`Failed to mint storage id for '${name}': ${idResult.message}`);
890
+ }
891
+ const id = idResult.value;
892
+ // Storage-first: write the private key before committing the vault entry.
893
+ const storeResult = await this._privateKeyStorage.store(id, keyToStore);
894
+ if (storeResult.isFailure()) {
895
+ return (0, ts_utils_1.fail)(`Failed to persist private key for '${name}': ${storeResult.message}`);
896
+ }
897
+ const entry = {
898
+ name,
899
+ type: 'asymmetric-keypair',
900
+ id,
901
+ algorithm: options.algorithm,
902
+ publicKeyJwk: jwkResult.value,
903
+ escrowedPrivateKeyJwk,
904
+ description: options.description,
905
+ createdAt: getCurrentTimestamp()
906
+ };
907
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
908
+ this._secrets.set(name, entry);
909
+ this._dirty = true;
910
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
911
+ }
912
+ /**
913
+ * Retrieves the keypair for an asymmetric-keypair entry. The private key is
914
+ * loaded from {@link CryptoUtils.KeyStore.IPrivateKeyStorage} on every call —
915
+ * the keystore never caches private `CryptoKey` references between calls.
916
+ * The public key is re-imported from the vault's JWK so callers always
917
+ * receive a `CryptoKey` rather than the JWK form.
918
+ *
919
+ * With `options.rehydrate: true`, if the storage backend holds no blob for
920
+ * the entry's `id` and the entry carries an `escrowedPrivateKeyJwk` (see
921
+ * `addKeyPair(name, { escrow: true })`), the escrowed JWK is imported and
922
+ * stored under the entry's `id` before being returned — recovering the
923
+ * private key on a fresh device from the vault plus master password. This is
924
+ * fill-a-gap only: an existing storage blob is never overwritten.
925
+ *
926
+ * @param name - Name of the entry
927
+ * @param options - Optional {@link CryptoUtils.KeyStore.IGetKeyPairOptions}
928
+ * (currently the `rehydrate` escrow-recovery flag).
929
+ * @returns Success with `{ publicKey, privateKey }`, Failure if not found,
930
+ * locked, wrong type, no provider, or storage load (and any escrow
931
+ * rehydration) failed.
932
+ * @public
933
+ */
934
+ async getKeyPair(name, options) {
935
+ if (!this._secrets) {
936
+ return (0, ts_utils_1.fail)('Key store is locked');
937
+ }
938
+ const entry = this._secrets.get(name);
939
+ if (!entry) {
940
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
941
+ }
942
+ if (entry.type !== 'asymmetric-keypair') {
943
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
944
+ }
945
+ if (!this._privateKeyStorage) {
946
+ return (0, ts_utils_1.fail)('No private key storage configured');
947
+ }
948
+ return (await this._loadOrRehydratePrivateKey(name, entry, options)).thenOnSuccess(async (privateKey) => (await this._cryptoProvider.importPublicKeyJwk(entry.publicKeyJwk, entry.algorithm))
949
+ .withErrorFormat((msg) => `Failed to re-import public key for '${name}': ${msg}`)
950
+ .onSuccess((publicKey) => (0, ts_utils_1.succeed)({ publicKey, privateKey })));
951
+ }
568
952
  /**
569
953
  * Lists secret names filtered by type.
570
954
  * @param type - The secret type to filter by
@@ -604,7 +988,8 @@ class KeyStore {
604
988
  if (oldName !== newName && this._secrets.has(newName)) {
605
989
  return (0, ts_utils_1.fail)(`Secret '${newName}' already exists`);
606
990
  }
607
- // Create new entry with new name (preserve type)
991
+ // Create new entry with new name. For asymmetric entries the spread
992
+ // preserves `id` so the storage handle survives the rename.
608
993
  const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
609
994
  this._secrets.delete(oldName);
610
995
  this._secrets.set(newName, newEntry);
@@ -634,49 +1019,29 @@ class KeyStore {
634
1019
  if (keyResult.isFailure()) {
635
1020
  return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
636
1021
  }
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}`);
1022
+ return this._encryptVault(keyResult.value);
1023
+ }
1024
+ /**
1025
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
1026
+ * derivation. Use this when the derived key has been stored externally
1027
+ * (e.g., in another key store) and the original password is no longer
1028
+ * available.
1029
+ *
1030
+ * The supplied key must be the same key that was (or would be) derived
1031
+ * from the master password using the key store's PBKDF2 parameters.
1032
+ *
1033
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
1034
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
1035
+ * @public
1036
+ */
1037
+ async saveWithKey(derivedKey) {
1038
+ if (!this._secrets || !this._salt) {
1039
+ return (0, ts_utils_1.fail)('Key store is locked');
657
1040
  }
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}`);
1041
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
1042
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
662
1043
  }
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);
1044
+ return this._encryptVault(derivedKey);
680
1045
  }
681
1046
  /**
682
1047
  * Changes the master password.
@@ -720,7 +1085,7 @@ class KeyStore {
720
1085
  }
721
1086
  }
722
1087
  // Generate new salt for the new password using crypto provider
723
- const saltResult = this._cryptoProvider.generateRandomBytes(model_1.MIN_SALT_LENGTH);
1088
+ const saltResult = this._cryptoProvider.generateRandomBytes(model_2.MIN_SALT_LENGTH);
724
1089
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
725
1090
  if (saltResult.isFailure()) {
726
1091
  return (0, ts_utils_1.fail)(`Failed to generate salt: ${saltResult.message}`);
@@ -744,6 +1109,9 @@ class KeyStore {
744
1109
  if (secretResult.isFailure()) {
745
1110
  return (0, ts_utils_1.fail)(`encryptByName: ${secretResult.message}`);
746
1111
  }
1112
+ if (secretResult.value.type === 'asymmetric-keypair') {
1113
+ return (0, ts_utils_1.fail)(`encryptByName: secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
1114
+ }
747
1115
  return (0, encryptedFile_1.createEncryptedFile)({
748
1116
  content,
749
1117
  secretName,
@@ -771,6 +1139,9 @@ class KeyStore {
771
1139
  if (!entry) {
772
1140
  return (0, ts_utils_1.fail)(`Secret '${secretName}' not found in key store`);
773
1141
  }
1142
+ if (entry.type === 'asymmetric-keypair') {
1143
+ return (0, ts_utils_1.fail)(`Secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
1144
+ }
774
1145
  return (0, ts_utils_1.succeed)(entry.key);
775
1146
  };
776
1147
  return (0, ts_utils_1.succeed)(provider);
@@ -790,6 +1161,289 @@ class KeyStore {
790
1161
  cryptoProvider: this._cryptoProvider
791
1162
  });
792
1163
  }
1164
+ // ============================================================================
1165
+ // Private: Vault Encryption / Decryption
1166
+ // ============================================================================
1167
+ /**
1168
+ * Encrypts the vault with a derived key and returns the key store file.
1169
+ * Shared by `save()` and `saveWithKey()`.
1170
+ */
1171
+ async _encryptVault(derivedKey) {
1172
+ // _secrets and _salt are guaranteed non-undefined by callers
1173
+ const secrets = this._secrets;
1174
+ const salt = this._salt;
1175
+ // Build vault contents
1176
+ const secretEntries = {};
1177
+ for (const [name, entry] of secrets) {
1178
+ if (entry.type === 'asymmetric-keypair') {
1179
+ secretEntries[name] = {
1180
+ name: entry.name,
1181
+ type: entry.type,
1182
+ id: entry.id,
1183
+ algorithm: entry.algorithm,
1184
+ publicKeyJwk: entry.publicKeyJwk,
1185
+ escrowedPrivateKeyJwk: entry.escrowedPrivateKeyJwk,
1186
+ description: entry.description,
1187
+ createdAt: entry.createdAt
1188
+ };
1189
+ }
1190
+ else {
1191
+ secretEntries[name] = {
1192
+ name: entry.name,
1193
+ type: entry.type,
1194
+ key: this._cryptoProvider.toBase64(entry.key),
1195
+ description: entry.description,
1196
+ createdAt: entry.createdAt
1197
+ };
1198
+ }
1199
+ }
1200
+ const vaultContents = {
1201
+ version: model_2.KEYSTORE_FORMAT,
1202
+ secrets: secretEntries
1203
+ };
1204
+ // Serialize and encrypt
1205
+ const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
1206
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1207
+ if (jsonResult.isFailure()) {
1208
+ return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
1209
+ }
1210
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
1211
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
1212
+ if (encryptResult.isFailure()) {
1213
+ return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
1214
+ }
1215
+ const { iv, authTag, encryptedData } = encryptResult.value;
1216
+ const keystoreFileData = {
1217
+ format: model_2.KEYSTORE_FORMAT,
1218
+ algorithm: Constants.DEFAULT_ALGORITHM,
1219
+ iv: this._cryptoProvider.toBase64(iv),
1220
+ authTag: this._cryptoProvider.toBase64(authTag),
1221
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
1222
+ keyDerivation: {
1223
+ kdf: 'pbkdf2',
1224
+ salt: this._cryptoProvider.toBase64(salt),
1225
+ iterations: this._iterations
1226
+ }
1227
+ };
1228
+ this._keystoreFile = keystoreFileData;
1229
+ this._dirty = false;
1230
+ this._isNew = false;
1231
+ return (0, ts_utils_1.succeed)(keystoreFileData);
1232
+ }
1233
+ /**
1234
+ * Decrypts the vault with a derived key and loads secrets into memory.
1235
+ * Shared by `unlock()` and `unlockWithKey()`.
1236
+ */
1237
+ async _decryptVault(derivedKey) {
1238
+ const keystoreFile = this._keystoreFile;
1239
+ /* c8 ignore next 3 - defensive: _decryptVault is only called after a successful open() or create() */
1240
+ if (keystoreFile === undefined) {
1241
+ return (0, ts_utils_1.fail)('No key store file loaded');
1242
+ }
1243
+ const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
1244
+ const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
1245
+ const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
1246
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
1247
+ if (ivResult.isFailure()) {
1248
+ return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
1249
+ }
1250
+ if (authTagResult.isFailure()) {
1251
+ return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
1252
+ }
1253
+ if (encryptedDataResult.isFailure()) {
1254
+ return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
1255
+ }
1256
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
1257
+ if (decryptResult.isFailure()) {
1258
+ return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
1259
+ }
1260
+ // Parse the vault contents
1261
+ const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
1262
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1263
+ if (parseResult.isFailure()) {
1264
+ return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
1265
+ }
1266
+ const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
1267
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1268
+ if (vaultResult.isFailure()) {
1269
+ return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
1270
+ }
1271
+ // Build secrets into local variables to avoid partial state on failure
1272
+ const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
1273
+ if (saltResult.isFailure()) {
1274
+ return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
1275
+ }
1276
+ const secrets = new Map();
1277
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
1278
+ if (jsonEntry.type === 'asymmetric-keypair') {
1279
+ const entry = {
1280
+ name,
1281
+ type: jsonEntry.type,
1282
+ id: jsonEntry.id,
1283
+ algorithm: jsonEntry.algorithm,
1284
+ publicKeyJwk: jsonEntry.publicKeyJwk,
1285
+ escrowedPrivateKeyJwk: jsonEntry.escrowedPrivateKeyJwk,
1286
+ description: jsonEntry.description,
1287
+ createdAt: jsonEntry.createdAt
1288
+ };
1289
+ secrets.set(name, entry);
1290
+ }
1291
+ else {
1292
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
1293
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1294
+ if (keyBytesResult.isFailure()) {
1295
+ return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
1296
+ }
1297
+ const entry = {
1298
+ name,
1299
+ type: jsonEntry.type,
1300
+ key: keyBytesResult.value,
1301
+ description: jsonEntry.description,
1302
+ createdAt: jsonEntry.createdAt
1303
+ };
1304
+ secrets.set(name, entry);
1305
+ }
1306
+ }
1307
+ // All validation passed — commit state atomically
1308
+ this._salt = saltResult.value;
1309
+ this._secrets = secrets;
1310
+ this._state = 'unlocked';
1311
+ this._dirty = false;
1312
+ return (0, ts_utils_1.succeed)(this);
1313
+ }
1314
+ // ============================================================================
1315
+ // Private: Helpers for asymmetric flows
1316
+ // ============================================================================
1317
+ /**
1318
+ * Releases the resources held by an entry being displaced from the vault.
1319
+ * Symmetric entries get their key buffer zeroed in place. Asymmetric entries
1320
+ * have their private-key blob best-effort deleted from
1321
+ * {@link CryptoUtils.KeyStore.IPrivateKeyStorage}; if the storage call fails,
1322
+ * a warning string is returned but the displacement still proceeds — the
1323
+ * orphaned blob is left for consumer-side GC. Without a configured provider,
1324
+ * asymmetric cleanup is silently skipped.
1325
+ * @returns A warning string if storage cleanup failed, otherwise undefined.
1326
+ */
1327
+ async _releaseEntryResources(entry) {
1328
+ if (entry.type === 'asymmetric-keypair') {
1329
+ if (!this._privateKeyStorage) {
1330
+ return undefined;
1331
+ }
1332
+ const deleteResult = await this._privateKeyStorage.delete(entry.id);
1333
+ if (deleteResult.isFailure()) {
1334
+ return `Failed to delete prior storage blob for '${entry.name}' (id ${entry.id}): ${deleteResult.message}`;
1335
+ }
1336
+ return undefined;
1337
+ }
1338
+ entry.key.fill(0);
1339
+ return undefined;
1340
+ }
1341
+ /**
1342
+ * Exports the transient extractable private `CryptoKey` to a JWK for escrow,
1343
+ * via WebCrypto's `exportKey('jwk', ...)` (cross-runtime through
1344
+ * `globalThis.crypto.subtle`). The result is carried in the vault entry — the
1345
+ * same custody class as the public JWK — and is never returned from a public
1346
+ * method nor logged.
1347
+ */
1348
+ async _exportPrivateKeyJwk(privateKey, name) {
1349
+ return (0, ts_utils_1.captureAsyncResult)(() => globalThis.crypto.subtle.exportKey('jwk', privateKey)).withErrorFormat((msg) => `Failed to export private key for escrow of '${name}': ${msg}`);
1350
+ }
1351
+ /**
1352
+ * Re-imports an escrowed private-key JWK as a `CryptoKey` for `algorithm`
1353
+ * with the requested extractability. The WebCrypto JWK-import descriptor is
1354
+ * shared between the public and private halves for every supported algorithm,
1355
+ * so `IKeyPairAlgorithmParams.importPublicKey` is reused; the private/public
1356
+ * distinction is carried by the requested usages (see
1357
+ * {@link KeyStore._privateKeyUsagesFor}). Cross-runtime through
1358
+ * `globalThis.crypto.subtle`.
1359
+ */
1360
+ async _importEscrowedPrivateKey(jwk, algorithm, extractable) {
1361
+ const params = keyPairAlgorithmParams_1.keyPairAlgorithmParams[algorithm];
1362
+ const usages = KeyStore._privateKeyUsagesFor(jwk, params);
1363
+ return (0, ts_utils_1.captureAsyncResult)(() => globalThis.crypto.subtle.importKey('jwk', jwk, params.importPublicKey, extractable, usages)).withErrorFormat((msg) => `Failed to import escrowed private key: ${msg}`);
1364
+ }
1365
+ /**
1366
+ * Loads the private key for `entry` from storage, or — when
1367
+ * `options.rehydrate` is set, storage holds no blob, and the entry carries an
1368
+ * escrowed JWK — imports the escrowed key, persists it under the entry's `id`
1369
+ * (fill-a-gap; never overwrites an existing blob), and returns it.
1370
+ */
1371
+ async _loadOrRehydratePrivateKey(name, entry, options) {
1372
+ // Non-undefined by getKeyPair's precondition check.
1373
+ const storage = this._privateKeyStorage;
1374
+ const loadResult = await storage.load(entry.id);
1375
+ if (loadResult.isSuccess()) {
1376
+ return (0, ts_utils_1.succeed)(loadResult.value);
1377
+ }
1378
+ if ((options === null || options === void 0 ? void 0 : options.rehydrate) !== true || entry.escrowedPrivateKeyJwk === undefined) {
1379
+ return (0, ts_utils_1.fail)(`Failed to load private key for '${name}': ${loadResult.message}`);
1380
+ }
1381
+ // Fill-a-gap recovery: import the escrowed key non-extractable where the
1382
+ // backend supports it (extractable otherwise, since a JWK-round-tripping
1383
+ // backend cannot store a non-extractable key), then persist it.
1384
+ const backendExtractable = !storage.supportsNonExtractable;
1385
+ const importResult = await this._importEscrowedPrivateKey(entry.escrowedPrivateKeyJwk, entry.algorithm, backendExtractable);
1386
+ if (importResult.isFailure()) {
1387
+ return (0, ts_utils_1.fail)(`Failed to rehydrate private key for '${name}' from escrow: ${importResult.message}`);
1388
+ }
1389
+ const storeResult = await storage.store(entry.id, importResult.value);
1390
+ if (storeResult.isFailure()) {
1391
+ return (0, ts_utils_1.fail)(`Failed to persist rehydrated private key for '${name}': ${storeResult.message}`);
1392
+ }
1393
+ return (0, ts_utils_1.succeed)(importResult.value);
1394
+ }
1395
+ /**
1396
+ * Computes the key usages to request when importing an escrowed private JWK.
1397
+ * Mirrors `EncryptedFilePrivateKeyStorage`: intersect the algorithm's private
1398
+ * usages (its keypair usages minus the public-only ones) with the JWK's
1399
+ * recorded `key_ops` so we request exactly the operations the stored key
1400
+ * supports; fall back to the algorithm's private usages when `key_ops` is
1401
+ * absent.
1402
+ */
1403
+ static _privateKeyUsagesFor(jwk, params) {
1404
+ const privateUsages = params.keyPairUsages.filter((usage) => !PUBLIC_ONLY_USAGES.includes(usage));
1405
+ const keyOps = jwk.key_ops;
1406
+ if (keyOps === undefined) {
1407
+ return [...privateUsages];
1408
+ }
1409
+ return privateUsages.filter((usage) => keyOps.includes(usage));
1410
+ }
1411
+ /**
1412
+ * Constant-time byte comparison. Returns false immediately for length
1413
+ * mismatch (length is not secret); for equal-length inputs, walks the full
1414
+ * buffer accumulating differences via XOR so the running time does not leak
1415
+ * the position of the first differing byte.
1416
+ */
1417
+ static _timingSafeEqual(a, b) {
1418
+ /* c8 ignore next 3 - defensive: callers compare equal-length 32-byte PBKDF2 keys */
1419
+ if (a.length !== b.length) {
1420
+ return false;
1421
+ }
1422
+ let diff = 0;
1423
+ for (let i = 0; i < a.length; i++) {
1424
+ // eslint-disable-next-line no-bitwise
1425
+ diff |= a[i] ^ b[i];
1426
+ }
1427
+ return diff === 0;
1428
+ }
1429
+ /**
1430
+ * Mints a fresh UUID v4 storage handle using the crypto provider's
1431
+ * {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
1432
+ * Random-bytes failures propagate as Failure.
1433
+ */
1434
+ _generateId() {
1435
+ return this._cryptoProvider.generateRandomBytes(16).onSuccess((bytes) => {
1436
+ // Per RFC 4122 §4.4: set version (4) and variant (10xx) bits.
1437
+ // eslint-disable-next-line no-bitwise
1438
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1439
+ // eslint-disable-next-line no-bitwise
1440
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1441
+ const hex = Array.from(bytes)
1442
+ .map((b) => b.toString(16).padStart(2, '0'))
1443
+ .join('');
1444
+ 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)}`);
1445
+ });
1446
+ }
793
1447
  }
794
1448
  exports.KeyStore = KeyStore;
795
1449
  //# sourceMappingURL=keyStore.js.map