@fgv/ts-extras 5.1.0-5 → 5.1.0-50

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