@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,1037 @@
1
+ /**
2
+ * WhatsApp linked-device protocol policy and bounded local projections.
3
+ *
4
+ * This provider is deliberately not a WhatsApp Web DOM adapter. Its reviewed
5
+ * transport is a pinned `wacli`/whatsmeow linked device plus two private SQLite
6
+ * stores: `session.db` for Noise/Signal device state and `wacli.db` for the
7
+ * locally synced message projection.
8
+ */
9
+
10
+ import type { OperationInput } from "../model";
11
+ import { isCanonicalWhatsAppAccountId } from "./whatsapp-account-identity";
12
+
13
+ export const WHATSAPP_PROTOCOL_PIN = Object.freeze({
14
+ implementation: "github.com/openclaw/wacli",
15
+ version: "0.15.0",
16
+ tag: "v0.15.0",
17
+ commit: "a020de724180d31eccfa5241d45443402d62fb06",
18
+ transport: "official-release",
19
+ darwinArm64Archive:
20
+ "wacli_0.15.0_darwin_arm64.tar.gz",
21
+ darwinArm64ArchiveSha256:
22
+ "2b54f33d246e913a5c33525b4fc895a345363c2dcc673c70fa5f19cffb15d17d",
23
+ darwinArm64BinarySha256:
24
+ "a900af4d0dfd10471bcdf74105b9f256d1a08574242a041df3e5985a548826aa",
25
+ signature: Object.freeze({
26
+ authority:
27
+ "Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)",
28
+ identifier: "org.openclaw.wacli",
29
+ teamIdentifier: "FWJYW4S8P8",
30
+ designatedRequirement:
31
+ 'designated => identifier "org.openclaw.wacli" and anchor apple generic and certificate leaf[subject.OU] = FWJYW4S8P8',
32
+ cdHash: "a67b5d50877d6a2c3386d969d24dfc991bcc6a85",
33
+ cdHashFull:
34
+ "a67b5d50877d6a2c3386d969d24dfc991bcc6a8571a3343afc82e8d6de32e486",
35
+ hardenedRuntime: true,
36
+ notarized: true,
37
+ }),
38
+ } as const);
39
+
40
+ export const WHATSAPP_WEB_OPERATION_NAMES = Object.freeze([
41
+ "contacts.list",
42
+ "content.edit",
43
+ "content.save",
44
+ "content.share",
45
+ "media.read",
46
+ "messaging.list",
47
+ "messaging.read",
48
+ "messaging.send",
49
+ "reactions.set",
50
+ ] as const);
51
+
52
+ export type WhatsAppWebOperationName =
53
+ (typeof WHATSAPP_WEB_OPERATION_NAMES)[number];
54
+ export type WhatsAppWebContractState = "observed" | "capture-required";
55
+ export type WhatsAppWebRisk = "R1" | "R2" | "R3" | "R4";
56
+
57
+ export type WhatsAppWebOperationContract = {
58
+ readonly effect: "read" | "write";
59
+ readonly risk: WhatsAppWebRisk;
60
+ readonly state: WhatsAppWebContractState;
61
+ readonly reason: string;
62
+ };
63
+
64
+ function captureRequired(
65
+ effect: "read" | "write",
66
+ risk: WhatsAppWebRisk,
67
+ reason: string,
68
+ ): WhatsAppWebOperationContract {
69
+ return Object.freeze({ effect, risk, state: "capture-required", reason });
70
+ }
71
+
72
+ function observed(
73
+ effect: "read" | "write",
74
+ risk: WhatsAppWebRisk,
75
+ reason: string,
76
+ ): WhatsAppWebOperationContract {
77
+ return Object.freeze({ effect, risk, state: "observed", reason });
78
+ }
79
+
80
+ export const WHATSAPP_WEB_OPERATIONS = Object.freeze({
81
+ "contacts.list": observed(
82
+ "read",
83
+ "R1",
84
+ "the account-bound Whatsmeow session.db contact table and content-free wacli.db insert evidence are projected by fixed killable read-only helpers after exact pinned-schema, owner, quiescence, and file-identity validation; rowid pages expose their exact database generation and never claim remote-history completeness",
85
+ ),
86
+ "messaging.list": observed(
87
+ "read",
88
+ "R1",
89
+ "the pinned read-only local projection is account-bound and a paired nonempty fixture proved bounded chat listing without opening a WhatsApp connection",
90
+ ),
91
+ "messaging.read": observed(
92
+ "read",
93
+ "R1",
94
+ "the pinned read-only local projection binds one exact conversation and a paired nonempty fixture proved bounded message listing without opening a WhatsApp connection",
95
+ ),
96
+ "media.read": observed(
97
+ "read",
98
+ "R1",
99
+ "the pinned metadata-only local projection is account-bound and a paired fixture proved exact attachment lookup while omitting paths and media keys",
100
+ ),
101
+ "reactions.set": captureRequired(
102
+ "write",
103
+ "R2",
104
+ "the exact linked-device reaction command and local readback are reserved, but create/remove fixtures and process-private payload delivery are not yet proven",
105
+ ),
106
+ "content.save": captureRequired(
107
+ "write",
108
+ "R2",
109
+ "the pinned protocol client can read starred state but does not expose a reviewed star/unstar mutation",
110
+ ),
111
+ "messaging.send": captureRequired(
112
+ "write",
113
+ "R3",
114
+ "the official read runtime exposes no Wrench-qualified mutation transport; sending remains unavailable until a separate process-private transport, controlled live fixture, and exact accepted-message reconciliation are proven",
115
+ ),
116
+ "content.edit": captureRequired(
117
+ "write",
118
+ "R3",
119
+ "the exact recent-self-message edit and local readback are reserved, but a low-stakes live fixture and process-private message delivery are not yet proven",
120
+ ),
121
+ "content.share": captureRequired(
122
+ "write",
123
+ "R3",
124
+ "the exact one-message forward and destination readback are reserved, but a low-stakes live fixture must still prove text and media variants",
125
+ ),
126
+ } as const satisfies Readonly<
127
+ Record<WhatsAppWebOperationName, WhatsAppWebOperationContract>
128
+ >);
129
+
130
+ export const WHATSAPP_COMMUNITY_MEMBERSHIP_POLICY = Object.freeze({
131
+ risk: "R4",
132
+ state: "prohibited",
133
+ reason:
134
+ "community and group membership changes remain outside the executable wrench boundary",
135
+ } as const);
136
+
137
+ const MAX_TEXT = 65_536;
138
+ const MAX_MESSAGE_BODY = 2_000;
139
+ const MAX_NAME = 1_024;
140
+ const MAX_MESSAGE_ID = 256;
141
+ const MAX_JID = 96;
142
+ const MAX_BUTTONS = 64;
143
+ const MAX_CHATS = 100;
144
+ const MAX_MESSAGES = 200;
145
+
146
+ type JsonRecord = Record<string, unknown>;
147
+
148
+ function isRecord(value: unknown): value is JsonRecord {
149
+ return typeof value === "object" && value !== null && !Array.isArray(value);
150
+ }
151
+
152
+ function record(value: unknown, label: string): JsonRecord {
153
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
154
+ return value;
155
+ }
156
+
157
+ function exactKeys(
158
+ value: JsonRecord,
159
+ required: readonly string[],
160
+ optional: readonly string[],
161
+ label: string,
162
+ ): void {
163
+ const keys = new Set(Object.keys(value));
164
+ for (const key of required) {
165
+ if (!keys.delete(key)) throw new Error(`${label} omitted ${key}`);
166
+ }
167
+ for (const key of optional) keys.delete(key);
168
+ if (keys.size > 0) throw new Error(`${label} contained unsupported fields`);
169
+ }
170
+
171
+ function text(
172
+ value: unknown,
173
+ label: string,
174
+ maximum = MAX_TEXT,
175
+ allowEmpty = false,
176
+ ): string {
177
+ if (
178
+ typeof value !== "string"
179
+ || (!allowEmpty && value.length < 1)
180
+ || value.length > maximum
181
+ || /[\0\r]/u.test(value)
182
+ ) throw new Error(`${label} must be bounded text`);
183
+ return value;
184
+ }
185
+
186
+ function optionalText(
187
+ value: unknown,
188
+ label: string,
189
+ maximum = MAX_TEXT,
190
+ ): string | null {
191
+ if (value === undefined || value === null || value === "") return null;
192
+ return text(value, label, maximum);
193
+ }
194
+
195
+ function boolean(value: unknown, label: string): boolean {
196
+ if (typeof value !== "boolean") throw new Error(`${label} must be boolean`);
197
+ return value;
198
+ }
199
+
200
+ function integer(
201
+ value: unknown,
202
+ label: string,
203
+ minimum = 0,
204
+ maximum = Number.MAX_SAFE_INTEGER,
205
+ ): number {
206
+ if (
207
+ !Number.isSafeInteger(value)
208
+ || (value as number) < minimum
209
+ || (value as number) > maximum
210
+ ) throw new Error(`${label} must be a bounded integer`);
211
+ return value as number;
212
+ }
213
+
214
+ function timestamp(value: unknown, label: string): string {
215
+ const result = text(value, label, 40);
216
+ if (
217
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/u.test(result)
218
+ || !Number.isFinite(Date.parse(result))
219
+ ) throw new Error(`${label} must be an RFC3339 UTC timestamp`);
220
+ return result;
221
+ }
222
+
223
+ function boundedArray(
224
+ value: unknown,
225
+ label: string,
226
+ maximum: number,
227
+ ): readonly unknown[] {
228
+ if (!Array.isArray(value) || value.length > maximum) {
229
+ throw new Error(`${label} must be a bounded array`);
230
+ }
231
+ return value;
232
+ }
233
+
234
+ const userJidPattern =
235
+ /^[0-9]{5,20}(?::[0-9]{1,5})?@s\.whatsapp\.net$/u;
236
+ const lidJidPattern = /^[0-9]{5,32}(?::[0-9]{1,5})?@lid$/u;
237
+ const groupJidPattern = /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u;
238
+ const newsletterJidPattern = /^[0-9]{5,32}@newsletter$/u;
239
+ const broadcastJidPattern = /^(?:status|[0-9]{5,32})@broadcast$/u;
240
+
241
+ export type WhatsAppJidKind =
242
+ | "user"
243
+ | "lid"
244
+ | "group"
245
+ | "newsletter"
246
+ | "broadcast";
247
+
248
+ export type WhatsAppJid = {
249
+ readonly jid: string;
250
+ readonly kind: WhatsAppJidKind;
251
+ };
252
+
253
+ export function parseWhatsAppJid(
254
+ value: unknown,
255
+ label = "WhatsApp JID",
256
+ ): WhatsAppJid {
257
+ const jid = text(value, label, MAX_JID);
258
+ const kind: WhatsAppJidKind | null = userJidPattern.test(jid)
259
+ ? "user"
260
+ : lidJidPattern.test(jid)
261
+ ? "lid"
262
+ : groupJidPattern.test(jid)
263
+ ? "group"
264
+ : newsletterJidPattern.test(jid)
265
+ ? "newsletter"
266
+ : broadcastJidPattern.test(jid)
267
+ ? "broadcast"
268
+ : null;
269
+ if (kind === null) throw new Error(`${label} must be an exact WhatsApp JID`);
270
+ return Object.freeze({ jid, kind });
271
+ }
272
+
273
+ export function whatsappTargetJid(
274
+ value: unknown,
275
+ label = "WhatsApp target JID",
276
+ ): string {
277
+ const parsed = parseWhatsAppJid(value, label);
278
+ if (
279
+ parsed.kind === "newsletter"
280
+ || parsed.kind === "broadcast"
281
+ || parsed.jid.includes(":")
282
+ ) throw new Error(`${label} is not an addressable user, LID, or group JID`);
283
+ return parsed.jid;
284
+ }
285
+
286
+ export function whatsappMessageId(
287
+ value: unknown,
288
+ label = "WhatsApp message ID",
289
+ ): string {
290
+ const result = text(value, label, MAX_MESSAGE_ID);
291
+ if (!/^[A-Za-z0-9._~:-]{1,256}$/u.test(result)) {
292
+ throw new Error(`${label} must be an exact bounded message ID`);
293
+ }
294
+ return result;
295
+ }
296
+
297
+ function senderJid(value: unknown, label: string): string | null {
298
+ if (value === "") return null;
299
+ return parseWhatsAppJid(value, label).jid;
300
+ }
301
+
302
+ function envelope(value: unknown, label: string): unknown {
303
+ const root = record(value, `${label} envelope`);
304
+ exactKeys(root, ["success", "data", "error"], [], `${label} envelope`);
305
+ if (root.success !== true || root.error !== null) {
306
+ throw new Error(`${label} returned an unsuccessful envelope`);
307
+ }
308
+ return root.data;
309
+ }
310
+
311
+ export type WhatsAppAuthStatus = {
312
+ readonly authenticated: boolean;
313
+ readonly linkedJid: string | null;
314
+ readonly subject: string | null;
315
+ };
316
+
317
+ export function whatsappSubjectFromLinkedJid(value: unknown): string {
318
+ const parsed = parseWhatsAppJid(value, "WhatsApp linked-device JID");
319
+ if (parsed.kind !== "user" && parsed.kind !== "lid") {
320
+ throw new Error("WhatsApp linked-device JID must name one user account");
321
+ }
322
+ const account = parsed.jid.slice(0, parsed.jid.indexOf("@")).split(":", 1)[0];
323
+ const kind = parsed.kind === "user" ? "pn" : "lid";
324
+ if (account === undefined || !isCanonicalWhatsAppAccountId(kind, account)) {
325
+ throw new Error("WhatsApp linked-device account identifier is not canonical");
326
+ }
327
+ return `whatsapp:${kind}:${account}`;
328
+ }
329
+
330
+ export function parseWhatsAppAuthStatusEnvelope(
331
+ value: unknown,
332
+ ): WhatsAppAuthStatus {
333
+ const data = record(envelope(value, "WhatsApp auth status"), "auth status");
334
+ const authenticated = boolean(data.authenticated, "auth status.authenticated");
335
+ const expected = ["authenticated"];
336
+ if (data.linked_jid !== undefined) expected.push("linked_jid");
337
+ if (data.phone !== undefined) expected.push("phone");
338
+ exactKeys(data, expected, [], "auth status");
339
+ if (!authenticated) {
340
+ if (data.linked_jid !== undefined || data.phone !== undefined) {
341
+ throw new Error("unauthenticated WhatsApp status exposed account fields");
342
+ }
343
+ return Object.freeze({
344
+ authenticated: false,
345
+ linkedJid: null,
346
+ subject: null,
347
+ });
348
+ }
349
+ const linkedJid = parseWhatsAppJid(
350
+ data.linked_jid,
351
+ "auth status.linked_jid",
352
+ );
353
+ if (linkedJid.kind !== "user" && linkedJid.kind !== "lid") {
354
+ throw new Error("authenticated WhatsApp status did not name one account");
355
+ }
356
+ if (data.phone !== undefined) {
357
+ const phone = text(data.phone, "auth status.phone", 20);
358
+ if (!isCanonicalWhatsAppAccountId("pn", phone)) {
359
+ throw new Error("auth status.phone must be a canonical international number");
360
+ }
361
+ const linkedAccount = linkedJid.jid.slice(0, linkedJid.jid.indexOf("@"))
362
+ .split(":", 1)[0];
363
+ if (linkedJid.kind === "user" && phone !== linkedAccount) {
364
+ throw new Error("auth status.phone did not match linked_jid");
365
+ }
366
+ }
367
+ return Object.freeze({
368
+ authenticated: true,
369
+ linkedJid: linkedJid.jid,
370
+ subject: whatsappSubjectFromLinkedJid(linkedJid.jid),
371
+ });
372
+ }
373
+
374
+ export type WhatsAppProjectedChat = {
375
+ readonly jid: string;
376
+ readonly kind: string;
377
+ readonly name: string | null;
378
+ readonly lastMessageAt: string;
379
+ readonly archived: boolean;
380
+ readonly pinned: boolean;
381
+ readonly mutedUntil: number;
382
+ readonly unread: boolean;
383
+ readonly unreadCount: number;
384
+ };
385
+
386
+ function projectChat(value: unknown, index: number): WhatsAppProjectedChat {
387
+ const item = record(value, `chat[${index}]`);
388
+ exactKeys(
389
+ item,
390
+ [
391
+ "jid",
392
+ "kind",
393
+ "name",
394
+ "last_message_ts",
395
+ "archived",
396
+ "pinned",
397
+ "muted_until",
398
+ "unread",
399
+ "unread_count",
400
+ ],
401
+ [],
402
+ `chat[${index}]`,
403
+ );
404
+ return Object.freeze({
405
+ jid: parseWhatsAppJid(item.jid, `chat[${index}].jid`).jid,
406
+ kind: text(item.kind, `chat[${index}].kind`, 64),
407
+ name: optionalText(item.name, `chat[${index}].name`, MAX_NAME),
408
+ lastMessageAt: timestamp(
409
+ item.last_message_ts,
410
+ `chat[${index}].last_message_ts`,
411
+ ),
412
+ archived: boolean(item.archived, `chat[${index}].archived`),
413
+ pinned: boolean(item.pinned, `chat[${index}].pinned`),
414
+ mutedUntil: integer(
415
+ item.muted_until,
416
+ `chat[${index}].muted_until`,
417
+ -1,
418
+ ),
419
+ unread: boolean(item.unread, `chat[${index}].unread`),
420
+ unreadCount: integer(
421
+ item.unread_count,
422
+ `chat[${index}].unread_count`,
423
+ 0,
424
+ 1_000_000,
425
+ ),
426
+ });
427
+ }
428
+
429
+ export function projectWhatsAppChatsEnvelope(
430
+ value: unknown,
431
+ maximum = MAX_CHATS,
432
+ ): readonly WhatsAppProjectedChat[] {
433
+ const limit = integer(maximum, "WhatsApp chat projection limit", 1, MAX_CHATS);
434
+ return Object.freeze(
435
+ boundedArray(envelope(value, "WhatsApp chats"), "chats", limit)
436
+ .map(projectChat),
437
+ );
438
+ }
439
+
440
+ export type WhatsAppProjectedButton = {
441
+ readonly type: string;
442
+ readonly displayText: string;
443
+ readonly index: number | null;
444
+ };
445
+
446
+ export type WhatsAppProjectedMedia = {
447
+ readonly type: string;
448
+ readonly caption: string | null;
449
+ readonly filename: string | null;
450
+ readonly mimeType: string | null;
451
+ readonly downloaded: boolean;
452
+ };
453
+
454
+ export type WhatsAppProjectedMessage = {
455
+ readonly chatJid: string;
456
+ readonly chatName: string | null;
457
+ readonly messageId: string;
458
+ readonly senderJid: string | null;
459
+ readonly senderName: string | null;
460
+ readonly timestamp: string;
461
+ readonly fromMe: boolean;
462
+ readonly text: string;
463
+ readonly displayText: string;
464
+ readonly quotedMessageId: string | null;
465
+ readonly quotedSenderJid: string | null;
466
+ readonly buttons: readonly WhatsAppProjectedButton[];
467
+ readonly forwarded: boolean;
468
+ readonly forwardingScore: number;
469
+ readonly reactionToId: string | null;
470
+ readonly reactionEmoji: string | null;
471
+ readonly media: WhatsAppProjectedMedia | null;
472
+ readonly starred: boolean;
473
+ readonly revoked: boolean;
474
+ readonly deletedForMe: boolean;
475
+ readonly snippet: string | null;
476
+ };
477
+
478
+ function projectButton(
479
+ value: unknown,
480
+ index: number,
481
+ ): WhatsAppProjectedButton {
482
+ const item = record(value, `button[${index}]`);
483
+ exactKeys(
484
+ item,
485
+ ["type", "display_text"],
486
+ ["id", "url", "phone_number", "description", "response_type", "index"],
487
+ `button[${index}]`,
488
+ );
489
+ // IDs, URLs, phone numbers, and response payloads are intentionally not
490
+ // returned; interactive message metadata is descriptive only.
491
+ return Object.freeze({
492
+ type: text(item.type, `button[${index}].type`, 64),
493
+ displayText: text(
494
+ item.display_text,
495
+ `button[${index}].display_text`,
496
+ 1_024,
497
+ true,
498
+ ),
499
+ index: item.index === undefined
500
+ ? null
501
+ : integer(item.index, `button[${index}].index`, 0, 1_000),
502
+ });
503
+ }
504
+
505
+ const messageRequiredKeys = [
506
+ "ChatJID",
507
+ "ChatName",
508
+ "MsgID",
509
+ "SenderJID",
510
+ "SenderName",
511
+ "Timestamp",
512
+ "FromMe",
513
+ "Text",
514
+ "DisplayText",
515
+ "IsForwarded",
516
+ "ForwardingScore",
517
+ "ReactionToID",
518
+ "ReactionEmoji",
519
+ "MediaType",
520
+ "MediaCaption",
521
+ "Filename",
522
+ "MimeType",
523
+ "DirectPath",
524
+ "LocalPath",
525
+ "DownloadedAt",
526
+ "Starred",
527
+ "StarredAt",
528
+ "Revoked",
529
+ "DeletedForMe",
530
+ "Snippet",
531
+ ] as const;
532
+
533
+ function projectMessage(
534
+ value: unknown,
535
+ label: string,
536
+ expectedChat?: string,
537
+ expectedMessageId?: string,
538
+ ): WhatsAppProjectedMessage {
539
+ const item = record(value, label);
540
+ exactKeys(
541
+ item,
542
+ messageRequiredKeys,
543
+ ["quoted_msg_id", "quoted_sender_jid", "Buttons"],
544
+ label,
545
+ );
546
+ const chatJid = parseWhatsAppJid(item.ChatJID, `${label}.ChatJID`).jid;
547
+ const messageId = whatsappMessageId(item.MsgID, `${label}.MsgID`);
548
+ if (expectedChat !== undefined && chatJid !== expectedChat) {
549
+ throw new Error(`${label} did not match the requested conversation`);
550
+ }
551
+ if (expectedMessageId !== undefined && messageId !== expectedMessageId) {
552
+ throw new Error(`${label} did not match the requested message`);
553
+ }
554
+ text(
555
+ item.DirectPath,
556
+ `${label}.DirectPath`,
557
+ 16_384,
558
+ true,
559
+ );
560
+ const localPath = text(item.LocalPath, `${label}.LocalPath`, 4_096, true);
561
+ const mediaType = optionalText(item.MediaType, `${label}.MediaType`, 64);
562
+ const downloadedAt = timestamp(item.DownloadedAt, `${label}.DownloadedAt`);
563
+ timestamp(item.StarredAt, `${label}.StarredAt`);
564
+ const buttons = item.Buttons === undefined
565
+ ? []
566
+ : boundedArray(item.Buttons, `${label}.Buttons`, MAX_BUTTONS)
567
+ .map(projectButton);
568
+ const sender = senderJid(item.SenderJID, `${label}.SenderJID`);
569
+ const quotedSender = item.quoted_sender_jid === undefined
570
+ ? null
571
+ : senderJid(item.quoted_sender_jid, `${label}.quoted_sender_jid`);
572
+ return Object.freeze({
573
+ chatJid,
574
+ chatName: optionalText(item.ChatName, `${label}.ChatName`, MAX_NAME),
575
+ messageId,
576
+ senderJid: sender,
577
+ senderName: optionalText(item.SenderName, `${label}.SenderName`, MAX_NAME),
578
+ timestamp: timestamp(item.Timestamp, `${label}.Timestamp`),
579
+ fromMe: boolean(item.FromMe, `${label}.FromMe`),
580
+ text: text(item.Text, `${label}.Text`, MAX_TEXT, true),
581
+ displayText: text(
582
+ item.DisplayText,
583
+ `${label}.DisplayText`,
584
+ MAX_TEXT,
585
+ true,
586
+ ),
587
+ quotedMessageId: item.quoted_msg_id === undefined
588
+ ? null
589
+ : whatsappMessageId(
590
+ item.quoted_msg_id,
591
+ `${label}.quoted_msg_id`,
592
+ ),
593
+ quotedSenderJid: quotedSender,
594
+ buttons: Object.freeze(buttons),
595
+ forwarded: boolean(item.IsForwarded, `${label}.IsForwarded`),
596
+ forwardingScore: integer(
597
+ item.ForwardingScore,
598
+ `${label}.ForwardingScore`,
599
+ 0,
600
+ 1_000_000,
601
+ ),
602
+ reactionToId: optionalText(
603
+ item.ReactionToID,
604
+ `${label}.ReactionToID`,
605
+ MAX_MESSAGE_ID,
606
+ ),
607
+ reactionEmoji: optionalText(
608
+ item.ReactionEmoji,
609
+ `${label}.ReactionEmoji`,
610
+ 64,
611
+ ),
612
+ media: mediaType === null
613
+ ? null
614
+ : Object.freeze({
615
+ type: mediaType,
616
+ caption: optionalText(
617
+ item.MediaCaption,
618
+ `${label}.MediaCaption`,
619
+ MAX_TEXT,
620
+ ),
621
+ filename: optionalText(
622
+ item.Filename,
623
+ `${label}.Filename`,
624
+ MAX_NAME,
625
+ ),
626
+ mimeType: optionalText(
627
+ item.MimeType,
628
+ `${label}.MimeType`,
629
+ 255,
630
+ ),
631
+ downloaded:
632
+ localPath !== ""
633
+ && downloadedAt !== "0001-01-01T00:00:00Z",
634
+ }),
635
+ starred: boolean(item.Starred, `${label}.Starred`),
636
+ revoked: boolean(item.Revoked, `${label}.Revoked`),
637
+ deletedForMe: boolean(item.DeletedForMe, `${label}.DeletedForMe`),
638
+ snippet: optionalText(item.Snippet, `${label}.Snippet`, 4_096),
639
+ });
640
+ }
641
+
642
+ export function projectWhatsAppMessagesEnvelope(
643
+ value: unknown,
644
+ expectedChat: string,
645
+ maximum = MAX_MESSAGES,
646
+ ): {
647
+ readonly messages: readonly WhatsAppProjectedMessage[];
648
+ readonly fullTextSearch: boolean;
649
+ } {
650
+ const chat = whatsappTargetJid(expectedChat, "requested conversation JID");
651
+ const limit = integer(
652
+ maximum,
653
+ "WhatsApp message projection limit",
654
+ 1,
655
+ MAX_MESSAGES,
656
+ );
657
+ const data = record(envelope(value, "WhatsApp messages"), "messages data");
658
+ exactKeys(data, ["messages", "fts"], [], "messages data");
659
+ const messages = boundedArray(data.messages, "messages", limit)
660
+ .map((item, index) =>
661
+ projectMessage(item, `message[${index}]`, chat)
662
+ );
663
+ return Object.freeze({
664
+ messages: Object.freeze(messages),
665
+ fullTextSearch: boolean(data.fts, "messages.fts"),
666
+ });
667
+ }
668
+
669
+ export function projectWhatsAppMessageEnvelope(
670
+ value: unknown,
671
+ expectedChat: string,
672
+ expectedMessageId: string,
673
+ ): WhatsAppProjectedMessage {
674
+ const chat = whatsappTargetJid(expectedChat, "requested conversation JID");
675
+ const messageId = whatsappMessageId(
676
+ expectedMessageId,
677
+ "requested message ID",
678
+ );
679
+ return projectMessage(
680
+ envelope(value, "WhatsApp message"),
681
+ "message",
682
+ chat,
683
+ messageId,
684
+ );
685
+ }
686
+
687
+ function inputString(
688
+ input: OperationInput,
689
+ name: string,
690
+ maximum: number,
691
+ allowEmpty = false,
692
+ ): string {
693
+ return text(input[name], `input.${name}`, maximum, allowEmpty);
694
+ }
695
+
696
+ function optionalInputString(
697
+ input: OperationInput,
698
+ name: string,
699
+ maximum: number,
700
+ ): string | undefined {
701
+ return input[name] === undefined
702
+ ? undefined
703
+ : inputString(input, name, maximum);
704
+ }
705
+
706
+ export type WhatsAppWritePlan =
707
+ | {
708
+ readonly action: "messaging.send";
709
+ readonly argv: readonly string[];
710
+ readonly destinationJid: string;
711
+ readonly expectedBody: string;
712
+ readonly expectedMedia: boolean;
713
+ }
714
+ | {
715
+ readonly action: "reactions.set";
716
+ readonly argv: readonly string[];
717
+ readonly conversationJid: string;
718
+ readonly targetMessageId: string;
719
+ readonly reaction: string;
720
+ }
721
+ | {
722
+ readonly action: "content.edit";
723
+ readonly argv: readonly string[];
724
+ readonly conversationJid: string;
725
+ readonly targetMessageId: string;
726
+ readonly expectedBody: string;
727
+ }
728
+ | {
729
+ readonly action: "content.share";
730
+ readonly argv: readonly string[];
731
+ readonly sourceConversationJid: string;
732
+ readonly sourceMessageId: string;
733
+ readonly destinationJid: string;
734
+ };
735
+
736
+ export function isWhatsAppWriteAction(
737
+ action: WhatsAppWebOperationName,
738
+ ): action is WhatsAppWritePlan["action"] {
739
+ return action === "messaging.send"
740
+ || action === "reactions.set"
741
+ || action === "content.edit"
742
+ || action === "content.share";
743
+ }
744
+
745
+ /**
746
+ * Construct only fixed wacli semantic commands. No arbitrary subcommand,
747
+ * recipient name, URL, flag, or query surface can pass through this function.
748
+ *
749
+ * The current contracts remain capture-required, so these plans are inert in
750
+ * production. In particular, text-bearing plans must move to a private stdin
751
+ * or Unix-socket payload before promotion because process argv is observable.
752
+ */
753
+ export function planWhatsAppWriteCommand(
754
+ action: WhatsAppWritePlan["action"],
755
+ input: OperationInput,
756
+ materializedAttachmentPath?: string,
757
+ ): WhatsAppWritePlan {
758
+ if (action === "messaging.send") {
759
+ const destinationJid = whatsappTargetJid(
760
+ inputString(input, "conversation_jid", MAX_JID),
761
+ "input.conversation_jid",
762
+ );
763
+ const body = inputString(input, "body", MAX_MESSAGE_BODY);
764
+ const replyTo = optionalInputString(input, "reply_to_message_id", MAX_MESSAGE_ID);
765
+ if (replyTo !== undefined) {
766
+ whatsappMessageId(replyTo, "input.reply_to_message_id");
767
+ }
768
+ if (materializedAttachmentPath === undefined) {
769
+ return Object.freeze({
770
+ action,
771
+ argv: Object.freeze([
772
+ "send",
773
+ "text",
774
+ "--to",
775
+ destinationJid,
776
+ "--message",
777
+ body,
778
+ "--no-preview",
779
+ "--post-send-wait",
780
+ "0",
781
+ ...(replyTo === undefined ? [] : ["--reply-to", replyTo]),
782
+ ]),
783
+ destinationJid,
784
+ expectedBody: body,
785
+ expectedMedia: false,
786
+ });
787
+ }
788
+ const path = text(
789
+ materializedAttachmentPath,
790
+ "materialized attachment path",
791
+ 4_096,
792
+ );
793
+ if (!path.startsWith("/")) {
794
+ throw new Error("materialized WhatsApp attachment path must be absolute");
795
+ }
796
+ const filename = optionalInputString(input, "filename", MAX_NAME);
797
+ const mimeType = optionalInputString(input, "mime_type", 255);
798
+ return Object.freeze({
799
+ action,
800
+ argv: Object.freeze([
801
+ "send",
802
+ "file",
803
+ "--to",
804
+ destinationJid,
805
+ "--file",
806
+ path,
807
+ "--post-send-wait",
808
+ "0",
809
+ "--caption",
810
+ body,
811
+ ...(filename === undefined ? [] : ["--filename", filename]),
812
+ ...(mimeType === undefined ? [] : ["--mime", mimeType]),
813
+ ...(replyTo === undefined ? [] : ["--reply-to", replyTo]),
814
+ ]),
815
+ destinationJid,
816
+ expectedBody: body,
817
+ expectedMedia: true,
818
+ });
819
+ }
820
+ if (action === "reactions.set") {
821
+ const conversationJid = whatsappTargetJid(
822
+ inputString(input, "conversation_jid", MAX_JID),
823
+ "input.conversation_jid",
824
+ );
825
+ const targetMessageId = whatsappMessageId(
826
+ inputString(input, "message_id", MAX_MESSAGE_ID),
827
+ "input.message_id",
828
+ );
829
+ const reaction = inputString(input, "reaction", 64, true);
830
+ const sender = optionalInputString(input, "sender_jid", MAX_JID);
831
+ const parsedChat = parseWhatsAppJid(conversationJid);
832
+ if (parsedChat.kind === "group" && sender === undefined) {
833
+ throw new Error("input.sender_jid is required for a group reaction");
834
+ }
835
+ const senderArgument = sender === undefined
836
+ ? []
837
+ : [
838
+ "--sender",
839
+ whatsappTargetJid(sender, "input.sender_jid"),
840
+ ];
841
+ return Object.freeze({
842
+ action,
843
+ argv: Object.freeze([
844
+ "send",
845
+ "react",
846
+ "--to",
847
+ conversationJid,
848
+ "--id",
849
+ targetMessageId,
850
+ "--reaction",
851
+ reaction,
852
+ "--post-send-wait",
853
+ "0",
854
+ ...senderArgument,
855
+ ]),
856
+ conversationJid,
857
+ targetMessageId,
858
+ reaction,
859
+ });
860
+ }
861
+ if (action === "content.edit") {
862
+ const conversationJid = whatsappTargetJid(
863
+ inputString(input, "conversation_jid", MAX_JID),
864
+ "input.conversation_jid",
865
+ );
866
+ const targetMessageId = whatsappMessageId(
867
+ inputString(input, "message_id", MAX_MESSAGE_ID),
868
+ "input.message_id",
869
+ );
870
+ const expectedBody = inputString(input, "body", MAX_MESSAGE_BODY);
871
+ return Object.freeze({
872
+ action,
873
+ argv: Object.freeze([
874
+ "messages",
875
+ "edit",
876
+ "--chat",
877
+ conversationJid,
878
+ "--id",
879
+ targetMessageId,
880
+ "--message",
881
+ expectedBody,
882
+ "--post-send-wait",
883
+ "0",
884
+ ]),
885
+ conversationJid,
886
+ targetMessageId,
887
+ expectedBody,
888
+ });
889
+ }
890
+ if (action === "content.share") {
891
+ const sourceConversationJid = whatsappTargetJid(
892
+ inputString(input, "source_conversation_jid", MAX_JID),
893
+ "input.source_conversation_jid",
894
+ );
895
+ const sourceMessageId = whatsappMessageId(
896
+ inputString(input, "message_id", MAX_MESSAGE_ID),
897
+ "input.message_id",
898
+ );
899
+ const destinationJid = whatsappTargetJid(
900
+ inputString(input, "destination_jid", MAX_JID),
901
+ "input.destination_jid",
902
+ );
903
+ return Object.freeze({
904
+ action,
905
+ argv: Object.freeze([
906
+ "messages",
907
+ "forward",
908
+ "--chat",
909
+ sourceConversationJid,
910
+ "--id",
911
+ sourceMessageId,
912
+ "--to",
913
+ destinationJid,
914
+ "--post-send-wait",
915
+ "0",
916
+ ]),
917
+ sourceConversationJid,
918
+ sourceMessageId,
919
+ destinationJid,
920
+ });
921
+ }
922
+ throw new Error(
923
+ `WhatsApp linked-device operation ${String(action)} has no reviewed write plan`,
924
+ );
925
+ }
926
+
927
+ export type WhatsAppWriteReceipt = {
928
+ readonly messageId: string;
929
+ readonly readbackChatJid: string;
930
+ };
931
+
932
+ export function parseWhatsAppWriteEnvelope(
933
+ plan: WhatsAppWritePlan,
934
+ value: unknown,
935
+ ): WhatsAppWriteReceipt {
936
+ const data = record(envelope(value, `WhatsApp ${plan.action}`), "write data");
937
+ if (plan.action === "messaging.send") {
938
+ exactKeys(data, ["sent", "to", "id"], ["file"], "send response");
939
+ if (data.sent !== true || data.to !== plan.destinationJid) {
940
+ throw new Error("WhatsApp send response did not bind the destination");
941
+ }
942
+ if (plan.expectedMedia && !isRecord(data.file)) {
943
+ throw new Error("WhatsApp media send response omitted file metadata");
944
+ }
945
+ return Object.freeze({
946
+ messageId: whatsappMessageId(data.id, "send response.id"),
947
+ readbackChatJid: plan.destinationJid,
948
+ });
949
+ }
950
+ if (plan.action === "reactions.set") {
951
+ exactKeys(
952
+ data,
953
+ ["sent", "to", "id", "target", "reaction"],
954
+ [],
955
+ "reaction response",
956
+ );
957
+ if (
958
+ data.sent !== true
959
+ || data.to !== plan.conversationJid
960
+ || data.target !== plan.targetMessageId
961
+ || data.reaction !== plan.reaction
962
+ ) throw new Error("WhatsApp reaction response did not bind the target");
963
+ return Object.freeze({
964
+ messageId: whatsappMessageId(data.id, "reaction response.id"),
965
+ readbackChatJid: plan.conversationJid,
966
+ });
967
+ }
968
+ if (plan.action === "content.edit") {
969
+ exactKeys(
970
+ data,
971
+ ["edited", "to", "id", "target", "message"],
972
+ [],
973
+ "edit response",
974
+ );
975
+ if (
976
+ data.edited !== true
977
+ || data.to !== plan.conversationJid
978
+ || data.target !== plan.targetMessageId
979
+ || data.message !== plan.expectedBody
980
+ ) throw new Error("WhatsApp edit response did not bind the target and text");
981
+ whatsappMessageId(data.id, "edit response.id");
982
+ return Object.freeze({
983
+ messageId: plan.targetMessageId,
984
+ readbackChatJid: plan.conversationJid,
985
+ });
986
+ }
987
+ exactKeys(
988
+ data,
989
+ ["forwarded", "to", "id", "source"],
990
+ [],
991
+ "forward response",
992
+ );
993
+ if (
994
+ data.forwarded !== true
995
+ || data.to !== plan.destinationJid
996
+ || data.source !== plan.sourceMessageId
997
+ ) throw new Error("WhatsApp forward response did not bind source and destination");
998
+ return Object.freeze({
999
+ messageId: whatsappMessageId(data.id, "forward response.id"),
1000
+ readbackChatJid: plan.destinationJid,
1001
+ });
1002
+ }
1003
+
1004
+ export function verifyWhatsAppWriteReadback(
1005
+ plan: WhatsAppWritePlan,
1006
+ receipt: WhatsAppWriteReceipt,
1007
+ readbackEnvelope: unknown,
1008
+ ): WhatsAppProjectedMessage {
1009
+ const message = projectWhatsAppMessageEnvelope(
1010
+ readbackEnvelope,
1011
+ receipt.readbackChatJid,
1012
+ receipt.messageId,
1013
+ );
1014
+ if (!message.fromMe) {
1015
+ throw new Error("WhatsApp write readback did not bind the linked-device actor");
1016
+ }
1017
+ if (plan.action === "messaging.send") {
1018
+ if (
1019
+ plan.expectedMedia
1020
+ ? message.media === null
1021
+ || message.media.caption !== plan.expectedBody
1022
+ : message.text !== plan.expectedBody
1023
+ ) throw new Error("WhatsApp send readback did not bind the confirmed content");
1024
+ } else if (plan.action === "reactions.set") {
1025
+ if (
1026
+ message.reactionToId !== plan.targetMessageId
1027
+ || message.reactionEmoji !== (plan.reaction === "" ? null : plan.reaction)
1028
+ ) throw new Error("WhatsApp reaction readback did not bind desired state");
1029
+ } else if (plan.action === "content.edit") {
1030
+ if (message.text !== plan.expectedBody) {
1031
+ throw new Error("WhatsApp edit readback did not bind confirmed text");
1032
+ }
1033
+ } else if (!message.forwarded) {
1034
+ throw new Error("WhatsApp forward readback was not marked forwarded");
1035
+ }
1036
+ return message;
1037
+ }