@hraness/ghostget 0.17.2

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 (500) hide show
  1. package/CHANGELOG.md +458 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1359 -0
  4. package/bunfig.toml +2 -0
  5. package/dist/apple-photos-client.js +704 -0
  6. package/dist/beeper-client.js +4602 -0
  7. package/dist/client.js +1605 -0
  8. package/dist/index-26yq8q16.js +31 -0
  9. package/dist/index-9t13b4k7.js +508 -0
  10. package/dist/index-axwm9b3v.js +5 -0
  11. package/dist/index-gwk7rbyj.js +30 -0
  12. package/dist/index.js +701 -0
  13. package/dist/messaging.js +1130 -0
  14. package/dist/omni-client.js +1406 -0
  15. package/dist/whatsapp-client.js +355 -0
  16. package/docs/imessage-direct-provider.md +106 -0
  17. package/docs/rental-listings.md +94 -0
  18. package/package.json +600 -0
  19. package/skills/ghostget/SKILL.md +251 -0
  20. package/skills/ghostget/agents/openai.yaml +4 -0
  21. package/skills/ghostget/references/adapter-contract.md +181 -0
  22. package/skills/ghostget/references/apple-photos.md +115 -0
  23. package/skills/ghostget/references/article-drafts.md +235 -0
  24. package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
  25. package/skills/ghostget/references/cross-posting-video.md +139 -0
  26. package/skills/ghostget/references/cross-posting.md +82 -0
  27. package/skills/ghostget/references/derivation.md +390 -0
  28. package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
  29. package/skills/ghostget/references/install.md +66 -0
  30. package/skills/ghostget/references/linkedin-adapter.md +442 -0
  31. package/skills/ghostget/references/messaging.md +168 -0
  32. package/skills/ghostget/references/meta-comet-contract.md +153 -0
  33. package/skills/ghostget/references/platform-patterns.md +122 -0
  34. package/skills/ghostget/references/provider-plugins.md +496 -0
  35. package/skills/ghostget/references/reddit-flair.md +78 -0
  36. package/skills/ghostget/references/safety-and-state.md +214 -0
  37. package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
  38. package/skills/ghostget/references/social-platform-routing.md +66 -0
  39. package/skills/ghostget/references/social-profile-stats.md +195 -0
  40. package/skills/ghostget/references/social-video-platform-routing.md +89 -0
  41. package/skills/ghostget/references/whatsapp-adapter.md +101 -0
  42. package/skills/ghostget/references/x-adapter.md +332 -0
  43. package/skills/ghostget/references/x-ai-disclosure.md +71 -0
  44. package/src/apple-photos-cli.ts +639 -0
  45. package/src/apple-photos-client-types.ts +168 -0
  46. package/src/apple-photos-client.ts +201 -0
  47. package/src/apple-photos-contact-evidence.ts +838 -0
  48. package/src/apple-photos-local-source.ts +1131 -0
  49. package/src/args.ts +2053 -0
  50. package/src/article-draft-document.ts +417 -0
  51. package/src/article-draft-embeds.ts +123 -0
  52. package/src/article-draft-images.ts +214 -0
  53. package/src/assets/adapter-template/wrench-adapter.json +40 -0
  54. package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
  55. package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
  56. package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
  57. package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
  58. package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
  59. package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
  60. package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
  61. package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
  62. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
  63. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
  64. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
  65. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
  66. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
  67. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
  68. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
  69. package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
  70. package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
  71. package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
  72. package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
  73. package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
  74. package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
  75. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
  76. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
  77. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
  78. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
  79. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
  80. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
  81. package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
  82. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
  83. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
  84. package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
  85. package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
  86. package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
  87. package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
  88. package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
  89. package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
  90. package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
  91. package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
  92. package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
  93. package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
  94. package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
  95. package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
  96. package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
  97. package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
  98. package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
  99. package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
  100. package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
  101. package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
  102. package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
  103. package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
  104. package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
  105. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
  106. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
  107. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
  108. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
  109. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
  110. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
  111. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
  112. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
  113. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
  114. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
  115. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
  116. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
  117. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
  118. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
  119. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
  120. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
  121. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
  122. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
  123. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
  124. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
  125. package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
  126. package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
  127. package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
  128. package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
  129. package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
  130. package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
  131. package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
  132. package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
  133. package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
  134. package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
  135. package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
  136. package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
  137. package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
  138. package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
  139. package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
  140. package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
  141. package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
  142. package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
  143. package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
  144. package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
  145. package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
  146. package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
  147. package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
  148. package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
  149. package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
  150. package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
  151. package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
  152. package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
  153. package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
  154. package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
  155. package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
  156. package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
  157. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
  158. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
  159. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
  160. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
  161. package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
  162. package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
  163. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
  164. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
  165. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
  166. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
  167. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
  168. package/src/assets/adapters/x/wrench-adapter.json +749 -0
  169. package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
  170. package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
  171. package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
  172. package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
  173. package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
  174. package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
  175. package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
  176. package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
  177. package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
  178. package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
  179. package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
  180. package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
  181. package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
  182. package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
  183. package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
  184. package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
  185. package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
  186. package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
  187. package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
  188. package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
  189. package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
  190. package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
  191. package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
  192. package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
  193. package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
  194. package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
  195. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  196. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  197. package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
  198. package/src/assets.d.ts +4 -0
  199. package/src/auth.ts +1157 -0
  200. package/src/beeper-client-types.ts +169 -0
  201. package/src/beeper-client.ts +250 -0
  202. package/src/beeper-contact-interactions-cli.ts +180 -0
  203. package/src/beeper-contact-interactions.ts +1612 -0
  204. package/src/beeper-message-bundle-v1.ts +257 -0
  205. package/src/beeper-message-like-me-cli.ts +84 -0
  206. package/src/beeper-message-like-me-export.ts +2274 -0
  207. package/src/beeper-message-like-me-recovery.ts +931 -0
  208. package/src/beeper-message-like-me-source.ts +4034 -0
  209. package/src/browser-admission.ts +928 -0
  210. package/src/browser-snapshots.ts +260 -0
  211. package/src/browser.ts +3873 -0
  212. package/src/canonical-json.ts +60 -0
  213. package/src/catalog-cli.ts +638 -0
  214. package/src/cli.ts +208 -0
  215. package/src/client-types.ts +301 -0
  216. package/src/client.ts +2726 -0
  217. package/src/confirmed-write-failure.ts +44 -0
  218. package/src/confirmed-write-model.ts +72 -0
  219. package/src/confirmed-write-platform.ts +1055 -0
  220. package/src/confirmed-write-program.ts +224 -0
  221. package/src/confirmed-write-runtime.ts +16 -0
  222. package/src/cursor-token.ts +228 -0
  223. package/src/derivation-dnr-readiness.ts +367 -0
  224. package/src/derivation-file-chooser.ts +384 -0
  225. package/src/derivation-network-boundary.ts +835 -0
  226. package/src/derivation-network-guard.ts +782 -0
  227. package/src/derivation-network-proxy-helper.ts +440 -0
  228. package/src/derivation-network-proxy.ts +385 -0
  229. package/src/derive-command-helper.ts +693 -0
  230. package/src/derive-fixtures.ts +343 -0
  231. package/src/derive-review.ts +980 -0
  232. package/src/derive.ts +4754 -0
  233. package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
  234. package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
  235. package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
  236. package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
  237. package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
  238. package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
  239. package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
  240. package/src/ghostget.ts +3672 -0
  241. package/src/gmail-capture.ts +1244 -0
  242. package/src/har-internal.ts +1242 -0
  243. package/src/har.ts +743 -0
  244. package/src/index.ts +218 -0
  245. package/src/invocation-read-platform.ts +55 -0
  246. package/src/invocation-read-program.ts +93 -0
  247. package/src/invocation-read-runtime.ts +11 -0
  248. package/src/linked-device-lifecycle-admission.ts +450 -0
  249. package/src/linked-device-lifecycle-journal.ts +1560 -0
  250. package/src/linked-device-lifecycle-runtime.ts +1602 -0
  251. package/src/local-cli-admission.ts +135 -0
  252. package/src/local-cli-contracts.ts +271 -0
  253. package/src/local-cli-execution.ts +98 -0
  254. package/src/local-cli-surface-contract.ts +1510 -0
  255. package/src/local-cli-tool-identity.ts +336 -0
  256. package/src/media/archive.ts +2325 -0
  257. package/src/media/args.ts +303 -0
  258. package/src/media/cli.ts +287 -0
  259. package/src/media/doctor.ts +475 -0
  260. package/src/media/ffmpeg.ts +1330 -0
  261. package/src/media/http-capture.ts +711 -0
  262. package/src/media/http-probe.ts +593 -0
  263. package/src/media/http.ts +387 -0
  264. package/src/media/index.ts +10 -0
  265. package/src/media/local-transcription.ts +613 -0
  266. package/src/media/lock.ts +321 -0
  267. package/src/media/manifest.ts +1712 -0
  268. package/src/media/metadata.ts +906 -0
  269. package/src/media/process.ts +723 -0
  270. package/src/media/revision.ts +166 -0
  271. package/src/media/runtime-closure.ts +1063 -0
  272. package/src/media/source-router.ts +187 -0
  273. package/src/media/transcriber-config.ts +1122 -0
  274. package/src/media/transcript-persistence-model.ts +15 -0
  275. package/src/media/transcript-persistence-platform.ts +89 -0
  276. package/src/media/transcript-persistence-program.ts +39 -0
  277. package/src/media/transcript-persistence-runtime.ts +24 -0
  278. package/src/media/transcript.ts +616 -0
  279. package/src/media/utf8-order.ts +30 -0
  280. package/src/media/whisper-cpp.ts +464 -0
  281. package/src/media/whisper-language.ts +58 -0
  282. package/src/media/yt-dlp.ts +439 -0
  283. package/src/message-like-me-agentic-messaging.ts +882 -0
  284. package/src/messaging-action-store.ts +793 -0
  285. package/src/messaging-runtime.ts +2136 -0
  286. package/src/messaging-store.ts +539 -0
  287. package/src/messaging-types.ts +1663 -0
  288. package/src/messaging.ts +334 -0
  289. package/src/model.ts +2671 -0
  290. package/src/oauth-google.ts +888 -0
  291. package/src/omni-client-types.ts +306 -0
  292. package/src/omni-client.ts +2225 -0
  293. package/src/omni-limits.ts +34 -0
  294. package/src/omni-model.ts +1614 -0
  295. package/src/omni-request.ts +379 -0
  296. package/src/omni-runtime.ts +1632 -0
  297. package/src/operation-deadline.ts +189 -0
  298. package/src/path-helper.ts +1656 -0
  299. package/src/pinned-https.ts +194 -0
  300. package/src/plan-assets.ts +832 -0
  301. package/src/platform-catalog.ts +1832 -0
  302. package/src/plugins/beeper-linked-device/plugin.ts +310 -0
  303. package/src/plugins/bluesky-web/plugin.ts +178 -0
  304. package/src/plugins/clasificados-web/plugin.ts +66 -0
  305. package/src/plugins/github-web/plugin.ts +80 -0
  306. package/src/plugins/gmail-official/plugin.ts +110 -0
  307. package/src/plugins/hacker-news-web/plugin.ts +52 -0
  308. package/src/plugins/imessage-direct/plugin.ts +195 -0
  309. package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
  310. package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
  311. package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
  312. package/src/plugins/linkedin-official/plugin.ts +84 -0
  313. package/src/plugins/linkedin-web/plugin.ts +412 -0
  314. package/src/plugins/meta-web/plugin.ts +317 -0
  315. package/src/plugins/reddit-web/flair.ts +492 -0
  316. package/src/plugins/reddit-web/plugin.ts +477 -0
  317. package/src/plugins/substack-web/plugin.ts +124 -0
  318. package/src/plugins/tiktok-web/plugin.ts +67 -0
  319. package/src/plugins/twitch-web/plugin.ts +54 -0
  320. package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
  321. package/src/plugins/x-official/plugin.ts +64 -0
  322. package/src/plugins/x-web/plugin.ts +488 -0
  323. package/src/plugins/youtube-web/plugin.ts +101 -0
  324. package/src/portable-run-recovery.ts +466 -0
  325. package/src/process-identity.ts +501 -0
  326. package/src/profile-clone-helper.ts +104 -0
  327. package/src/provider-catalog-views.ts +51 -0
  328. package/src/provider-context.ts +69 -0
  329. package/src/provider-contract-definitions-x.ts +45 -0
  330. package/src/provider-contract-definitions.ts +948 -0
  331. package/src/provider-contract-input-gmail.ts +176 -0
  332. package/src/provider-contract-input-linkedin.ts +77 -0
  333. package/src/provider-contract-input-x.ts +304 -0
  334. package/src/provider-contract-planning.ts +23 -0
  335. package/src/provider-contract-semantic-identity.ts +54 -0
  336. package/src/provider-contracts.ts +251 -0
  337. package/src/provider-http.ts +723 -0
  338. package/src/provider-plugin-auth.ts +36 -0
  339. package/src/provider-plugin-builtins.ts +229 -0
  340. package/src/provider-plugin-cleanup-barrier.ts +229 -0
  341. package/src/provider-plugin-cleanup-execution.ts +79 -0
  342. package/src/provider-plugin-cleanup-resource.ts +470 -0
  343. package/src/provider-plugin-contract-identity.ts +707 -0
  344. package/src/provider-plugin-host.ts +1972 -0
  345. package/src/provider-plugin-identifiers.ts +60 -0
  346. package/src/provider-plugin-invocation-lease.ts +889 -0
  347. package/src/provider-plugin-lifecycle-kernel.ts +566 -0
  348. package/src/provider-plugin-lifecycle.ts +1455 -0
  349. package/src/provider-plugin-module-analysis.ts +336 -0
  350. package/src/provider-plugin-package.ts +2530 -0
  351. package/src/provider-plugin-portable-authority.ts +106 -0
  352. package/src/provider-plugin-portable-catalog.ts +94 -0
  353. package/src/provider-plugin-portable-identity.ts +315 -0
  354. package/src/provider-plugin-portable-runtime.ts +2057 -0
  355. package/src/provider-plugin-protocol.ts +1625 -0
  356. package/src/provider-plugin-registry.ts +3342 -0
  357. package/src/provider-plugin-store.ts +2155 -0
  358. package/src/provider-plugin-test-fixture.ts +5 -0
  359. package/src/provider-plugin.ts +4907 -0
  360. package/src/provider-plugins.generated.ts +43 -0
  361. package/src/provider-plugins.ts +8 -0
  362. package/src/provider-subject.ts +41 -0
  363. package/src/provider.ts +315 -0
  364. package/src/providers/beeper-local-runtime.ts +5426 -0
  365. package/src/providers/beeper-local.ts +2739 -0
  366. package/src/providers/beeper-messaging.ts +462 -0
  367. package/src/providers/beeper-omni.ts +1159 -0
  368. package/src/providers/bluesky-web-runtime.ts +2895 -0
  369. package/src/providers/bluesky-web.ts +1614 -0
  370. package/src/providers/clasificados-web-runtime.ts +255 -0
  371. package/src/providers/clasificados-web.ts +503 -0
  372. package/src/providers/contact-projection.ts +301 -0
  373. package/src/providers/github-read-model.ts +94 -0
  374. package/src/providers/github-read-platform.ts +165 -0
  375. package/src/providers/github-read-program.ts +97 -0
  376. package/src/providers/github-web-runtime.ts +320 -0
  377. package/src/providers/github-web.ts +403 -0
  378. package/src/providers/gmail-api.ts +2834 -0
  379. package/src/providers/gmail-omni.ts +830 -0
  380. package/src/providers/gmail.ts +1130 -0
  381. package/src/providers/hacker-news-web-runtime.ts +220 -0
  382. package/src/providers/hacker-news-web.ts +809 -0
  383. package/src/providers/imessage-direct-install.ts +354 -0
  384. package/src/providers/imessage-direct-messaging.ts +532 -0
  385. package/src/providers/imessage-direct-omni.ts +400 -0
  386. package/src/providers/imessage-direct-runtime.ts +1498 -0
  387. package/src/providers/imessage-direct.ts +351 -0
  388. package/src/providers/instagram-video-foundations.ts +913 -0
  389. package/src/providers/instagram-web-profile-browser.ts +600 -0
  390. package/src/providers/iso-bmff.ts +338 -0
  391. package/src/providers/linkedin-company-platform.ts +42 -0
  392. package/src/providers/linkedin-company-program.ts +88 -0
  393. package/src/providers/linkedin-contact-failure.ts +84 -0
  394. package/src/providers/linkedin-contact-platform.ts +45 -0
  395. package/src/providers/linkedin-contact-program.ts +91 -0
  396. package/src/providers/linkedin-profile-activity-failure.ts +76 -0
  397. package/src/providers/linkedin-profile-activity-platform.ts +41 -0
  398. package/src/providers/linkedin-profile-activity-program.ts +67 -0
  399. package/src/providers/linkedin-read-failure.ts +52 -0
  400. package/src/providers/linkedin-self-platform.ts +53 -0
  401. package/src/providers/linkedin-self-program.ts +122 -0
  402. package/src/providers/linkedin-web-article-browser.ts +794 -0
  403. package/src/providers/linkedin-web-bootstrap.ts +146 -0
  404. package/src/providers/linkedin-web-contact.ts +830 -0
  405. package/src/providers/linkedin-web-feed-browser.ts +832 -0
  406. package/src/providers/linkedin-web-feed.ts +864 -0
  407. package/src/providers/linkedin-web-post-browser.ts +653 -0
  408. package/src/providers/linkedin-web-profile-browser.ts +683 -0
  409. package/src/providers/linkedin-web-runtime.ts +2388 -0
  410. package/src/providers/linkedin-web.ts +3305 -0
  411. package/src/providers/linkedin.ts +1885 -0
  412. package/src/providers/meta-bootstrap.ts +1043 -0
  413. package/src/providers/meta-facebook-group.ts +690 -0
  414. package/src/providers/meta-marketplace-relay.ts +747 -0
  415. package/src/providers/meta-omni.ts +373 -0
  416. package/src/providers/meta-relay-bundle.ts +1049 -0
  417. package/src/providers/meta-web-descriptors.ts +1740 -0
  418. package/src/providers/meta-web-runtime.ts +3388 -0
  419. package/src/providers/meta-web.ts +2931 -0
  420. package/src/providers/read-failure.ts +139 -0
  421. package/src/providers/reddit-omni.ts +462 -0
  422. package/src/providers/reddit-web-runtime.ts +1929 -0
  423. package/src/providers/reddit-web.ts +1611 -0
  424. package/src/providers/rental-listings.ts +460 -0
  425. package/src/providers/substack-omni.ts +406 -0
  426. package/src/providers/substack-video-mp4.ts +33 -0
  427. package/src/providers/substack-web-runtime.ts +2812 -0
  428. package/src/providers/substack-web.ts +1502 -0
  429. package/src/providers/tiktok-video-mp4.ts +33 -0
  430. package/src/providers/tiktok-web-runtime.ts +770 -0
  431. package/src/providers/tiktok-web.ts +2786 -0
  432. package/src/providers/twitch-web-runtime.ts +226 -0
  433. package/src/providers/twitch-web.ts +274 -0
  434. package/src/providers/whatsapp-account-identity.ts +55 -0
  435. package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
  436. package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
  437. package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
  438. package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
  439. package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
  440. package/src/providers/whatsapp-messaging.ts +132 -0
  441. package/src/providers/whatsapp-omni.ts +586 -0
  442. package/src/providers/whatsapp-web-runtime.ts +3108 -0
  443. package/src/providers/whatsapp-web.ts +1037 -0
  444. package/src/providers/x-image-provenance.ts +523 -0
  445. package/src/providers/x-made-with-ai.ts +106 -0
  446. package/src/providers/x-transaction-id.ts +413 -0
  447. package/src/providers/x-web-runtime.ts +3485 -0
  448. package/src/providers/x-web.ts +2332 -0
  449. package/src/providers/x.ts +1889 -0
  450. package/src/providers/youtube-web-runtime.ts +1400 -0
  451. package/src/providers/youtube-web.ts +1221 -0
  452. package/src/read-admission-runtime.ts +91 -0
  453. package/src/read-client.ts +521 -0
  454. package/src/read-effect-platform.ts +7 -0
  455. package/src/read-effect-runtime.ts +14 -0
  456. package/src/read-effect.ts +52 -0
  457. package/src/read-projection-admission.ts +866 -0
  458. package/src/read-projections.ts +3151 -0
  459. package/src/recovery.ts +1492 -0
  460. package/src/reviewed-template.ts +146 -0
  461. package/src/run-journal.ts +1272 -0
  462. package/src/runtime.ts +5116 -0
  463. package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
  464. package/src/scripts/install-whatsapp-protocol.sh +232 -0
  465. package/src/scripts/recover-install-stages.ts +1491 -0
  466. package/src/scripts/resolve-state-home.ts +30 -0
  467. package/src/scripts/scaffold-web-provider.ts +973 -0
  468. package/src/scripts/sync-bundled-adapters.ts +695 -0
  469. package/src/session-secrets.ts +1183 -0
  470. package/src/state-helper.bunfig.toml +2 -0
  471. package/src/state-helper.ts +2914 -0
  472. package/src/storage.ts +3842 -0
  473. package/src/transport-policy.ts +2 -0
  474. package/src/usage.ts +182 -0
  475. package/src/version.ts +2 -0
  476. package/src/web-session-authentication-policy.ts +136 -0
  477. package/src/web-session-cleanup-admission.ts +2344 -0
  478. package/src/web-session-cleanup-join.ts +99 -0
  479. package/src/web-session-client.ts +996 -0
  480. package/src/web-session-contract-definitions.ts +820 -0
  481. package/src/web-session-contract-planning.ts +42 -0
  482. package/src/web-session-contracts.ts +228 -0
  483. package/src/web-session-cookies.ts +500 -0
  484. package/src/web-session-execution.ts +385 -0
  485. package/src/web-session-http.ts +373 -0
  486. package/src/web-session-read-errors.ts +19 -0
  487. package/src/web-session-read-runtime.ts +124 -0
  488. package/src/web-session-recovery.ts +818 -0
  489. package/src/web-session-sites.ts +58 -0
  490. package/src/web-session-template.ts +994 -0
  491. package/src/web-session.ts +93 -0
  492. package/src/whatsapp-client-binding.ts +21 -0
  493. package/src/whatsapp-client-types.ts +75 -0
  494. package/src/whatsapp-client.ts +395 -0
  495. package/src/whatsapp-export-coordinate.ts +17 -0
  496. package/src/whatsapp-message-bundle-v2.ts +195 -0
  497. package/src/whatsapp-message-like-me-cli.ts +195 -0
  498. package/src/whatsapp-message-like-me-export.ts +1072 -0
  499. package/src/whatsapp-message-like-me-source.ts +1012 -0
  500. package/tsconfig.json +67 -0
@@ -0,0 +1,1492 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ randomBytes,
5
+ } from "node:crypto";
6
+ import { existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ import type { GhostgetAuth } from "./auth";
10
+ import {
11
+ canonicalJson,
12
+ sha256,
13
+ type OperationInput,
14
+ type OperationRisk,
15
+ } from "./model";
16
+ import {
17
+ isProviderPluginOperationName,
18
+ isProviderPluginSurfaceId,
19
+ type ProviderPluginOperationName,
20
+ type ProviderPluginSurfaceId,
21
+ } from "./provider-plugin-identifiers";
22
+ import {
23
+ parsePortableOperationIdentityV1,
24
+ type PortableOperationIdentityV1,
25
+ } from "./provider-plugin-portable-identity";
26
+ import {
27
+ parseLocalCliContractIdentityV1,
28
+ type LocalCliContractIdentityV1,
29
+ } from "./local-cli-contracts";
30
+ import {
31
+ createPrivateJsonIfAbsent,
32
+ ensurePrivateStateDirectory,
33
+ listPrivateStateDirectory,
34
+ readPrivateStateFileIfPresent,
35
+ readRegularFile,
36
+ removePrivateStateDirectoryTree,
37
+ removePrivateStateFile,
38
+ ghostgetStateHome,
39
+ snapshotPrivateStateDirectory,
40
+ } from "./storage";
41
+
42
+ type Environment = Readonly<Record<string, string | undefined>>;
43
+ type JsonRecord = Record<string, unknown>;
44
+
45
+ const RECOVERY_DIRECTORY = "recovery";
46
+ const CAPSULE_DIRECTORY = "capsules";
47
+ const PROVIDER_ACCEPTED_TARGET_DIRECTORY = "provider-accepted-targets";
48
+ const OBSERVATION_DIRECTORY = "observations";
49
+ const RECOVERY_KEY = ".recovery-encryption-key";
50
+ const MAX_KEY_BYTES = 128;
51
+ const MAX_CAPSULE_PLAINTEXT_BYTES = 1536 * 1024;
52
+ const MAX_CAPSULE_ENCRYPTED_BYTES = 3 * 1024 * 1024;
53
+ const MAX_PROVIDER_ACCEPTED_TARGET_PLAINTEXT_BYTES = 32 * 1024;
54
+ const MAX_PROVIDER_ACCEPTED_TARGET_ENCRYPTED_BYTES = 64 * 1024;
55
+ const MAX_PROVIDER_ACCEPTED_TARGET_BYTES = 8 * 1024;
56
+ const MAX_OBSERVATION_BYTES = 64 * 1024;
57
+ const PORTABLE_RECOVERY_CONTRACT_HASH_DOMAIN =
58
+ "io-recovery-portable-contract-v1\0";
59
+
60
+ export type RecoveryContractIdentity =
61
+ | {
62
+ readonly transport: "provider-api";
63
+ readonly provider: ProviderPluginSurfaceId;
64
+ readonly action: ProviderPluginOperationName;
65
+ readonly version: number;
66
+ readonly hash: string;
67
+ }
68
+ | {
69
+ readonly transport: "web-session-api";
70
+ readonly site: ProviderPluginSurfaceId;
71
+ readonly action: ProviderPluginOperationName;
72
+ readonly version: number;
73
+ readonly hash: string;
74
+ }
75
+ | {
76
+ readonly transport: "reviewed-template-api";
77
+ readonly version: 1;
78
+ readonly hash: string;
79
+ }
80
+ | {
81
+ readonly transport: "portable-provider-plugin";
82
+ readonly identity: PortableOperationIdentityV1;
83
+ }
84
+ | {
85
+ readonly transport: "local-cli";
86
+ readonly identity: LocalCliContractIdentityV1;
87
+ };
88
+
89
+ export type RecoveryCapsule = {
90
+ readonly schemaVersion: 1;
91
+ readonly runId: string;
92
+ readonly createdAt: string;
93
+ readonly planDigest: string;
94
+ readonly adapter: {
95
+ readonly id: string;
96
+ readonly version: string;
97
+ readonly hash: string;
98
+ };
99
+ readonly operation: string;
100
+ readonly risk: Extract<OperationRisk, "R2" | "R3">;
101
+ readonly input: OperationInput;
102
+ readonly inputHash: string;
103
+ readonly auth: {
104
+ readonly id: string;
105
+ readonly hash: string;
106
+ readonly kind: GhostgetAuth["kind"];
107
+ };
108
+ readonly contract: RecoveryContractIdentity;
109
+ };
110
+
111
+ export type RecoveryCapsuleListEntry =
112
+ | {
113
+ readonly capsule: RecoveryCapsule;
114
+ }
115
+ | {
116
+ readonly runId: string;
117
+ readonly invalid: true;
118
+ };
119
+
120
+ export type ProviderBoundMutationTarget = {
121
+ readonly schemaVersion: 1;
122
+ /**
123
+ * Provider-owned stable identifier obtained from either a strict mutation
124
+ * response or a strict target read immediately before one exact dispatch.
125
+ */
126
+ readonly identifier: string;
127
+ };
128
+
129
+ /** Historical public name retained for response-derived create targets. */
130
+ export type ProviderAcceptedMutationTarget = ProviderBoundMutationTarget;
131
+
132
+ export type ProviderAcceptedMutationTargetDispatch = {
133
+ readonly id: string;
134
+ readonly index: number;
135
+ readonly planned: number;
136
+ };
137
+
138
+ /**
139
+ * Private evidence that one exact target was strictly bound to one active
140
+ * web-session dispatch. A create binds it from the provider acceptance
141
+ * response; a deletion may bind it from a strict read before its request.
142
+ * `target` is encrypted at rest and must never be copied into receipts,
143
+ * journals, observations, or diagnostics.
144
+ */
145
+ export type ProviderAcceptedMutationTargetEvidence = {
146
+ readonly schemaVersion: 1;
147
+ readonly runId: string;
148
+ /**
149
+ * Historical field name. For a provider-bound deletion target this is the
150
+ * instant the exact pre-read target was durably retained after dispatch
151
+ * start, before the mutation request left the process.
152
+ */
153
+ readonly acceptedAt: string;
154
+ readonly planDigest: string;
155
+ readonly adapter: RecoveryCapsule["adapter"];
156
+ readonly operation: string;
157
+ readonly inputHash: string;
158
+ readonly auth: RecoveryCapsule["auth"];
159
+ readonly contract: Extract<
160
+ RecoveryContractIdentity,
161
+ { readonly transport: "web-session-api" | "local-cli" }
162
+ >;
163
+ readonly dispatch: ProviderAcceptedMutationTargetDispatch;
164
+ readonly target: ProviderAcceptedMutationTarget;
165
+ };
166
+
167
+ type EncryptedProviderAcceptedMutationTargetEvidence = {
168
+ readonly schemaVersion: 1;
169
+ readonly encryption: "aes-256-gcm";
170
+ readonly runId: string;
171
+ readonly adapterHash: string;
172
+ readonly authHash: string;
173
+ readonly contractHash: string;
174
+ readonly inputHash: string;
175
+ readonly dispatchId: string;
176
+ readonly dispatchIndex: number;
177
+ readonly iv: string;
178
+ readonly ciphertext: string;
179
+ readonly tag: string;
180
+ };
181
+
182
+ type EncryptedRecoveryCapsule = {
183
+ readonly schemaVersion: 1;
184
+ readonly encryption: "aes-256-gcm";
185
+ readonly runId: string;
186
+ readonly authId: string;
187
+ readonly authHash: string;
188
+ readonly contractHash: string;
189
+ readonly iv: string;
190
+ readonly ciphertext: string;
191
+ readonly tag: string;
192
+ };
193
+
194
+ export type ReconciliationOutcome =
195
+ | "desired-state-observed"
196
+ | "desired-state-not-observed"
197
+ | "inconclusive";
198
+
199
+ export type ReconciliationObservation = {
200
+ readonly schemaVersion: 1;
201
+ readonly observationId: string;
202
+ readonly runId: string;
203
+ readonly observedAt: string;
204
+ readonly receiptHash: string;
205
+ readonly adapterHash: string;
206
+ readonly operation: string;
207
+ readonly inputHash: string;
208
+ readonly authHash: string;
209
+ readonly contractHash: string;
210
+ readonly inputSource: "capsule" | "provided";
211
+ readonly outcome: ReconciliationOutcome;
212
+ readonly desiredStateMatched: boolean | null;
213
+ readonly actualState: boolean | null;
214
+ readonly reason: "exact-readback" | "readback-failed";
215
+ };
216
+
217
+ function isRecord(value: unknown): value is JsonRecord {
218
+ return typeof value === "object" && value !== null && !Array.isArray(value);
219
+ }
220
+
221
+ function dataRecord(value: unknown, label: string): JsonRecord {
222
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
223
+ const prototype: unknown = Object.getPrototypeOf(value) as unknown;
224
+ if (prototype !== Object.prototype && prototype !== null) {
225
+ throw new Error(`${label} has an unsupported prototype`);
226
+ }
227
+ const descriptors = Object.getOwnPropertyDescriptors(value);
228
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")) {
229
+ throw new Error(`${label} has unsupported symbol fields`);
230
+ }
231
+ const result: JsonRecord = {};
232
+ for (const [key, descriptor] of Object.entries(descriptors)) {
233
+ if (!descriptor.enumerable || !("value" in descriptor)) {
234
+ throw new Error(`${label} has unsupported accessor fields`);
235
+ }
236
+ Object.defineProperty(result, key, {
237
+ value: descriptor.value,
238
+ enumerable: true,
239
+ configurable: false,
240
+ writable: false,
241
+ });
242
+ }
243
+ return result;
244
+ }
245
+
246
+ function exactKeys(value: JsonRecord, expected: readonly string[], label: string): void {
247
+ const actual = Object.keys(value).sort();
248
+ const wanted = [...expected].sort();
249
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
250
+ throw new Error(`${label} has unsupported fields`);
251
+ }
252
+ }
253
+
254
+ function assertHash(value: unknown, label: string): asserts value is string {
255
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
256
+ throw new Error(`${label} is malformed`);
257
+ }
258
+ }
259
+
260
+ function assertRunId(value: unknown, label = "recovery run ID"): asserts value is string {
261
+ if (
262
+ typeof value !== "string"
263
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value)
264
+ ) throw new Error(`${label} is malformed`);
265
+ }
266
+
267
+ function assertId(value: unknown, label: string): asserts value is string {
268
+ if (typeof value !== "string" || !/^[a-z][a-z0-9-]{0,47}$/u.test(value)) {
269
+ throw new Error(`${label} is malformed`);
270
+ }
271
+ }
272
+
273
+ function assertBoundedString(
274
+ value: unknown,
275
+ label: string,
276
+ maximum: number,
277
+ ): asserts value is string {
278
+ if (
279
+ typeof value !== "string"
280
+ || value.length < 1
281
+ || value.length > maximum
282
+ || /[\0\r\n]/u.test(value)
283
+ ) throw new Error(`${label} is malformed`);
284
+ }
285
+
286
+ function assertOperation(value: unknown, label: string): asserts value is string {
287
+ if (!isProviderPluginOperationName(value)) {
288
+ throw new Error(`${label} is malformed`);
289
+ }
290
+ }
291
+
292
+ function assertTimestamp(value: unknown, label: string): asserts value is string {
293
+ if (
294
+ typeof value !== "string"
295
+ || value.length > 64
296
+ || !Number.isFinite(Date.parse(value))
297
+ || new Date(value).toISOString() !== value
298
+ ) {
299
+ throw new Error(`${label} is malformed`);
300
+ }
301
+ }
302
+
303
+ function assertAuthKind(value: unknown): asserts value is GhostgetAuth["kind"] {
304
+ if (
305
+ value !== "cookie-source"
306
+ && value !== "cookies-file"
307
+ && value !== "browser-profile"
308
+ && value !== "oauth-token-file"
309
+ && value !== "linked-device-store"
310
+ ) throw new Error("recovery capsule auth kind is malformed");
311
+ }
312
+
313
+ function parseContract(value: unknown): RecoveryContractIdentity {
314
+ const record = dataRecord(value, "recovery capsule contract");
315
+ if (record.transport === "portable-provider-plugin") {
316
+ exactKeys(
317
+ record,
318
+ ["transport", "identity"],
319
+ "recovery capsule contract",
320
+ );
321
+ return Object.freeze({
322
+ transport: "portable-provider-plugin",
323
+ identity: parsePortableOperationIdentityV1(record.identity),
324
+ });
325
+ }
326
+ if (record.transport === "local-cli") {
327
+ exactKeys(record, ["transport", "identity"], "recovery capsule contract");
328
+ return Object.freeze({
329
+ transport: "local-cli",
330
+ identity: parseLocalCliContractIdentityV1(record.identity),
331
+ });
332
+ }
333
+ if (record.transport === "provider-api") {
334
+ exactKeys(record, ["transport", "provider", "action", "version", "hash"], "recovery capsule contract");
335
+ if (!isProviderPluginSurfaceId(record.provider)) {
336
+ throw new Error("recovery capsule provider is malformed");
337
+ }
338
+ assertOperation(record.action, "recovery capsule provider action");
339
+ if (!Number.isSafeInteger(record.version) || (record.version as number) < 1 || (record.version as number) > 1_000_000) {
340
+ throw new Error("recovery capsule provider version is malformed");
341
+ }
342
+ assertHash(record.hash, "recovery capsule provider contract hash");
343
+ return Object.freeze({
344
+ transport: "provider-api",
345
+ provider: record.provider,
346
+ action: record.action,
347
+ version: record.version as number,
348
+ hash: record.hash,
349
+ });
350
+ }
351
+ if (record.transport === "web-session-api") {
352
+ exactKeys(record, ["transport", "site", "action", "version", "hash"], "recovery capsule contract");
353
+ if (!isProviderPluginSurfaceId(record.site)) {
354
+ throw new Error("recovery capsule site is malformed");
355
+ }
356
+ assertOperation(record.action, "recovery capsule web-session action");
357
+ if (!Number.isSafeInteger(record.version) || (record.version as number) < 1 || (record.version as number) > 1_000_000) {
358
+ throw new Error("recovery capsule web-session version is malformed");
359
+ }
360
+ assertHash(record.hash, "recovery capsule web-session contract hash");
361
+ return Object.freeze({
362
+ transport: "web-session-api",
363
+ site: record.site,
364
+ action: record.action,
365
+ version: record.version as number,
366
+ hash: record.hash,
367
+ });
368
+ }
369
+ if (record.transport === "reviewed-template-api") {
370
+ exactKeys(record, ["transport", "version", "hash"], "recovery capsule contract");
371
+ if (record.version !== 1) throw new Error("recovery capsule reviewed-template version is malformed");
372
+ assertHash(record.hash, "recovery capsule reviewed-template contract hash");
373
+ return Object.freeze({
374
+ transport: "reviewed-template-api",
375
+ version: 1,
376
+ hash: record.hash,
377
+ });
378
+ }
379
+ throw new Error("recovery capsule transport is unsupported");
380
+ }
381
+
382
+ export function recoveryContractHash(
383
+ contractValue: RecoveryContractIdentity,
384
+ ): string {
385
+ const contract = parseContract(contractValue);
386
+ return contract.transport === "portable-provider-plugin"
387
+ ? sha256(
388
+ `${PORTABLE_RECOVERY_CONTRACT_HASH_DOMAIN}${canonicalJson(contract)}`,
389
+ )
390
+ : contract.transport === "local-cli"
391
+ ? contract.identity.hash
392
+ : contract.hash;
393
+ }
394
+
395
+ function parseCapsule(value: unknown): RecoveryCapsule {
396
+ if (!isRecord(value)) throw new Error("recovery capsule must be an object");
397
+ exactKeys(
398
+ value,
399
+ [
400
+ "schemaVersion",
401
+ "runId",
402
+ "createdAt",
403
+ "planDigest",
404
+ "adapter",
405
+ "operation",
406
+ "risk",
407
+ "input",
408
+ "inputHash",
409
+ "auth",
410
+ "contract",
411
+ ],
412
+ "recovery capsule",
413
+ );
414
+ if (value.schemaVersion !== 1) throw new Error("recovery capsule schema is unsupported");
415
+ assertRunId(value.runId);
416
+ assertTimestamp(value.createdAt, "recovery capsule creation time");
417
+ assertHash(value.planDigest, "recovery capsule plan digest");
418
+ assertOperation(value.operation, "recovery capsule operation");
419
+ if (value.risk !== "R2" && value.risk !== "R3") {
420
+ throw new Error("recovery capsule risk is not a recoverable write");
421
+ }
422
+ if (!isRecord(value.input)) throw new Error("recovery capsule input must be an object");
423
+ assertHash(value.inputHash, "recovery capsule input hash");
424
+ if (sha256(canonicalJson(value.input)) !== value.inputHash) {
425
+ throw new Error("recovery capsule input no longer matches its canonical hash");
426
+ }
427
+ if (!isRecord(value.adapter)) throw new Error("recovery capsule adapter must be an object");
428
+ exactKeys(value.adapter, ["id", "version", "hash"], "recovery capsule adapter");
429
+ assertId(value.adapter.id, "recovery capsule adapter ID");
430
+ assertBoundedString(value.adapter.version, "recovery capsule adapter version", 64);
431
+ assertHash(value.adapter.hash, "recovery capsule adapter hash");
432
+ if (!isRecord(value.auth)) throw new Error("recovery capsule auth must be an object");
433
+ exactKeys(value.auth, ["id", "hash", "kind"], "recovery capsule auth");
434
+ assertId(value.auth.id, "recovery capsule auth ID");
435
+ assertHash(value.auth.hash, "recovery capsule auth hash");
436
+ assertAuthKind(value.auth.kind);
437
+ return {
438
+ schemaVersion: 1,
439
+ runId: value.runId,
440
+ createdAt: value.createdAt,
441
+ planDigest: value.planDigest,
442
+ adapter: {
443
+ id: value.adapter.id,
444
+ version: value.adapter.version,
445
+ hash: value.adapter.hash,
446
+ },
447
+ operation: value.operation,
448
+ risk: value.risk,
449
+ input: value.input as OperationInput,
450
+ inputHash: value.inputHash,
451
+ auth: {
452
+ id: value.auth.id,
453
+ hash: value.auth.hash,
454
+ kind: value.auth.kind,
455
+ },
456
+ contract: parseContract(value.contract),
457
+ };
458
+ }
459
+
460
+ function providerAcceptedTargetHasUnpairedSurrogate(value: string): boolean {
461
+ for (let index = 0; index < value.length; index += 1) {
462
+ const code = value.charCodeAt(index);
463
+ if (code >= 0xd800 && code <= 0xdbff) {
464
+ const next = value.charCodeAt(index + 1);
465
+ if (!Number.isFinite(next) || next < 0xdc00 || next > 0xdfff) return true;
466
+ index += 1;
467
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
468
+ return true;
469
+ }
470
+ }
471
+ return false;
472
+ }
473
+
474
+ function parseProviderAcceptedMutationTarget(
475
+ value: unknown,
476
+ ): ProviderAcceptedMutationTarget {
477
+ const record = dataRecord(value, "provider-accepted mutation target");
478
+ exactKeys(
479
+ record,
480
+ ["schemaVersion", "identifier"],
481
+ "provider-accepted mutation target",
482
+ );
483
+ if (record.schemaVersion !== 1) {
484
+ throw new Error("provider-accepted mutation target schema is unsupported");
485
+ }
486
+ if (
487
+ typeof record.identifier !== "string"
488
+ || record.identifier.length < 1
489
+ || Buffer.byteLength(record.identifier, "utf8")
490
+ > MAX_PROVIDER_ACCEPTED_TARGET_BYTES
491
+ || providerAcceptedTargetHasUnpairedSurrogate(record.identifier)
492
+ || [...record.identifier].some((character) => {
493
+ const codePoint = character.codePointAt(0);
494
+ return codePoint === undefined
495
+ || codePoint === 0
496
+ || codePoint === 0x7f
497
+ || codePoint < 0x20;
498
+ })
499
+ ) {
500
+ throw new Error("provider-accepted mutation target identifier is malformed");
501
+ }
502
+ return Object.freeze({
503
+ schemaVersion: 1,
504
+ identifier: record.identifier,
505
+ });
506
+ }
507
+
508
+ function parseProviderAcceptedMutationTargetDispatch(
509
+ value: unknown,
510
+ ): ProviderAcceptedMutationTargetDispatch {
511
+ const record = dataRecord(
512
+ value,
513
+ "provider-accepted mutation target dispatch",
514
+ );
515
+ exactKeys(
516
+ record,
517
+ ["id", "index", "planned"],
518
+ "provider-accepted mutation target dispatch",
519
+ );
520
+ if (
521
+ typeof record.id !== "string"
522
+ || record.id.length > 256
523
+ || !/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*(?:\[[1-9][0-9]*\])?$/u
524
+ .test(record.id)
525
+ ) {
526
+ throw new Error("provider-accepted mutation target dispatch ID is malformed");
527
+ }
528
+ if (
529
+ !Number.isSafeInteger(record.index)
530
+ || !Number.isSafeInteger(record.planned)
531
+ || (record.index as number) < 1
532
+ || (record.planned as number) < 1
533
+ || (record.planned as number) > 25
534
+ || (record.index as number) > (record.planned as number)
535
+ ) {
536
+ throw new Error("provider-accepted mutation target dispatch position is malformed");
537
+ }
538
+ return Object.freeze({
539
+ id: record.id,
540
+ index: record.index as number,
541
+ planned: record.planned as number,
542
+ });
543
+ }
544
+
545
+ export function parseProviderAcceptedMutationTargetEvidence(
546
+ value: unknown,
547
+ ): ProviderAcceptedMutationTargetEvidence {
548
+ const record = dataRecord(
549
+ value,
550
+ "provider-accepted mutation target evidence",
551
+ );
552
+ exactKeys(
553
+ record,
554
+ [
555
+ "schemaVersion",
556
+ "runId",
557
+ "acceptedAt",
558
+ "planDigest",
559
+ "adapter",
560
+ "operation",
561
+ "inputHash",
562
+ "auth",
563
+ "contract",
564
+ "dispatch",
565
+ "target",
566
+ ],
567
+ "provider-accepted mutation target evidence",
568
+ );
569
+ if (record.schemaVersion !== 1) {
570
+ throw new Error("provider-accepted mutation target evidence schema is unsupported");
571
+ }
572
+ assertRunId(record.runId, "provider-accepted mutation target run ID");
573
+ assertTimestamp(
574
+ record.acceptedAt,
575
+ "provider-accepted mutation target acceptance time",
576
+ );
577
+ assertHash(record.planDigest, "provider-accepted mutation target plan digest");
578
+ assertOperation(record.operation, "provider-accepted mutation target operation");
579
+ assertHash(record.inputHash, "provider-accepted mutation target input hash");
580
+
581
+ const adapter = dataRecord(
582
+ record.adapter,
583
+ "provider-accepted mutation target adapter",
584
+ );
585
+ exactKeys(
586
+ adapter,
587
+ ["id", "version", "hash"],
588
+ "provider-accepted mutation target adapter",
589
+ );
590
+ assertId(adapter.id, "provider-accepted mutation target adapter ID");
591
+ assertBoundedString(
592
+ adapter.version,
593
+ "provider-accepted mutation target adapter version",
594
+ 64,
595
+ );
596
+ assertHash(adapter.hash, "provider-accepted mutation target adapter hash");
597
+
598
+ const auth = dataRecord(
599
+ record.auth,
600
+ "provider-accepted mutation target auth",
601
+ );
602
+ exactKeys(
603
+ auth,
604
+ ["id", "hash", "kind"],
605
+ "provider-accepted mutation target auth",
606
+ );
607
+ assertId(auth.id, "provider-accepted mutation target auth ID");
608
+ assertHash(auth.hash, "provider-accepted mutation target auth hash");
609
+ assertAuthKind(auth.kind);
610
+
611
+ const contract = parseContract(record.contract);
612
+ if (
613
+ contract.transport !== "web-session-api"
614
+ && contract.transport !== "local-cli"
615
+ ) {
616
+ throw new Error(
617
+ "provider-accepted mutation target requires a session or local CLI contract",
618
+ );
619
+ }
620
+ const contractAction = contract.transport === "local-cli"
621
+ ? contract.identity.action
622
+ : contract.action;
623
+ if (contractAction !== record.operation) {
624
+ throw new Error(
625
+ "provider-accepted mutation target operation does not match its contract",
626
+ );
627
+ }
628
+ const evidence: ProviderAcceptedMutationTargetEvidence = Object.freeze({
629
+ schemaVersion: 1,
630
+ runId: record.runId,
631
+ acceptedAt: record.acceptedAt,
632
+ planDigest: record.planDigest,
633
+ adapter: Object.freeze({
634
+ id: adapter.id,
635
+ version: adapter.version,
636
+ hash: adapter.hash,
637
+ }),
638
+ operation: record.operation,
639
+ inputHash: record.inputHash,
640
+ auth: Object.freeze({
641
+ id: auth.id,
642
+ hash: auth.hash,
643
+ kind: auth.kind,
644
+ }),
645
+ contract,
646
+ dispatch: parseProviderAcceptedMutationTargetDispatch(record.dispatch),
647
+ target: parseProviderAcceptedMutationTarget(record.target),
648
+ });
649
+ canonicalJson(evidence);
650
+ return evidence;
651
+ }
652
+
653
+ function recoveryRoot(environment: Environment): string {
654
+ return join(ghostgetStateHome(environment), RECOVERY_DIRECTORY);
655
+ }
656
+
657
+ function capsuleDirectory(environment: Environment): string {
658
+ return join(recoveryRoot(environment), CAPSULE_DIRECTORY);
659
+ }
660
+
661
+ function providerAcceptedTargetDirectory(environment: Environment): string {
662
+ return join(recoveryRoot(environment), PROVIDER_ACCEPTED_TARGET_DIRECTORY);
663
+ }
664
+
665
+ function providerAcceptedTargetRunDirectory(
666
+ runId: string,
667
+ environment: Environment,
668
+ ): string {
669
+ assertRunId(runId, "provider-accepted mutation target run ID");
670
+ return join(providerAcceptedTargetDirectory(environment), runId);
671
+ }
672
+
673
+ function providerAcceptedTargetPath(
674
+ runId: string,
675
+ dispatchIndex: number,
676
+ environment: Environment,
677
+ ): string {
678
+ if (
679
+ !Number.isSafeInteger(dispatchIndex)
680
+ || dispatchIndex < 1
681
+ || dispatchIndex > 25
682
+ ) {
683
+ throw new Error(
684
+ "provider-accepted mutation target dispatch index is malformed",
685
+ );
686
+ }
687
+ return join(
688
+ providerAcceptedTargetRunDirectory(runId, environment),
689
+ `${dispatchIndex}.json`,
690
+ );
691
+ }
692
+
693
+ function capsulePath(runId: string, environment: Environment): string {
694
+ assertRunId(runId);
695
+ return join(capsuleDirectory(environment), `${runId}.json`);
696
+ }
697
+
698
+ function observationRunDirectory(runId: string, environment: Environment): string {
699
+ assertRunId(runId);
700
+ return join(recoveryRoot(environment), OBSERVATION_DIRECTORY, runId);
701
+ }
702
+
703
+ function keyPath(environment: Environment): string {
704
+ return join(ghostgetStateHome(environment), RECOVERY_KEY);
705
+ }
706
+
707
+ function parseRecoveryKey(text: string): Buffer {
708
+ let parsed: unknown;
709
+ try {
710
+ parsed = JSON.parse(text.trim()) as unknown;
711
+ } catch {
712
+ throw new Error("recovery encryption key is malformed");
713
+ }
714
+ if (!isRecord(parsed)) throw new Error("recovery encryption key is malformed");
715
+ exactKeys(parsed, ["schemaVersion", "key"], "recovery encryption key");
716
+ if (
717
+ parsed.schemaVersion !== 1
718
+ || typeof parsed.key !== "string"
719
+ || !/^[a-f0-9]{64}$/u.test(parsed.key)
720
+ ) throw new Error("recovery encryption key is malformed");
721
+ return Buffer.from(parsed.key, "hex");
722
+ }
723
+
724
+ function createRecoveryKey(environment: Environment): Buffer {
725
+ const path = keyPath(environment);
726
+ if (
727
+ !existsSync(path)
728
+ && (
729
+ listPrivateStateDirectory(capsuleDirectory(environment), environment).length > 0
730
+ || listPrivateStateDirectory(
731
+ providerAcceptedTargetDirectory(environment),
732
+ environment,
733
+ ).length > 0
734
+ )
735
+ ) {
736
+ throw new Error(
737
+ "recovery encryption key is missing while encrypted recovery evidence still exists; refusing to replace it",
738
+ );
739
+ }
740
+ createPrivateJsonIfAbsent(path, {
741
+ schemaVersion: 1,
742
+ key: randomBytes(32).toString("hex"),
743
+ }, { environment, privateParent: true });
744
+ return parseRecoveryKey(readRegularFile(path, MAX_KEY_BYTES, "recovery encryption key"));
745
+ }
746
+
747
+ function readRecoveryKey(environment: Environment): Buffer {
748
+ return parseRecoveryKey(readRegularFile(
749
+ keyPath(environment),
750
+ MAX_KEY_BYTES,
751
+ "recovery encryption key",
752
+ ));
753
+ }
754
+
755
+ function capsuleAdditionalData(
756
+ runId: string,
757
+ authId: string,
758
+ authHash: string,
759
+ contractHash: string,
760
+ ): Buffer {
761
+ return Buffer.from(
762
+ `io-recovery-capsule-v1\0${runId}\0${authId}\0${authHash}\0${contractHash}`,
763
+ "utf8",
764
+ );
765
+ }
766
+
767
+ function providerAcceptedTargetAdditionalData(
768
+ evidence: Pick<
769
+ ProviderAcceptedMutationTargetEvidence,
770
+ "runId" | "planDigest" | "inputHash" | "operation"
771
+ > & {
772
+ readonly adapterHash: string;
773
+ readonly authHash: string;
774
+ readonly contractHash: string;
775
+ readonly dispatchId: string;
776
+ readonly dispatchIndex: number;
777
+ readonly dispatchPlanned: number;
778
+ },
779
+ ): Buffer {
780
+ return Buffer.from(
781
+ [
782
+ "io-provider-accepted-mutation-target-v1",
783
+ evidence.runId,
784
+ evidence.planDigest,
785
+ evidence.adapterHash,
786
+ evidence.operation,
787
+ evidence.inputHash,
788
+ evidence.authHash,
789
+ evidence.contractHash,
790
+ evidence.dispatchId,
791
+ String(evidence.dispatchIndex),
792
+ String(evidence.dispatchPlanned),
793
+ ].join("\0"),
794
+ "utf8",
795
+ );
796
+ }
797
+
798
+ function boundedBase64(value: unknown, label: string, maximumBytes: number): Buffer {
799
+ if (
800
+ typeof value !== "string"
801
+ || value.length > Math.ceil(maximumBytes / 3) * 4 + 4
802
+ || !/^[A-Za-z0-9+/]*={0,2}$/u.test(value)
803
+ ) throw new Error(`${label} is malformed`);
804
+ const bytes = Buffer.from(value, "base64");
805
+ if (
806
+ bytes.byteLength > maximumBytes
807
+ || bytes.toString("base64") !== value
808
+ ) throw new Error(`${label} is malformed`);
809
+ return bytes;
810
+ }
811
+
812
+ function parseEncryptedCapsule(value: unknown): EncryptedRecoveryCapsule {
813
+ if (!isRecord(value)) throw new Error("encrypted recovery capsule must be an object");
814
+ exactKeys(
815
+ value,
816
+ [
817
+ "schemaVersion",
818
+ "encryption",
819
+ "runId",
820
+ "authId",
821
+ "authHash",
822
+ "contractHash",
823
+ "iv",
824
+ "ciphertext",
825
+ "tag",
826
+ ],
827
+ "encrypted recovery capsule",
828
+ );
829
+ if (value.schemaVersion !== 1 || value.encryption !== "aes-256-gcm") {
830
+ throw new Error("encrypted recovery capsule schema is unsupported");
831
+ }
832
+ assertRunId(value.runId);
833
+ assertId(value.authId, "encrypted recovery capsule auth ID");
834
+ assertHash(value.authHash, "encrypted recovery capsule auth hash");
835
+ assertHash(value.contractHash, "encrypted recovery capsule contract hash");
836
+ boundedBase64(value.iv, "recovery capsule IV", 12);
837
+ boundedBase64(value.ciphertext, "recovery capsule ciphertext", MAX_CAPSULE_PLAINTEXT_BYTES + 16);
838
+ boundedBase64(value.tag, "recovery capsule authentication tag", 16);
839
+ return value as EncryptedRecoveryCapsule;
840
+ }
841
+
842
+ function parseEncryptedProviderAcceptedMutationTargetEvidence(
843
+ value: unknown,
844
+ ): EncryptedProviderAcceptedMutationTargetEvidence {
845
+ const record = dataRecord(
846
+ value,
847
+ "encrypted provider-accepted mutation target evidence",
848
+ );
849
+ exactKeys(
850
+ record,
851
+ [
852
+ "schemaVersion",
853
+ "encryption",
854
+ "runId",
855
+ "adapterHash",
856
+ "authHash",
857
+ "contractHash",
858
+ "inputHash",
859
+ "dispatchId",
860
+ "dispatchIndex",
861
+ "iv",
862
+ "ciphertext",
863
+ "tag",
864
+ ],
865
+ "encrypted provider-accepted mutation target evidence",
866
+ );
867
+ if (record.schemaVersion !== 1 || record.encryption !== "aes-256-gcm") {
868
+ throw new Error(
869
+ "encrypted provider-accepted mutation target evidence schema is unsupported",
870
+ );
871
+ }
872
+ assertRunId(record.runId, "encrypted provider-accepted mutation target run ID");
873
+ assertHash(
874
+ record.adapterHash,
875
+ "encrypted provider-accepted mutation target adapter hash",
876
+ );
877
+ assertHash(
878
+ record.authHash,
879
+ "encrypted provider-accepted mutation target auth hash",
880
+ );
881
+ assertHash(
882
+ record.contractHash,
883
+ "encrypted provider-accepted mutation target contract hash",
884
+ );
885
+ assertHash(
886
+ record.inputHash,
887
+ "encrypted provider-accepted mutation target input hash",
888
+ );
889
+ if (
890
+ typeof record.dispatchId !== "string"
891
+ || record.dispatchId.length > 256
892
+ || !/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*(?:\[[1-9][0-9]*\])?$/u
893
+ .test(record.dispatchId)
894
+ || !Number.isSafeInteger(record.dispatchIndex)
895
+ || (record.dispatchIndex as number) < 1
896
+ || (record.dispatchIndex as number) > 25
897
+ ) {
898
+ throw new Error(
899
+ "encrypted provider-accepted mutation target dispatch is malformed",
900
+ );
901
+ }
902
+ boundedBase64(record.iv, "provider-accepted mutation target IV", 12);
903
+ boundedBase64(
904
+ record.ciphertext,
905
+ "provider-accepted mutation target ciphertext",
906
+ MAX_PROVIDER_ACCEPTED_TARGET_PLAINTEXT_BYTES + 16,
907
+ );
908
+ boundedBase64(
909
+ record.tag,
910
+ "provider-accepted mutation target authentication tag",
911
+ 16,
912
+ );
913
+ return Object.freeze({
914
+ schemaVersion: 1,
915
+ encryption: "aes-256-gcm",
916
+ runId: record.runId,
917
+ adapterHash: record.adapterHash,
918
+ authHash: record.authHash,
919
+ contractHash: record.contractHash,
920
+ inputHash: record.inputHash,
921
+ dispatchId: record.dispatchId,
922
+ dispatchIndex: record.dispatchIndex as number,
923
+ iv: record.iv as string,
924
+ ciphertext: record.ciphertext as string,
925
+ tag: record.tag as string,
926
+ });
927
+ }
928
+
929
+ function decryptCapsule(
930
+ encrypted: EncryptedRecoveryCapsule,
931
+ environment: Environment,
932
+ ): RecoveryCapsule {
933
+ const iv = boundedBase64(encrypted.iv, "recovery capsule IV", 12);
934
+ const ciphertext = boundedBase64(
935
+ encrypted.ciphertext,
936
+ "recovery capsule ciphertext",
937
+ MAX_CAPSULE_PLAINTEXT_BYTES + 16,
938
+ );
939
+ const tag = boundedBase64(encrypted.tag, "recovery capsule authentication tag", 16);
940
+ if (iv.byteLength !== 12 || tag.byteLength !== 16) {
941
+ throw new Error("encrypted recovery capsule has invalid cryptographic parameters");
942
+ }
943
+ let plaintext: Buffer;
944
+ try {
945
+ const decipher = createDecipheriv("aes-256-gcm", readRecoveryKey(environment), iv);
946
+ decipher.setAAD(capsuleAdditionalData(
947
+ encrypted.runId,
948
+ encrypted.authId,
949
+ encrypted.authHash,
950
+ encrypted.contractHash,
951
+ ));
952
+ decipher.setAuthTag(tag);
953
+ plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
954
+ } catch (error) {
955
+ throw new Error("encrypted recovery capsule failed authentication", { cause: error });
956
+ }
957
+ if (plaintext.byteLength > MAX_CAPSULE_PLAINTEXT_BYTES) {
958
+ throw new Error("decrypted recovery capsule exceeded its byte bound");
959
+ }
960
+ let parsed: unknown;
961
+ try {
962
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext)) as unknown;
963
+ } catch {
964
+ throw new Error("decrypted recovery capsule is malformed");
965
+ }
966
+ const capsule = parseCapsule(parsed);
967
+ const contractHash = recoveryContractHash(capsule.contract);
968
+ if (
969
+ capsule.runId !== encrypted.runId
970
+ || capsule.auth.id !== encrypted.authId
971
+ || capsule.auth.hash !== encrypted.authHash
972
+ || contractHash !== encrypted.contractHash
973
+ ) throw new Error("encrypted recovery capsule coordinates do not match its plaintext");
974
+ return capsule;
975
+ }
976
+
977
+ export function writeRecoveryCapsule(
978
+ capsuleValue: RecoveryCapsule,
979
+ environment: Environment = process.env,
980
+ ): void {
981
+ const capsule = parseCapsule(capsuleValue);
982
+ const plaintext = Buffer.from(canonicalJson(capsule), "utf8");
983
+ if (plaintext.byteLength > MAX_CAPSULE_PLAINTEXT_BYTES) {
984
+ throw new Error("recovery capsule exceeded its plaintext byte bound");
985
+ }
986
+ ensurePrivateStateDirectory(capsuleDirectory(environment), environment);
987
+ const contractHash = recoveryContractHash(capsule.contract);
988
+ const iv = randomBytes(12);
989
+ const cipher = createCipheriv("aes-256-gcm", createRecoveryKey(environment), iv);
990
+ cipher.setAAD(capsuleAdditionalData(
991
+ capsule.runId,
992
+ capsule.auth.id,
993
+ capsule.auth.hash,
994
+ contractHash,
995
+ ));
996
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
997
+ const encrypted: EncryptedRecoveryCapsule = {
998
+ schemaVersion: 1,
999
+ encryption: "aes-256-gcm",
1000
+ runId: capsule.runId,
1001
+ authId: capsule.auth.id,
1002
+ authHash: capsule.auth.hash,
1003
+ contractHash,
1004
+ iv: iv.toString("base64"),
1005
+ ciphertext: ciphertext.toString("base64"),
1006
+ tag: cipher.getAuthTag().toString("base64"),
1007
+ };
1008
+ const path = capsulePath(capsule.runId, environment);
1009
+ const created = createPrivateJsonIfAbsent(path, encrypted, {
1010
+ environment,
1011
+ privateParent: true,
1012
+ });
1013
+ if (created.created) return;
1014
+ const existing = readRecoveryCapsule(
1015
+ capsule.runId,
1016
+ capsule.auth.id,
1017
+ capsule.auth.hash,
1018
+ environment,
1019
+ );
1020
+ if (existing === null || canonicalJson(existing) !== canonicalJson(capsule)) {
1021
+ throw new Error("an existing recovery capsule does not match this run");
1022
+ }
1023
+ }
1024
+
1025
+ export function readRecoveryCapsule(
1026
+ runId: string,
1027
+ authId: string,
1028
+ authHash: string,
1029
+ environment: Environment = process.env,
1030
+ ): RecoveryCapsule | null {
1031
+ assertRunId(runId);
1032
+ assertId(authId, "recovery capsule auth ID");
1033
+ assertHash(authHash, "recovery capsule auth hash");
1034
+ ensurePrivateStateDirectory(capsuleDirectory(environment), environment);
1035
+ const text = readPrivateStateFileIfPresent(
1036
+ capsulePath(runId, environment),
1037
+ MAX_CAPSULE_ENCRYPTED_BYTES,
1038
+ "encrypted recovery capsule",
1039
+ environment,
1040
+ );
1041
+ if (text === null) return null;
1042
+ let parsed: unknown;
1043
+ try {
1044
+ parsed = JSON.parse(text) as unknown;
1045
+ } catch {
1046
+ throw new Error("encrypted recovery capsule is malformed");
1047
+ }
1048
+ const encrypted = parseEncryptedCapsule(parsed);
1049
+ if (
1050
+ encrypted.runId !== runId
1051
+ || encrypted.authId !== authId
1052
+ || encrypted.authHash !== authHash
1053
+ ) throw new Error("encrypted recovery capsule is bound to different run or auth coordinates");
1054
+ return decryptCapsule(encrypted, environment);
1055
+ }
1056
+
1057
+ /**
1058
+ * Enumerate encrypted recovery capsules without trusting filenames, headers,
1059
+ * or plaintext coordinates. Invalid or concurrently changed entries remain
1060
+ * visible so lifecycle decisions can fail closed instead of silently skipping
1061
+ * unknown recovery ownership.
1062
+ */
1063
+ export function listRecoveryCapsuleSnapshots(
1064
+ environment: Environment = process.env,
1065
+ ): readonly RecoveryCapsuleListEntry[] {
1066
+ const directory = capsuleDirectory(environment);
1067
+ const snapshot = snapshotPrivateStateDirectory(directory, environment);
1068
+ const directoryIdentity = snapshot.identity;
1069
+ if (directoryIdentity === null) return Object.freeze([]);
1070
+ const candidates = snapshot.entries.filter((entry) =>
1071
+ /^[0-9a-f-]{36}\.json$/u.test(entry.name)
1072
+ );
1073
+ return Object.freeze(candidates.map((entry): RecoveryCapsuleListEntry => {
1074
+ const runId = entry.name.slice(0, -5);
1075
+ if (entry.kind !== "file") return { runId, invalid: true };
1076
+ try {
1077
+ const content = readRegularFile(
1078
+ join(directory, entry.name),
1079
+ MAX_CAPSULE_ENCRYPTED_BYTES,
1080
+ "encrypted recovery capsule",
1081
+ directoryIdentity,
1082
+ );
1083
+ const encrypted = parseEncryptedCapsule(
1084
+ JSON.parse(content) as unknown,
1085
+ );
1086
+ if (encrypted.runId !== runId) return { runId, invalid: true };
1087
+ const capsule = decryptCapsule(encrypted, environment);
1088
+ return { capsule };
1089
+ } catch {
1090
+ return { runId, invalid: true };
1091
+ }
1092
+ }));
1093
+ }
1094
+
1095
+ export function removeRecoveryCapsule(
1096
+ runId: string,
1097
+ environment: Environment = process.env,
1098
+ ): boolean {
1099
+ return removePrivateStateFile(capsulePath(runId, environment), environment);
1100
+ }
1101
+
1102
+ type ProviderAcceptedMutationTargetBinding = Omit<
1103
+ ProviderAcceptedMutationTargetEvidence,
1104
+ "schemaVersion" | "acceptedAt" | "target"
1105
+ >;
1106
+
1107
+ function providerAcceptedMutationTargetBinding(
1108
+ evidence: ProviderAcceptedMutationTargetEvidence,
1109
+ ): ProviderAcceptedMutationTargetBinding {
1110
+ return {
1111
+ runId: evidence.runId,
1112
+ planDigest: evidence.planDigest,
1113
+ adapter: evidence.adapter,
1114
+ operation: evidence.operation,
1115
+ inputHash: evidence.inputHash,
1116
+ auth: evidence.auth,
1117
+ contract: evidence.contract,
1118
+ dispatch: evidence.dispatch,
1119
+ };
1120
+ }
1121
+
1122
+ function providerAcceptedMutationTargetBindingFromCapsule(
1123
+ capsuleValue: unknown,
1124
+ dispatchValue: unknown,
1125
+ ): ProviderAcceptedMutationTargetBinding {
1126
+ const capsule = parseCapsule(capsuleValue);
1127
+ if (
1128
+ capsule.contract.transport !== "web-session-api"
1129
+ && capsule.contract.transport !== "local-cli"
1130
+ ) {
1131
+ throw new Error(
1132
+ "provider-accepted mutation target requires a session or local CLI recovery capsule",
1133
+ );
1134
+ }
1135
+ return {
1136
+ runId: capsule.runId,
1137
+ planDigest: capsule.planDigest,
1138
+ adapter: capsule.adapter,
1139
+ operation: capsule.operation,
1140
+ inputHash: capsule.inputHash,
1141
+ auth: capsule.auth,
1142
+ contract: capsule.contract,
1143
+ dispatch: parseProviderAcceptedMutationTargetDispatch(dispatchValue),
1144
+ };
1145
+ }
1146
+
1147
+ function decryptProviderAcceptedMutationTargetEvidence(
1148
+ encrypted: EncryptedProviderAcceptedMutationTargetEvidence,
1149
+ expected: ProviderAcceptedMutationTargetBinding,
1150
+ environment: Environment,
1151
+ ): ProviderAcceptedMutationTargetEvidence {
1152
+ const contractHash = recoveryContractHash(expected.contract);
1153
+ if (
1154
+ encrypted.runId !== expected.runId
1155
+ || encrypted.adapterHash !== expected.adapter.hash
1156
+ || encrypted.authHash !== expected.auth.hash
1157
+ || encrypted.contractHash !== contractHash
1158
+ || encrypted.inputHash !== expected.inputHash
1159
+ || encrypted.dispatchId !== expected.dispatch.id
1160
+ || encrypted.dispatchIndex !== expected.dispatch.index
1161
+ ) {
1162
+ throw new Error(
1163
+ "encrypted provider-accepted mutation target is bound to different run coordinates",
1164
+ );
1165
+ }
1166
+ const iv = boundedBase64(
1167
+ encrypted.iv,
1168
+ "provider-accepted mutation target IV",
1169
+ 12,
1170
+ );
1171
+ const ciphertext = boundedBase64(
1172
+ encrypted.ciphertext,
1173
+ "provider-accepted mutation target ciphertext",
1174
+ MAX_PROVIDER_ACCEPTED_TARGET_PLAINTEXT_BYTES + 16,
1175
+ );
1176
+ const tag = boundedBase64(
1177
+ encrypted.tag,
1178
+ "provider-accepted mutation target authentication tag",
1179
+ 16,
1180
+ );
1181
+ if (iv.byteLength !== 12 || tag.byteLength !== 16) {
1182
+ throw new Error(
1183
+ "encrypted provider-accepted mutation target has invalid cryptographic parameters",
1184
+ );
1185
+ }
1186
+ let plaintext: Buffer;
1187
+ try {
1188
+ const decipher = createDecipheriv(
1189
+ "aes-256-gcm",
1190
+ readRecoveryKey(environment),
1191
+ iv,
1192
+ );
1193
+ decipher.setAAD(providerAcceptedTargetAdditionalData({
1194
+ runId: expected.runId,
1195
+ planDigest: expected.planDigest,
1196
+ adapterHash: expected.adapter.hash,
1197
+ operation: expected.operation,
1198
+ inputHash: expected.inputHash,
1199
+ authHash: expected.auth.hash,
1200
+ contractHash,
1201
+ dispatchId: expected.dispatch.id,
1202
+ dispatchIndex: expected.dispatch.index,
1203
+ dispatchPlanned: expected.dispatch.planned,
1204
+ }));
1205
+ decipher.setAuthTag(tag);
1206
+ plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1207
+ } catch (error) {
1208
+ throw new Error(
1209
+ "encrypted provider-accepted mutation target failed authentication",
1210
+ { cause: error },
1211
+ );
1212
+ }
1213
+ if (plaintext.byteLength > MAX_PROVIDER_ACCEPTED_TARGET_PLAINTEXT_BYTES) {
1214
+ throw new Error(
1215
+ "decrypted provider-accepted mutation target exceeded its byte bound",
1216
+ );
1217
+ }
1218
+ let parsed: unknown;
1219
+ try {
1220
+ parsed = JSON.parse(
1221
+ new TextDecoder("utf-8", { fatal: true }).decode(plaintext),
1222
+ ) as unknown;
1223
+ } catch {
1224
+ throw new Error("decrypted provider-accepted mutation target is malformed");
1225
+ }
1226
+ const evidence = parseProviderAcceptedMutationTargetEvidence(parsed);
1227
+ if (
1228
+ canonicalJson(providerAcceptedMutationTargetBinding(evidence))
1229
+ !== canonicalJson(expected)
1230
+ ) {
1231
+ throw new Error(
1232
+ "decrypted provider-accepted mutation target does not match its run binding",
1233
+ );
1234
+ }
1235
+ return evidence;
1236
+ }
1237
+
1238
+ /**
1239
+ * Persist one provider-accepted or pre-dispatch provider-bound target before
1240
+ * any independent readback. A byte-for-byte equivalent repeated callback is
1241
+ * idempotent; any contradictory duplicate fails closed and leaves the first
1242
+ * encrypted evidence untouched.
1243
+ */
1244
+ export function writeProviderAcceptedMutationTargetEvidence(
1245
+ evidenceValue: unknown,
1246
+ environment: Environment = process.env,
1247
+ ): void {
1248
+ const evidence = parseProviderAcceptedMutationTargetEvidence(evidenceValue);
1249
+ const plaintext = Buffer.from(canonicalJson(evidence), "utf8");
1250
+ if (plaintext.byteLength > MAX_PROVIDER_ACCEPTED_TARGET_PLAINTEXT_BYTES) {
1251
+ throw new Error(
1252
+ "provider-accepted mutation target exceeded its plaintext byte bound",
1253
+ );
1254
+ }
1255
+ const recoveryKey = readRecoveryKey(environment);
1256
+ ensurePrivateStateDirectory(
1257
+ providerAcceptedTargetRunDirectory(evidence.runId, environment),
1258
+ environment,
1259
+ );
1260
+ const contractHash = recoveryContractHash(evidence.contract);
1261
+ const iv = randomBytes(12);
1262
+ const cipher = createCipheriv(
1263
+ "aes-256-gcm",
1264
+ recoveryKey,
1265
+ iv,
1266
+ );
1267
+ cipher.setAAD(providerAcceptedTargetAdditionalData({
1268
+ runId: evidence.runId,
1269
+ planDigest: evidence.planDigest,
1270
+ adapterHash: evidence.adapter.hash,
1271
+ operation: evidence.operation,
1272
+ inputHash: evidence.inputHash,
1273
+ authHash: evidence.auth.hash,
1274
+ contractHash,
1275
+ dispatchId: evidence.dispatch.id,
1276
+ dispatchIndex: evidence.dispatch.index,
1277
+ dispatchPlanned: evidence.dispatch.planned,
1278
+ }));
1279
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1280
+ const encrypted: EncryptedProviderAcceptedMutationTargetEvidence = {
1281
+ schemaVersion: 1,
1282
+ encryption: "aes-256-gcm",
1283
+ runId: evidence.runId,
1284
+ adapterHash: evidence.adapter.hash,
1285
+ authHash: evidence.auth.hash,
1286
+ contractHash,
1287
+ inputHash: evidence.inputHash,
1288
+ dispatchId: evidence.dispatch.id,
1289
+ dispatchIndex: evidence.dispatch.index,
1290
+ iv: iv.toString("base64"),
1291
+ ciphertext: ciphertext.toString("base64"),
1292
+ tag: cipher.getAuthTag().toString("base64"),
1293
+ };
1294
+ const path = providerAcceptedTargetPath(
1295
+ evidence.runId,
1296
+ evidence.dispatch.index,
1297
+ environment,
1298
+ );
1299
+ const created = createPrivateJsonIfAbsent(path, encrypted, {
1300
+ environment,
1301
+ privateParent: true,
1302
+ });
1303
+ if (created.created) return;
1304
+ const text = readPrivateStateFileIfPresent(
1305
+ path,
1306
+ MAX_PROVIDER_ACCEPTED_TARGET_ENCRYPTED_BYTES,
1307
+ "encrypted provider-accepted mutation target evidence",
1308
+ environment,
1309
+ );
1310
+ if (text === null) {
1311
+ throw new Error(
1312
+ "provider-accepted mutation target changed during create-once verification",
1313
+ );
1314
+ }
1315
+ let existingValue: unknown;
1316
+ try {
1317
+ existingValue = JSON.parse(text) as unknown;
1318
+ } catch {
1319
+ throw new Error("encrypted provider-accepted mutation target is malformed");
1320
+ }
1321
+ const existing = decryptProviderAcceptedMutationTargetEvidence(
1322
+ parseEncryptedProviderAcceptedMutationTargetEvidence(existingValue),
1323
+ providerAcceptedMutationTargetBinding(evidence),
1324
+ environment,
1325
+ );
1326
+ if (canonicalJson(existing) !== canonicalJson(evidence)) {
1327
+ throw new Error(
1328
+ "a contradictory provider-accepted mutation target already exists for this dispatch",
1329
+ );
1330
+ }
1331
+ }
1332
+
1333
+ /**
1334
+ * Read only evidence whose complete run and dispatch binding matches the exact
1335
+ * encrypted recovery capsule. Historical runs without evidence return null;
1336
+ * callers must not infer or manufacture a replacement target.
1337
+ */
1338
+ export function readProviderAcceptedMutationTargetEvidence(
1339
+ capsuleValue: unknown,
1340
+ dispatchValue: unknown,
1341
+ environment: Environment = process.env,
1342
+ ): ProviderAcceptedMutationTargetEvidence | null {
1343
+ const expected = providerAcceptedMutationTargetBindingFromCapsule(
1344
+ capsuleValue,
1345
+ dispatchValue,
1346
+ );
1347
+ const text = readPrivateStateFileIfPresent(
1348
+ providerAcceptedTargetPath(
1349
+ expected.runId,
1350
+ expected.dispatch.index,
1351
+ environment,
1352
+ ),
1353
+ MAX_PROVIDER_ACCEPTED_TARGET_ENCRYPTED_BYTES,
1354
+ "encrypted provider-accepted mutation target evidence",
1355
+ environment,
1356
+ );
1357
+ if (text === null) return null;
1358
+ let parsed: unknown;
1359
+ try {
1360
+ parsed = JSON.parse(text) as unknown;
1361
+ } catch {
1362
+ throw new Error("encrypted provider-accepted mutation target is malformed");
1363
+ }
1364
+ return decryptProviderAcceptedMutationTargetEvidence(
1365
+ parseEncryptedProviderAcceptedMutationTargetEvidence(parsed),
1366
+ expected,
1367
+ environment,
1368
+ );
1369
+ }
1370
+
1371
+ export function removeProviderAcceptedMutationTargetEvidence(
1372
+ runId: string,
1373
+ environment: Environment = process.env,
1374
+ ): boolean {
1375
+ assertRunId(runId, "provider-accepted mutation target run ID");
1376
+ return removePrivateStateDirectoryTree(
1377
+ providerAcceptedTargetRunDirectory(runId, environment),
1378
+ environment,
1379
+ );
1380
+ }
1381
+
1382
+ function parseObservation(value: unknown): ReconciliationObservation {
1383
+ if (!isRecord(value)) throw new Error("reconciliation observation must be an object");
1384
+ exactKeys(
1385
+ value,
1386
+ [
1387
+ "schemaVersion",
1388
+ "observationId",
1389
+ "runId",
1390
+ "observedAt",
1391
+ "receiptHash",
1392
+ "adapterHash",
1393
+ "operation",
1394
+ "inputHash",
1395
+ "authHash",
1396
+ "contractHash",
1397
+ "inputSource",
1398
+ "outcome",
1399
+ "desiredStateMatched",
1400
+ "actualState",
1401
+ "reason",
1402
+ ],
1403
+ "reconciliation observation",
1404
+ );
1405
+ if (value.schemaVersion !== 1) throw new Error("reconciliation observation schema is unsupported");
1406
+ assertRunId(value.observationId, "reconciliation observation ID");
1407
+ assertRunId(value.runId, "reconciliation run ID");
1408
+ assertTimestamp(value.observedAt, "reconciliation observation time");
1409
+ assertHash(value.receiptHash, "reconciliation receipt hash");
1410
+ assertHash(value.adapterHash, "reconciliation adapter hash");
1411
+ assertOperation(value.operation, "reconciliation operation");
1412
+ assertHash(value.inputHash, "reconciliation input hash");
1413
+ assertHash(value.authHash, "reconciliation auth hash");
1414
+ assertHash(value.contractHash, "reconciliation contract hash");
1415
+ if (value.inputSource !== "capsule" && value.inputSource !== "provided") {
1416
+ throw new Error("reconciliation input source is malformed");
1417
+ }
1418
+ if (
1419
+ value.outcome !== "desired-state-observed"
1420
+ && value.outcome !== "desired-state-not-observed"
1421
+ && value.outcome !== "inconclusive"
1422
+ ) throw new Error("reconciliation outcome is malformed");
1423
+ if (
1424
+ value.desiredStateMatched !== null
1425
+ && typeof value.desiredStateMatched !== "boolean"
1426
+ ) throw new Error("reconciliation desired-state match is malformed");
1427
+ if (value.actualState !== null && typeof value.actualState !== "boolean") {
1428
+ throw new Error("reconciliation actual state is malformed");
1429
+ }
1430
+ if (value.reason !== "exact-readback" && value.reason !== "readback-failed") {
1431
+ throw new Error("reconciliation reason is malformed");
1432
+ }
1433
+ if (
1434
+ value.outcome === "inconclusive"
1435
+ ? (
1436
+ value.desiredStateMatched !== null
1437
+ || value.actualState !== null
1438
+ || value.reason !== "readback-failed"
1439
+ )
1440
+ : (
1441
+ typeof value.desiredStateMatched !== "boolean"
1442
+ || typeof value.actualState !== "boolean"
1443
+ || value.reason !== "exact-readback"
1444
+ || (value.outcome === "desired-state-observed") !== value.desiredStateMatched
1445
+ )
1446
+ ) throw new Error("reconciliation observation fields disagree");
1447
+ return value as ReconciliationObservation;
1448
+ }
1449
+
1450
+ export function appendReconciliationObservation(
1451
+ observationValue: ReconciliationObservation,
1452
+ environment: Environment = process.env,
1453
+ ): string {
1454
+ const observation = parseObservation(observationValue);
1455
+ const directory = observationRunDirectory(observation.runId, environment);
1456
+ const identity = ensurePrivateStateDirectory(directory, environment);
1457
+ const path = join(directory, `${observation.observationId}.json`);
1458
+ const result = createPrivateJsonIfAbsent(path, observation, {
1459
+ environment,
1460
+ expectedStateParent: identity,
1461
+ });
1462
+ if (!result.created) throw new Error("reconciliation observation ID already exists");
1463
+ return path;
1464
+ }
1465
+
1466
+ export function listReconciliationObservations(
1467
+ runId: string,
1468
+ environment: Environment = process.env,
1469
+ ): readonly ReconciliationObservation[] {
1470
+ const directory = observationRunDirectory(runId, environment);
1471
+ const identity = ensurePrivateStateDirectory(directory, environment);
1472
+ return listPrivateStateDirectory(directory, environment, identity)
1473
+ .filter((entry) => entry.kind === "file" && /^[0-9a-f-]{36}\.json$/u.test(entry.name))
1474
+ .map((entry) => {
1475
+ const path = join(directory, entry.name);
1476
+ const text = readRegularFile(path, MAX_OBSERVATION_BYTES, "reconciliation observation", identity);
1477
+ let parsed: unknown;
1478
+ try {
1479
+ parsed = JSON.parse(text) as unknown;
1480
+ } catch {
1481
+ throw new Error("reconciliation observation is malformed");
1482
+ }
1483
+ const observation = parseObservation(parsed);
1484
+ if (observation.runId !== runId || `${observation.observationId}.json` !== entry.name) {
1485
+ throw new Error("reconciliation observation coordinates do not match its path");
1486
+ }
1487
+ return observation;
1488
+ })
1489
+ .sort((left, right) =>
1490
+ Date.parse(left.observedAt) - Date.parse(right.observedAt)
1491
+ || left.observationId.localeCompare(right.observationId));
1492
+ }