@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,3108 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ constants as fsConstants,
4
+ createReadStream,
5
+ type Stats,
6
+ type BigIntStats,
7
+ } from "node:fs";
8
+ import {
9
+ chmod,
10
+ lstat,
11
+ mkdtemp,
12
+ mkdir,
13
+ open,
14
+ readdir,
15
+ realpath,
16
+ rmdir,
17
+ unlink,
18
+ type FileHandle,
19
+ } from "node:fs/promises";
20
+ import { tmpdir } from "node:os";
21
+ import { dirname, isAbsolute, join, resolve } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ import { BoundedByteBuffer } from "@hraness/kb/clip/bounded-byte-buffer";
25
+
26
+ import type { GhostgetAuth } from "../auth";
27
+ import { canonicalJson } from "../canonical-json";
28
+ import type { OperationInput, WebSessionRecipe } from "../model";
29
+ import { OperationDeadline } from "../operation-deadline";
30
+ import type {
31
+ ProviderPluginLinkedDeviceAttemptBoundaryV1,
32
+ } from "../provider-plugin";
33
+ import { assertSafeStatePath, ghostgetStateHome } from "../storage";
34
+ import type {
35
+ WebSessionExecution,
36
+ WebSessionCleanupBarrierRegistrar,
37
+ WebSessionOperationDeadline,
38
+ } from "../web-session-execution";
39
+ import { startWebSessionCleanupTrackedOperation } from "../web-session-execution";
40
+ import {
41
+ WHATSAPP_CONTACT_PROJECTION_MAX_STDOUT_BYTES,
42
+ WHATSAPP_CONTACT_PROJECTION_PROTOCOL_VERSION,
43
+ isExactWhatsAppContactProjectionMode,
44
+ parseWhatsAppContactProjectionResponse,
45
+ parseWhatsAppContactProjectionSubject,
46
+ type WhatsAppContactProjectionRequest,
47
+ } from "./whatsapp-contact-projection-protocol";
48
+ import {
49
+ WHATSAPP_INTERACTION_PROJECTION_MAX_STDOUT_BYTES,
50
+ WHATSAPP_INTERACTION_PROJECTION_PROTOCOL_VERSION,
51
+ parseWhatsAppInteractionProjectionRequest,
52
+ parseWhatsAppInteractionProjectionResponse,
53
+ parseWhatsAppInteractionRowid,
54
+ type WhatsAppInteractionProjectionRequest,
55
+ } from "./whatsapp-interaction-projection-protocol";
56
+ import {
57
+ WHATSAPP_PROTOCOL_PIN,
58
+ WHATSAPP_WEB_OPERATIONS,
59
+ WHATSAPP_WEB_OPERATION_NAMES,
60
+ parseWhatsAppAuthStatusEnvelope,
61
+ parseWhatsAppJid,
62
+ projectWhatsAppChatsEnvelope,
63
+ projectWhatsAppMessageEnvelope,
64
+ projectWhatsAppMessagesEnvelope,
65
+ whatsappMessageId,
66
+ whatsappTargetJid,
67
+ type WhatsAppWebOperationName,
68
+ } from "./whatsapp-web";
69
+ import { projectContactDirectionStats } from "./contact-projection";
70
+
71
+ const WHATSAPP_ORIGIN = "https://web.whatsapp.com";
72
+ const DEFAULT_LIMIT = 50;
73
+ const MAX_STDERR_BYTES = 64 * 1024;
74
+ const MAX_STORE_ENTRIES = 10_000;
75
+ const MAX_SYNC_MESSAGES = 200_000;
76
+ const MAX_SYNC_DB_SIZE = "2GB";
77
+ const MAX_CONTACT_PROJECTION_STDERR_BYTES = 16 * 1024;
78
+ const CONTACT_PROJECTION_FORCE_KILL_DELAY_MS = 1_000;
79
+ const MESSAGE_EXPORT_SESSION_MAX_TOTAL_STDOUT_BYTES = 512 * 1024 * 1024;
80
+ const MESSAGE_EXPORT_SESSION_MAX_FRAMES = 1_001;
81
+ const MESSAGE_EXPORT_SESSION_SPOOL_CHUNK_BYTES = 64 * 1024;
82
+ const MESSAGE_EXPORT_SESSION_PRIVATE_DIRECTORY_MODE = 0o700;
83
+ const MESSAGE_EXPORT_SESSION_PRIVATE_FILE_MODE = 0o600;
84
+ const WEB_SESSION_OPERATION_LABEL = "authenticated web operation deadline";
85
+
86
+ type WhatsAppAuth = Extract<
87
+ GhostgetAuth,
88
+ { readonly kind: "linked-device-store" }
89
+ >;
90
+
91
+ export type WacliInvocation = {
92
+ readonly binary: string;
93
+ readonly arguments: readonly string[];
94
+ readonly environment: Readonly<Record<string, string>>;
95
+ readonly timeoutMs: number;
96
+ readonly maxOutputBytes: number;
97
+ readonly signal?: AbortSignal;
98
+ };
99
+
100
+ export type WacliInvocationResult = {
101
+ readonly exitCode: number;
102
+ readonly stdout: string;
103
+ readonly stderr: string;
104
+ };
105
+
106
+ export type WhatsAppContactProjectionHelperInvocation = {
107
+ readonly command: readonly string[];
108
+ readonly cwd: string;
109
+ readonly environment: Readonly<Record<string, string>>;
110
+ readonly stdin: string;
111
+ readonly timeoutMs: number;
112
+ readonly maxOutputBytes: number;
113
+ readonly maxStderrBytes: number;
114
+ readonly signal?: AbortSignal;
115
+ /** Called exactly once after spawn with the owned helper PID. */
116
+ readonly onSpawned?: (pid: number) => void;
117
+ /** Internal streaming validator; called synchronously for each canonical frame. */
118
+ readonly onCanonicalFrame?: (
119
+ frame: WhatsAppMessageExportSessionCanonicalFrame,
120
+ ) => void;
121
+ /** Test-only process-group probe seam. */
122
+ readonly processGroupIsAbsentForTest?: () => boolean;
123
+ /** Test-only process-group poll observation seam. */
124
+ readonly onProcessGroupPollForTest?: () => void;
125
+ /** Test-only delay inside private-spool setup, before custody returns. */
126
+ readonly beforeSpoolReadyForTest?: () => Promise<void>;
127
+ };
128
+
129
+ export type WhatsAppContactProjectionHelperResult = {
130
+ readonly exitCode: number;
131
+ readonly stdout: string;
132
+ readonly stderr: string;
133
+ };
134
+
135
+ export type WhatsAppMessageExportSessionCanonicalFrame = Readonly<{
136
+ index: number;
137
+ canonical: string;
138
+ value: unknown;
139
+ }>;
140
+
141
+ export type WhatsAppMessageExportSessionSpool = Readonly<{
142
+ frameCount: number;
143
+ totalBytes: number;
144
+ stdoutSha256: string;
145
+ replay: <Value>(
146
+ project: (frame: WhatsAppMessageExportSessionCanonicalFrame) => Value,
147
+ ) => AsyncGenerator<Value>;
148
+ close: () => Promise<void>;
149
+ }>;
150
+
151
+ export type WhatsAppMessageExportSessionHelperResult = Readonly<{
152
+ exitCode: number;
153
+ spool: WhatsAppMessageExportSessionSpool;
154
+ stderr: string;
155
+ }>;
156
+
157
+ export class WhatsAppContactProjectionCleanupUnverifiedError extends Error {
158
+ constructor() {
159
+ super(
160
+ "WhatsApp contact projection helper cleanup could not be verified",
161
+ );
162
+ this.name = "WhatsAppContactProjectionCleanupUnverifiedError";
163
+ }
164
+ }
165
+
166
+ export function containsWhatsAppContactProjectionCleanupUnverified(
167
+ error: unknown,
168
+ ): boolean {
169
+ const pending: unknown[] = [error];
170
+ const visited = new Set<object>();
171
+ while (pending.length > 0) {
172
+ const current = pending.pop();
173
+ if (current instanceof WhatsAppContactProjectionCleanupUnverifiedError) return true;
174
+ if (typeof current !== "object" || current === null || visited.has(current)) continue;
175
+ visited.add(current);
176
+ if (current instanceof AggregateError) {
177
+ try {
178
+ pending.push(...current.errors);
179
+ } catch {
180
+ // A hostile wrapper cannot prove cleanup safety.
181
+ return true;
182
+ }
183
+ }
184
+ try {
185
+ if ("cause" in current) pending.push(current.cause);
186
+ } catch {
187
+ // An uninspectable wrapper cannot prove cleanup safety.
188
+ return true;
189
+ }
190
+ }
191
+ return false;
192
+ }
193
+
194
+ export type WhatsAppWebRuntimeDependencies = {
195
+ /**
196
+ * Test-only binary seam. Production resolution accepts only the pinned
197
+ * release in fixed installation locations and verifies its SHA-256.
198
+ */
199
+ readonly binaryPath?: string;
200
+ readonly run?: (
201
+ invocation: WacliInvocation,
202
+ ) => Promise<WacliInvocationResult>;
203
+ readonly runInteractive?: (
204
+ plan: WhatsAppPairingPlan,
205
+ ) => Promise<number>;
206
+ /** Test-only observation seam; production always uses the fixed Bun helper. */
207
+ readonly runContactProjectionHelper?: (
208
+ invocation: WhatsAppContactProjectionHelperInvocation,
209
+ ) => Promise<WhatsAppContactProjectionHelperResult>;
210
+ /** Test-only observation seam; production always uses the fixed Bun helper. */
211
+ readonly runInteractionProjectionHelper?: (
212
+ invocation: WhatsAppContactProjectionHelperInvocation,
213
+ ) => Promise<WhatsAppContactProjectionHelperResult>;
214
+ };
215
+
216
+ function isWhatsAppOperation(
217
+ value: string,
218
+ ): value is WhatsAppWebOperationName {
219
+ return (WHATSAPP_WEB_OPERATION_NAMES as readonly string[]).includes(value);
220
+ }
221
+
222
+ function requireWhatsAppAuth(auth: GhostgetAuth): WhatsAppAuth {
223
+ if (
224
+ auth.kind !== "linked-device-store"
225
+ || auth.provider !== "whatsapp"
226
+ ) {
227
+ throw new Error(
228
+ "WhatsApp protocol operations require a WhatsApp linked-device-store auth realm",
229
+ );
230
+ }
231
+ if (!isAbsolute(auth.path)) {
232
+ throw new Error("WhatsApp linked-device store path must be absolute");
233
+ }
234
+ return auth;
235
+ }
236
+
237
+ function ownedByCurrentUser(stats: Stats): boolean {
238
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
239
+ return uid === null || stats.uid === uid;
240
+ }
241
+
242
+ function assertPrivateOwned(
243
+ stats: Stats,
244
+ label: string,
245
+ kind: "directory" | "file" | "socket",
246
+ ): void {
247
+ const matchesKind = kind === "directory"
248
+ ? stats.isDirectory()
249
+ : kind === "file"
250
+ ? stats.isFile()
251
+ : stats.isSocket();
252
+ if (!matchesKind || !ownedByCurrentUser(stats)) {
253
+ throw new Error(`${label} must be an owned ${kind}`);
254
+ }
255
+ if ((stats.mode & 0o077) !== 0) {
256
+ throw new Error(`${label} must not grant group or world access`);
257
+ }
258
+ }
259
+
260
+ export type WhatsAppStoreValidationPurpose =
261
+ | "pair"
262
+ | "probe"
263
+ | "sync"
264
+ | "projection"
265
+ | "contact-projection";
266
+
267
+ /**
268
+ * Validate the complete top level of the credential/message store. This
269
+ * complements wacli's own 0700/0600 creation policy and catches later
270
+ * permission widening or symlink substitution before any trusted read.
271
+ */
272
+ export async function validateWhatsAppStoreDirectory(
273
+ pathValue: string,
274
+ purpose: WhatsAppStoreValidationPurpose,
275
+ ): Promise<string> {
276
+ if (!isAbsolute(pathValue)) {
277
+ throw new Error("WhatsApp linked-device store path must be absolute");
278
+ }
279
+ const lexical = resolve(pathValue);
280
+ let directoryStats: Stats;
281
+ try {
282
+ directoryStats = await lstat(lexical);
283
+ } catch (error) {
284
+ if (
285
+ purpose === "pair"
286
+ && typeof error === "object"
287
+ && error !== null
288
+ && "code" in error
289
+ && error.code === "ENOENT"
290
+ ) {
291
+ await mkdir(lexical, { recursive: true, mode: 0o700 });
292
+ await chmod(lexical, 0o700);
293
+ directoryStats = await lstat(lexical);
294
+ } else {
295
+ throw new Error("WhatsApp linked-device store is unavailable");
296
+ }
297
+ }
298
+ if (directoryStats.isSymbolicLink()) {
299
+ throw new Error("WhatsApp linked-device store must not be a symbolic link");
300
+ }
301
+ assertPrivateOwned(directoryStats, "WhatsApp linked-device store", "directory");
302
+ const canonical = await realpath(lexical);
303
+ if (canonical !== lexical) {
304
+ throw new Error("WhatsApp linked-device store path must be canonical");
305
+ }
306
+
307
+ const entries = await readdir(canonical, { withFileTypes: true });
308
+ if (entries.length > MAX_STORE_ENTRIES) {
309
+ throw new Error("WhatsApp linked-device store has too many entries");
310
+ }
311
+ for (const entry of entries) {
312
+ if (
313
+ entry.name.length < 1
314
+ || entry.name.length > 255
315
+ || entry.name.includes("\0")
316
+ || entry.isSymbolicLink()
317
+ ) throw new Error("WhatsApp linked-device store contains an unsafe entry");
318
+ const entryPath = join(canonical, entry.name);
319
+ const stats = await lstat(entryPath);
320
+ if (stats.isSymbolicLink()) {
321
+ throw new Error("WhatsApp linked-device store contains a symbolic link");
322
+ }
323
+ if (stats.isDirectory()) {
324
+ assertPrivateOwned(stats, "WhatsApp linked-device store entry", "directory");
325
+ } else if (stats.isFile()) {
326
+ assertPrivateOwned(stats, "WhatsApp linked-device store entry", "file");
327
+ } else if (stats.isSocket()) {
328
+ assertPrivateOwned(stats, "WhatsApp linked-device store entry", "socket");
329
+ } else {
330
+ throw new Error("WhatsApp linked-device store contains an unsupported entry");
331
+ }
332
+ }
333
+
334
+ const requireRegular = async (name: string): Promise<void> => {
335
+ let stats: Stats;
336
+ try {
337
+ stats = await lstat(join(canonical, name));
338
+ } catch {
339
+ throw new Error(`WhatsApp linked-device store omitted ${name}`);
340
+ }
341
+ assertPrivateOwned(stats, `WhatsApp ${name}`, "file");
342
+ };
343
+ if (
344
+ purpose === "sync"
345
+ || purpose === "projection"
346
+ || purpose === "contact-projection"
347
+ ) {
348
+ await requireRegular("session.db");
349
+ }
350
+ if (purpose === "projection") {
351
+ await requireRegular("wacli.db");
352
+ }
353
+ return canonical;
354
+ }
355
+
356
+ async function sha256File(path: string): Promise<string> {
357
+ const hash = createHash("sha256");
358
+ const stream = createReadStream(path, { flags: "r" });
359
+ for await (const chunkValue of stream) {
360
+ const chunk: unknown = chunkValue;
361
+ if (!Buffer.isBuffer(chunk)) {
362
+ throw new Error("WhatsApp protocol binary stream returned non-byte data");
363
+ }
364
+ hash.update(chunk);
365
+ }
366
+ return hash.digest("hex");
367
+ }
368
+
369
+ type FixedCommandResult = {
370
+ readonly exitCode: number;
371
+ readonly output: string;
372
+ };
373
+
374
+ async function runFixedCodesign(
375
+ arguments_: readonly string[],
376
+ signal?: AbortSignal,
377
+ ): Promise<FixedCommandResult> {
378
+ const isAborted = (): boolean => signal?.aborted === true;
379
+ if (isAborted()) {
380
+ return Object.freeze({ exitCode: -1, output: "" });
381
+ }
382
+ const child = Bun.spawn(["/usr/bin/codesign", ...arguments_], {
383
+ env: {
384
+ PATH: "/usr/bin:/bin",
385
+ LANG: "C",
386
+ LC_ALL: "C",
387
+ },
388
+ stdin: "ignore",
389
+ stdout: "pipe",
390
+ stderr: "pipe",
391
+ });
392
+ let interrupted = false;
393
+ const interrupt = (): void => {
394
+ interrupted = true;
395
+ try {
396
+ child.kill("SIGKILL");
397
+ } catch {
398
+ // child.exited remains the process-cleanup proof.
399
+ }
400
+ };
401
+ const onAbort = (): void => interrupt();
402
+ signal?.addEventListener("abort", onAbort, { once: true });
403
+ if (isAborted()) onAbort();
404
+ const timeout = setTimeout(interrupt, 5_000);
405
+ try {
406
+ const [stdout, stderr, exitCode] = await Promise.all([
407
+ readBoundedStream(child.stdout, 64 * 1024),
408
+ readBoundedStream(child.stderr, 64 * 1024),
409
+ child.exited,
410
+ ]);
411
+ return Object.freeze({
412
+ exitCode: interrupted ? -1 : exitCode,
413
+ output: interrupted ? "" : `${stdout}${stderr}`,
414
+ });
415
+ } catch (error) {
416
+ interrupt();
417
+ await child.exited;
418
+ throw error;
419
+ } finally {
420
+ clearTimeout(timeout);
421
+ signal?.removeEventListener("abort", onAbort);
422
+ }
423
+ }
424
+
425
+ function codeSignatureValue(
426
+ display: string,
427
+ key: string,
428
+ ): string | null {
429
+ const prefix = `${key}=`;
430
+ const lines = display.split(/\r?\n/u)
431
+ .filter((line) => line.startsWith(prefix));
432
+ if (lines.length !== 1) return null;
433
+ return lines[0]?.slice(prefix.length) ?? null;
434
+ }
435
+
436
+ function normalizeCodeRequirement(value: string): string {
437
+ return value
438
+ .replace(/\s+/gu, " ")
439
+ .replace(/"([A-Za-z0-9.]+)"/gu, "$1")
440
+ .trim();
441
+ }
442
+
443
+ async function verifyPinnedWacliSignature(
444
+ path: string,
445
+ signal?: AbortSignal,
446
+ ): Promise<boolean> {
447
+ const verified = await runFixedCodesign([
448
+ "--verify",
449
+ "--strict",
450
+ "--verbose=4",
451
+ path,
452
+ ], signal);
453
+ if (verified.exitCode !== 0) return false;
454
+
455
+ const display = await runFixedCodesign([
456
+ "--display",
457
+ "--verbose=4",
458
+ path,
459
+ ], signal);
460
+ if (display.exitCode !== 0) return false;
461
+ const signature = WHATSAPP_PROTOCOL_PIN.signature;
462
+ const authorities = display.output.split(/\r?\n/u)
463
+ .filter((line) => line.startsWith("Authority="))
464
+ .map((line) => line.slice("Authority=".length))
465
+ .filter((authority) => authority.startsWith("Developer ID Application:"));
466
+ if (
467
+ codeSignatureValue(display.output, "Identifier") !== signature.identifier
468
+ || codeSignatureValue(display.output, "TeamIdentifier")
469
+ !== signature.teamIdentifier
470
+ || codeSignatureValue(display.output, "CDHash") !== signature.cdHash
471
+ || codeSignatureValue(display.output, "CandidateCDHashFull sha256")
472
+ !== signature.cdHashFull
473
+ || authorities.length !== 1
474
+ || authorities[0] !== signature.authority
475
+ || !/\bflags=0x[0-9a-f]+\(runtime\)/iu.test(display.output)
476
+ || !/^Timestamp=(?!none$).+/mu.test(display.output)
477
+ ) return false;
478
+
479
+ const requirements = await runFixedCodesign([
480
+ "--display",
481
+ "--requirements",
482
+ "-",
483
+ path,
484
+ ], signal);
485
+ if (requirements.exitCode !== 0) return false;
486
+ const embeddedRequirements = requirements.output.split(/\r?\n/u)
487
+ .map((line) => line.trim())
488
+ .filter((line) => line.startsWith("designated =>"));
489
+ if (
490
+ embeddedRequirements.length !== 1
491
+ || normalizeCodeRequirement(embeddedRequirements[0] ?? "")
492
+ !== normalizeCodeRequirement(signature.designatedRequirement)
493
+ ) return false;
494
+
495
+ return true;
496
+ }
497
+
498
+ async function pinnedBinaryCandidate(
499
+ pathValue: string,
500
+ signal?: AbortSignal,
501
+ ): Promise<string | null> {
502
+ let canonical: string;
503
+ try {
504
+ canonical = await realpath(pathValue);
505
+ } catch {
506
+ return null;
507
+ }
508
+ try {
509
+ const stats = await lstat(canonical);
510
+ if (
511
+ !stats.isFile()
512
+ || (stats.mode & 0o077) !== 0
513
+ || (stats.mode & 0o111) === 0
514
+ || !ownedByCurrentUser(stats)
515
+ ) return null;
516
+ if (process.platform !== "darwin" || process.arch !== "arm64") return null;
517
+ if (
518
+ await sha256File(canonical)
519
+ !== WHATSAPP_PROTOCOL_PIN.darwinArm64BinarySha256
520
+ ) return null;
521
+ if (!await verifyPinnedWacliSignature(canonical, signal)) return null;
522
+ const finalStats = await lstat(canonical);
523
+ if (
524
+ !finalStats.isFile()
525
+ || finalStats.dev !== stats.dev
526
+ || finalStats.ino !== stats.ino
527
+ || finalStats.size !== stats.size
528
+ || finalStats.mode !== stats.mode
529
+ || !ownedByCurrentUser(finalStats)
530
+ || await sha256File(canonical)
531
+ !== WHATSAPP_PROTOCOL_PIN.darwinArm64BinarySha256
532
+ ) return null;
533
+ return canonical;
534
+ } catch {
535
+ return null;
536
+ }
537
+ }
538
+
539
+ export async function resolvePinnedWacliBinary(
540
+ environment: Readonly<Record<string, string | undefined>> = process.env,
541
+ signal?: AbortSignal,
542
+ ): Promise<string> {
543
+ const candidates = [
544
+ join(
545
+ ghostgetStateHome(environment),
546
+ "tools",
547
+ "wacli",
548
+ WHATSAPP_PROTOCOL_PIN.version,
549
+ WHATSAPP_PROTOCOL_PIN.transport,
550
+ "wacli",
551
+ ),
552
+ ];
553
+ for (const candidate of candidates) {
554
+ try {
555
+ assertSafeStatePath(candidate, environment);
556
+ } catch {
557
+ continue;
558
+ }
559
+ const found = await pinnedBinaryCandidate(candidate, signal);
560
+ if (found === null) continue;
561
+ try {
562
+ assertSafeStatePath(candidate, environment);
563
+ if (await realpath(candidate) === found) return found;
564
+ } catch {
565
+ // The lexical state path changed after validation.
566
+ }
567
+ }
568
+ throw new Error(
569
+ `pinned WhatsApp protocol runtime wacli ${WHATSAPP_PROTOCOL_PIN.version} is not installed or failed integrity verification`,
570
+ );
571
+ }
572
+
573
+ export type WhatsAppProtocolRuntimeStatus = {
574
+ readonly ready: boolean;
575
+ readonly implementation: typeof WHATSAPP_PROTOCOL_PIN.implementation;
576
+ readonly version: typeof WHATSAPP_PROTOCOL_PIN.version;
577
+ readonly integrity: "official-release+sha256+offline-code-signature";
578
+ readonly transport: typeof WHATSAPP_PROTOCOL_PIN.transport;
579
+ readonly archiveSha256:
580
+ typeof WHATSAPP_PROTOCOL_PIN.darwinArm64ArchiveSha256;
581
+ readonly binarySha256:
582
+ typeof WHATSAPP_PROTOCOL_PIN.darwinArm64BinarySha256;
583
+ readonly signature: Omit<typeof WHATSAPP_PROTOCOL_PIN.signature, "notarized">;
584
+ readonly qualification: "read-only-runtime";
585
+ readonly setupCommand: string;
586
+ };
587
+
588
+ function shellQuote(value: string): string {
589
+ return `'${value.replaceAll("'", "'\\''")}'`;
590
+ }
591
+
592
+ export async function inspectWhatsAppProtocolRuntime(
593
+ environment: Readonly<Record<string, string | undefined>> = process.env,
594
+ ): Promise<WhatsAppProtocolRuntimeStatus> {
595
+ const installer = fileURLToPath(
596
+ new URL("../scripts/install-whatsapp-protocol.sh", import.meta.url),
597
+ );
598
+ let ready = false;
599
+ try {
600
+ await resolvePinnedWacliBinary(environment);
601
+ ready = true;
602
+ } catch {
603
+ // Doctor exposes categorical readiness and a deterministic setup command,
604
+ // never candidate paths or integrity failure details.
605
+ }
606
+ return Object.freeze({
607
+ ready,
608
+ implementation: WHATSAPP_PROTOCOL_PIN.implementation,
609
+ version: WHATSAPP_PROTOCOL_PIN.version,
610
+ integrity: "official-release+sha256+offline-code-signature",
611
+ transport: WHATSAPP_PROTOCOL_PIN.transport,
612
+ archiveSha256: WHATSAPP_PROTOCOL_PIN.darwinArm64ArchiveSha256,
613
+ binarySha256: WHATSAPP_PROTOCOL_PIN.darwinArm64BinarySha256,
614
+ signature: Object.freeze({
615
+ authority: WHATSAPP_PROTOCOL_PIN.signature.authority,
616
+ identifier: WHATSAPP_PROTOCOL_PIN.signature.identifier,
617
+ teamIdentifier: WHATSAPP_PROTOCOL_PIN.signature.teamIdentifier,
618
+ designatedRequirement: WHATSAPP_PROTOCOL_PIN.signature.designatedRequirement,
619
+ cdHash: WHATSAPP_PROTOCOL_PIN.signature.cdHash,
620
+ cdHashFull: WHATSAPP_PROTOCOL_PIN.signature.cdHashFull,
621
+ hardenedRuntime: WHATSAPP_PROTOCOL_PIN.signature.hardenedRuntime,
622
+ }),
623
+ qualification: "read-only-runtime",
624
+ setupCommand: `/bin/sh ${shellQuote(installer)}`,
625
+ });
626
+ }
627
+
628
+ async function readBoundedStream(
629
+ stream: ReadableStream<Uint8Array>,
630
+ maximum: number,
631
+ ): Promise<string> {
632
+ return readBoundedStreamControlled(stream, maximum).promise;
633
+ }
634
+
635
+ function readBoundedStreamControlled(
636
+ stream: ReadableStream<Uint8Array>,
637
+ maximum: number,
638
+ ): Readonly<{ promise: Promise<string>; cancel: () => Promise<void> }> {
639
+ const reader = stream.getReader();
640
+ const output = new BoundedByteBuffer(maximum);
641
+ const promise = (async (): Promise<string> => {
642
+ try {
643
+ for (;;) {
644
+ const item = await reader.read();
645
+ if (item.done) break;
646
+ if (!output.append(item.value)) {
647
+ throw new Error("WhatsApp protocol process output exceeded its bound");
648
+ }
649
+ }
650
+ } finally {
651
+ reader.releaseLock();
652
+ }
653
+ return new TextDecoder("utf-8", { fatal: true }).decode(
654
+ output.toUint8Array(),
655
+ );
656
+ })();
657
+ return Object.freeze({
658
+ promise,
659
+ cancel: async () => {
660
+ try { await reader.cancel(); } catch { /* settlement remains authoritative */ }
661
+ },
662
+ });
663
+ }
664
+
665
+ async function runWacli(
666
+ invocation: WacliInvocation,
667
+ ): Promise<WacliInvocationResult> {
668
+ const cancellationSignal = invocation.signal;
669
+ const isCancelled = (): boolean =>
670
+ cancellationSignal?.aborted === true;
671
+ if (isCancelled()) {
672
+ throw new Error("WhatsApp protocol command was cancelled");
673
+ }
674
+ const ownsProcessGroup = (
675
+ cancellationSignal !== undefined
676
+ && process.platform !== "win32"
677
+ );
678
+ const child = Bun.spawn(
679
+ [invocation.binary, ...invocation.arguments],
680
+ {
681
+ env: { ...invocation.environment },
682
+ stdin: "ignore",
683
+ stdout: "pipe",
684
+ stderr: "pipe",
685
+ detached: ownsProcessGroup,
686
+ },
687
+ );
688
+ let timedOut = false;
689
+ let cancelled = false;
690
+ let forceKill: ReturnType<typeof setTimeout> | null = null;
691
+ const signalChild = (signal: "SIGTERM" | "SIGKILL"): void => {
692
+ if (ownsProcessGroup) {
693
+ try {
694
+ process.kill(-child.pid, signal);
695
+ return;
696
+ } catch (error) {
697
+ if (
698
+ typeof error === "object"
699
+ && error !== null
700
+ && "code" in error
701
+ && error.code === "ESRCH"
702
+ ) return;
703
+ }
704
+ }
705
+ try {
706
+ child.kill(signal);
707
+ } catch {
708
+ // The process already exited between the state check and the signal.
709
+ }
710
+ };
711
+ const terminate = (): void => {
712
+ signalChild("SIGTERM");
713
+ if (forceKill === null) {
714
+ forceKill = setTimeout(() => signalChild("SIGKILL"), 1_000);
715
+ }
716
+ };
717
+ const onAbort = (): void => {
718
+ cancelled = true;
719
+ terminate();
720
+ };
721
+ cancellationSignal?.addEventListener("abort", onAbort, { once: true });
722
+ if (isCancelled()) onAbort();
723
+ const timeout = setTimeout(() => {
724
+ timedOut = true;
725
+ terminate();
726
+ }, invocation.timeoutMs);
727
+ try {
728
+ const [stdout, stderr, exitCode] = await Promise.all([
729
+ readBoundedStream(child.stdout, invocation.maxOutputBytes),
730
+ readBoundedStream(
731
+ child.stderr,
732
+ Math.min(invocation.maxOutputBytes, MAX_STDERR_BYTES),
733
+ ),
734
+ child.exited,
735
+ ]);
736
+ if (cancelled) throw new Error("WhatsApp protocol command was cancelled");
737
+ if (timedOut) throw new Error("WhatsApp protocol command timed out");
738
+ return { exitCode, stdout, stderr };
739
+ } catch (error) {
740
+ signalChild("SIGKILL");
741
+ await child.exited;
742
+ throw error;
743
+ } finally {
744
+ clearTimeout(timeout);
745
+ if (forceKill !== null) clearTimeout(forceKill);
746
+ cancellationSignal?.removeEventListener("abort", onAbort);
747
+ }
748
+ }
749
+
750
+ function wacliEnvironment(
751
+ readOnly: boolean,
752
+ ): Readonly<Record<string, string>> {
753
+ return Object.freeze({
754
+ PATH: "/usr/bin:/bin",
755
+ LANG: "C.UTF-8",
756
+ ...(readOnly ? { WACLI_READONLY: "1" } : {}),
757
+ });
758
+ }
759
+
760
+ function remainingTimeoutMs(
761
+ timeoutMs: number,
762
+ operationDeadline: WebSessionOperationDeadline | undefined,
763
+ ): number {
764
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
765
+ const remaining = Math.min(
766
+ timeoutMs,
767
+ operationDeadline?.remainingTimeMs() ?? timeoutMs,
768
+ );
769
+ if (remaining < 1) {
770
+ throw new Error("WhatsApp authenticated web operation timed out");
771
+ }
772
+ return remaining;
773
+ }
774
+
775
+ async function checkedRun(
776
+ binary: string,
777
+ arguments_: readonly string[],
778
+ options: {
779
+ readonly timeoutMs: number;
780
+ readonly maxOutputBytes: number;
781
+ readonly readOnly: boolean;
782
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
783
+ readonly operationDeadline?: WebSessionOperationDeadline;
784
+ },
785
+ ): Promise<unknown> {
786
+ const timeoutMs = remainingTimeoutMs(
787
+ options.timeoutMs,
788
+ options.operationDeadline,
789
+ );
790
+ const run = options.dependencies?.run ?? runWacli;
791
+ const invoke = () => run({
792
+ binary,
793
+ arguments: arguments_,
794
+ environment: wacliEnvironment(options.readOnly),
795
+ timeoutMs,
796
+ maxOutputBytes: options.maxOutputBytes,
797
+ ...(options.operationDeadline === undefined
798
+ ? {}
799
+ : { signal: options.operationDeadline.signal }),
800
+ });
801
+ const result = options.operationDeadline === undefined
802
+ ? await invoke()
803
+ : await options.operationDeadline.run(
804
+ invoke,
805
+ WEB_SESSION_OPERATION_LABEL,
806
+ );
807
+ options.operationDeadline?.throwIfUnavailable(
808
+ WEB_SESSION_OPERATION_LABEL,
809
+ );
810
+ if (result.exitCode !== 0) {
811
+ throw new Error(
812
+ "WhatsApp protocol command failed before producing reviewed output",
813
+ );
814
+ }
815
+ const raw = result.stdout.trim();
816
+ if (raw.length < 1) {
817
+ throw new Error("WhatsApp protocol command omitted JSON output");
818
+ }
819
+ try {
820
+ return JSON.parse(raw) as unknown;
821
+ } catch {
822
+ throw new Error("WhatsApp protocol command returned malformed JSON");
823
+ }
824
+ }
825
+
826
+ async function runtimeBinary(
827
+ dependencies: WhatsAppWebRuntimeDependencies | undefined,
828
+ environment: Readonly<Record<string, string | undefined>>,
829
+ operationDeadline?: WebSessionOperationDeadline,
830
+ ): Promise<string> {
831
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
832
+ const resolveBinary = () => resolvePinnedWacliBinary(
833
+ environment,
834
+ operationDeadline?.signal,
835
+ );
836
+ const binary = dependencies?.binaryPath
837
+ ?? (operationDeadline === undefined
838
+ ? await resolveBinary()
839
+ : await operationDeadline.run(
840
+ resolveBinary,
841
+ WEB_SESSION_OPERATION_LABEL,
842
+ ));
843
+ if (dependencies?.binaryPath !== undefined && !isAbsolute(binary)) {
844
+ throw new Error("test WhatsApp protocol binary path must be absolute");
845
+ }
846
+ const timeoutMs = remainingTimeoutMs(5_000, operationDeadline);
847
+ const run = dependencies?.run ?? runWacli;
848
+ const invoke = () => run({
849
+ binary,
850
+ arguments: ["version"],
851
+ environment: wacliEnvironment(true),
852
+ timeoutMs,
853
+ maxOutputBytes: 1_024,
854
+ ...(operationDeadline === undefined
855
+ ? {}
856
+ : { signal: operationDeadline.signal }),
857
+ });
858
+ const result = operationDeadline === undefined
859
+ ? await invoke()
860
+ : await operationDeadline.run(
861
+ invoke,
862
+ WEB_SESSION_OPERATION_LABEL,
863
+ );
864
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
865
+ if (
866
+ result.exitCode !== 0
867
+ || result.stdout.trim() !== WHATSAPP_PROTOCOL_PIN.version
868
+ ) {
869
+ throw new Error("WhatsApp protocol runtime version did not match its pin");
870
+ }
871
+ return binary;
872
+ }
873
+
874
+ function readOnlyArguments(
875
+ store: string,
876
+ timeoutMs: number,
877
+ command: readonly string[],
878
+ ): readonly string[] {
879
+ return Object.freeze([
880
+ "--store",
881
+ store,
882
+ "--read-only",
883
+ "--json",
884
+ "--full",
885
+ "--timeout",
886
+ `${Math.max(1, timeoutMs)}ms`,
887
+ ...command,
888
+ ]);
889
+ }
890
+
891
+ async function authStatus(
892
+ binary: string,
893
+ store: string,
894
+ timeoutMs: number,
895
+ dependencies: WhatsAppWebRuntimeDependencies | undefined,
896
+ operationDeadline?: WebSessionOperationDeadline,
897
+ ): Promise<ReturnType<typeof parseWhatsAppAuthStatusEnvelope>> {
898
+ const commandTimeoutMs = remainingTimeoutMs(
899
+ timeoutMs,
900
+ operationDeadline,
901
+ );
902
+ return parseWhatsAppAuthStatusEnvelope(await checkedRun(
903
+ binary,
904
+ readOnlyArguments(store, commandTimeoutMs, ["auth", "status"]),
905
+ {
906
+ timeoutMs: commandTimeoutMs,
907
+ maxOutputBytes: 64 * 1024,
908
+ readOnly: true,
909
+ ...(dependencies === undefined ? {} : { dependencies }),
910
+ ...(operationDeadline === undefined
911
+ ? {}
912
+ : { operationDeadline }),
913
+ },
914
+ ));
915
+ }
916
+
917
+ async function boundRuntime(
918
+ auth: GhostgetAuth,
919
+ purpose: "probe" | "projection",
920
+ timeoutMs: number,
921
+ dependencies: WhatsAppWebRuntimeDependencies | undefined,
922
+ environment: Readonly<Record<string, string | undefined>>,
923
+ operationDeadline?: WebSessionOperationDeadline,
924
+ ): Promise<{
925
+ readonly auth: WhatsAppAuth;
926
+ readonly binary: string;
927
+ readonly store: string;
928
+ readonly subject: string;
929
+ }> {
930
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
931
+ const linked = requireWhatsAppAuth(auth);
932
+ const validateStore = () =>
933
+ validateWhatsAppStoreDirectory(linked.path, purpose);
934
+ const store = operationDeadline === undefined
935
+ ? await validateStore()
936
+ : await operationDeadline.run(
937
+ validateStore,
938
+ WEB_SESSION_OPERATION_LABEL,
939
+ );
940
+ const binary = await runtimeBinary(
941
+ dependencies,
942
+ environment,
943
+ operationDeadline,
944
+ );
945
+ const status = await authStatus(
946
+ binary,
947
+ store,
948
+ timeoutMs,
949
+ dependencies,
950
+ operationDeadline,
951
+ );
952
+ if (!status.authenticated || status.subject === null) {
953
+ throw new Error(
954
+ "WhatsApp linked-device store is not paired; run the explicit auth pairing flow",
955
+ );
956
+ }
957
+ if (purpose === "projection") {
958
+ if (linked.subject === undefined) {
959
+ throw new Error(
960
+ "WhatsApp linked-device auth must be bound to its current account before private reads",
961
+ );
962
+ }
963
+ if (linked.subject !== status.subject) {
964
+ throw new Error(
965
+ "WhatsApp linked-device account did not match the bound auth realm",
966
+ );
967
+ }
968
+ }
969
+ return {
970
+ auth: linked,
971
+ binary,
972
+ store,
973
+ subject: status.subject,
974
+ };
975
+ }
976
+
977
+ export async function probeWhatsAppWebSubject(
978
+ auth: GhostgetAuth,
979
+ options: {
980
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
981
+ readonly environment?: Readonly<Record<string, string | undefined>>;
982
+ readonly timeoutMs?: number;
983
+ readonly signal?: AbortSignal;
984
+ } = {},
985
+ ): Promise<string> {
986
+ const timeoutMs = options.timeoutMs ?? 10_000;
987
+ const deadline = new OperationDeadline(timeoutMs, {
988
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
989
+ });
990
+ try {
991
+ const runtime = await boundRuntime(
992
+ auth,
993
+ "probe",
994
+ timeoutMs,
995
+ options.dependencies,
996
+ options.environment ?? process.env,
997
+ deadline,
998
+ );
999
+ deadline.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1000
+ return runtime.subject;
1001
+ } finally {
1002
+ deadline.dispose();
1003
+ }
1004
+ }
1005
+
1006
+ function inputInteger(
1007
+ input: OperationInput,
1008
+ name: string,
1009
+ fallback: number,
1010
+ maximum: number,
1011
+ ): number {
1012
+ const value = input[name] ?? fallback;
1013
+ if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
1014
+ throw new Error(`input.${name} must be an integer between 1 and ${maximum}`);
1015
+ }
1016
+ return value as number;
1017
+ }
1018
+
1019
+ function inputFolder(input: OperationInput): "all" | "active" | "archived" | "unread" {
1020
+ const value = input.folder ?? "all";
1021
+ if (
1022
+ value !== "all"
1023
+ && value !== "active"
1024
+ && value !== "archived"
1025
+ && value !== "unread"
1026
+ ) throw new Error("input.folder must be all, active, archived, or unread");
1027
+ return value;
1028
+ }
1029
+
1030
+ type ExactContactInput =
1031
+ | { readonly collection: "contacts"; readonly cursor: string | null; readonly limit: number }
1032
+ | { readonly collection: "interactions"; readonly cursor: string; readonly cursorAnchor: string | null; readonly limit: number };
1033
+
1034
+ function exactContactInput(input: OperationInput): ExactContactInput {
1035
+ const unexpected = Object.keys(input).filter(
1036
+ (key) => key !== "collection" && key !== "cursor" && key !== "cursor_anchor" && key !== "limit",
1037
+ );
1038
+ if (unexpected.length > 0) {
1039
+ throw new Error("WhatsApp contacts.list input contained unsupported fields");
1040
+ }
1041
+ const collection = input.collection ?? "contacts";
1042
+ if (collection !== "contacts" && collection !== "interactions") {
1043
+ throw new Error("input.collection must be contacts or interactions");
1044
+ }
1045
+ if (collection === "interactions") {
1046
+ const cursor = input.cursor === undefined
1047
+ ? "0"
1048
+ : parseWhatsAppInteractionRowid(input.cursor, "input.cursor");
1049
+ const cursorAnchor = input.cursor_anchor === undefined
1050
+ ? null
1051
+ : typeof input.cursor_anchor === "string" && /^[a-f0-9]{64}$/u.test(input.cursor_anchor)
1052
+ ? input.cursor_anchor
1053
+ : (() => { throw new Error("input.cursor_anchor must be a SHA-256 digest"); })();
1054
+ if ((cursor === "0") !== (cursorAnchor === null)) {
1055
+ throw new Error("input.cursor_anchor must bind every nonzero interaction cursor");
1056
+ }
1057
+ return Object.freeze({
1058
+ collection,
1059
+ cursor,
1060
+ cursorAnchor,
1061
+ limit: inputInteger(input, "limit", DEFAULT_LIMIT, 1_000),
1062
+ });
1063
+ }
1064
+ if (input.cursor_anchor !== undefined) {
1065
+ throw new Error("input.cursor_anchor is only supported for interaction scans");
1066
+ }
1067
+ let cursor: string | null = null;
1068
+ if (input.cursor !== undefined) {
1069
+ const parsed = parseWhatsAppJid(input.cursor, "input.cursor");
1070
+ if (
1071
+ (parsed.kind !== "user" && parsed.kind !== "lid")
1072
+ || parsed.jid.includes(":")
1073
+ ) {
1074
+ throw new Error("input.cursor must be one exact contact user or LID JID");
1075
+ }
1076
+ cursor = parsed.jid;
1077
+ }
1078
+ return Object.freeze({
1079
+ collection,
1080
+ cursor,
1081
+ limit: inputInteger(input, "limit", DEFAULT_LIMIT, 100),
1082
+ });
1083
+ }
1084
+
1085
+ type ContactProjectionParentIdentity = Readonly<{
1086
+ store: BigIntStats;
1087
+ session: BigIntStats;
1088
+ }>;
1089
+
1090
+ type InteractionProjectionParentIdentity = Readonly<{
1091
+ store: BigIntStats;
1092
+ session: BigIntStats;
1093
+ messageStore: BigIntStats;
1094
+ }>;
1095
+
1096
+ function sameContactProjectionSnapshot(
1097
+ left: BigIntStats,
1098
+ right: BigIntStats,
1099
+ ): boolean {
1100
+ return left.dev === right.dev
1101
+ && left.ino === right.ino
1102
+ && left.mode === right.mode
1103
+ && left.uid === right.uid
1104
+ && left.gid === right.gid
1105
+ && left.nlink === right.nlink
1106
+ && left.size === right.size
1107
+ && left.mtimeNs === right.mtimeNs
1108
+ && left.ctimeNs === right.ctimeNs;
1109
+ }
1110
+
1111
+ function currentBigIntUid(): bigint | null {
1112
+ return typeof process.getuid === "function" ? BigInt(process.getuid()) : null;
1113
+ }
1114
+
1115
+ function assertParentContactProjectionIdentity(
1116
+ store: BigIntStats,
1117
+ session: BigIntStats,
1118
+ ): void {
1119
+ const uid = currentBigIntUid();
1120
+ if (
1121
+ !store.isDirectory()
1122
+ || store.isSymbolicLink()
1123
+ || (uid !== null && store.uid !== uid)
1124
+ || !isExactWhatsAppContactProjectionMode(store.mode, 0o700)
1125
+ || !session.isFile()
1126
+ || session.isSymbolicLink()
1127
+ || session.nlink !== 1n
1128
+ || (uid !== null && session.uid !== uid)
1129
+ || !isExactWhatsAppContactProjectionMode(session.mode, 0o600)
1130
+ || session.size < 1n
1131
+ || session.size > 128n * 1024n * 1024n
1132
+ ) {
1133
+ throw new Error(
1134
+ "WhatsApp contact projection parent could not verify its private session store",
1135
+ );
1136
+ }
1137
+ }
1138
+
1139
+ async function captureContactProjectionParentIdentity(
1140
+ store: string,
1141
+ ): Promise<ContactProjectionParentIdentity> {
1142
+ try {
1143
+ if (await realpath(store) !== store) {
1144
+ throw new Error("non-canonical");
1145
+ }
1146
+ const [storeStats, sessionStats] = await Promise.all([
1147
+ lstat(store, { bigint: true }),
1148
+ lstat(join(store, "session.db"), { bigint: true }),
1149
+ ]);
1150
+ assertParentContactProjectionIdentity(storeStats, sessionStats);
1151
+ return Object.freeze({ store: storeStats, session: sessionStats });
1152
+ } catch {
1153
+ throw new Error(
1154
+ "WhatsApp contact projection parent could not bind its private session store",
1155
+ );
1156
+ }
1157
+ }
1158
+
1159
+ async function revalidateContactProjectionParentIdentity(
1160
+ store: string,
1161
+ initial: ContactProjectionParentIdentity,
1162
+ ): Promise<void> {
1163
+ try {
1164
+ if (await realpath(store) !== store) throw new Error("non-canonical");
1165
+ const [storeStats, sessionStats] = await Promise.all([
1166
+ lstat(store, { bigint: true }),
1167
+ lstat(join(store, "session.db"), { bigint: true }),
1168
+ ]);
1169
+ assertParentContactProjectionIdentity(storeStats, sessionStats);
1170
+ if (
1171
+ !sameContactProjectionSnapshot(initial.store, storeStats)
1172
+ || !sameContactProjectionSnapshot(initial.session, sessionStats)
1173
+ ) throw new Error("identity changed");
1174
+ } catch {
1175
+ throw new Error(
1176
+ "WhatsApp contact projection parent binding changed during the helper read",
1177
+ );
1178
+ }
1179
+ }
1180
+
1181
+ function assertParentInteractionProjectionIdentity(
1182
+ store: BigIntStats,
1183
+ session: BigIntStats,
1184
+ messageStore: BigIntStats,
1185
+ ): void {
1186
+ assertParentContactProjectionIdentity(store, session);
1187
+ const uid = currentBigIntUid();
1188
+ if (
1189
+ !messageStore.isFile()
1190
+ || messageStore.isSymbolicLink()
1191
+ || messageStore.nlink !== 1n
1192
+ || (uid !== null && messageStore.uid !== uid)
1193
+ || !isExactWhatsAppContactProjectionMode(messageStore.mode, 0o600)
1194
+ || messageStore.size < 1n
1195
+ || messageStore.size > 2n * 1024n * 1024n * 1024n
1196
+ ) {
1197
+ throw new Error(
1198
+ "WhatsApp interaction projection parent could not verify its private message store",
1199
+ );
1200
+ }
1201
+ }
1202
+
1203
+ async function captureInteractionProjectionParentIdentity(
1204
+ store: string,
1205
+ ): Promise<InteractionProjectionParentIdentity> {
1206
+ try {
1207
+ if (await realpath(store) !== store) throw new Error("non-canonical");
1208
+ const [storeStats, sessionStats, messageStoreStats] = await Promise.all([
1209
+ lstat(store, { bigint: true }),
1210
+ lstat(join(store, "session.db"), { bigint: true }),
1211
+ lstat(join(store, "wacli.db"), { bigint: true }),
1212
+ ]);
1213
+ assertParentInteractionProjectionIdentity(
1214
+ storeStats,
1215
+ sessionStats,
1216
+ messageStoreStats,
1217
+ );
1218
+ return Object.freeze({
1219
+ store: storeStats,
1220
+ session: sessionStats,
1221
+ messageStore: messageStoreStats,
1222
+ });
1223
+ } catch {
1224
+ throw new Error(
1225
+ "WhatsApp interaction projection parent could not bind its private stores",
1226
+ );
1227
+ }
1228
+ }
1229
+
1230
+ async function revalidateInteractionProjectionParentIdentity(
1231
+ store: string,
1232
+ initial: InteractionProjectionParentIdentity,
1233
+ ): Promise<void> {
1234
+ try {
1235
+ if (await realpath(store) !== store) throw new Error("non-canonical");
1236
+ const [storeStats, sessionStats, messageStoreStats] = await Promise.all([
1237
+ lstat(store, { bigint: true }),
1238
+ lstat(join(store, "session.db"), { bigint: true }),
1239
+ lstat(join(store, "wacli.db"), { bigint: true }),
1240
+ ]);
1241
+ assertParentInteractionProjectionIdentity(
1242
+ storeStats,
1243
+ sessionStats,
1244
+ messageStoreStats,
1245
+ );
1246
+ if (
1247
+ !sameContactProjectionSnapshot(initial.store, storeStats)
1248
+ || !sameContactProjectionSnapshot(initial.session, sessionStats)
1249
+ || !sameContactProjectionSnapshot(initial.messageStore, messageStoreStats)
1250
+ ) throw new Error("identity changed");
1251
+ } catch {
1252
+ throw new Error(
1253
+ "WhatsApp interaction projection parent binding changed during the helper read",
1254
+ );
1255
+ }
1256
+ }
1257
+
1258
+ type FixedContactProjectionFiles = Readonly<{
1259
+ helper: string;
1260
+ config: string;
1261
+ }>;
1262
+
1263
+ async function fixedContactProjectionFile(
1264
+ pathValue: string,
1265
+ ): Promise<string | null> {
1266
+ try {
1267
+ const stats = await lstat(pathValue);
1268
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1269
+ if (
1270
+ stats.isSymbolicLink()
1271
+ || !stats.isFile()
1272
+ || stats.nlink !== 1
1273
+ || stats.size < 1
1274
+ || stats.size > 2 * 1024 * 1024
1275
+ || (stats.mode & 0o022) !== 0
1276
+ || (uid !== null && stats.uid !== uid && stats.uid !== 0)
1277
+ || await realpath(pathValue) !== pathValue
1278
+ ) {
1279
+ throw new Error("unsafe fixed file");
1280
+ }
1281
+ return pathValue;
1282
+ } catch (error) {
1283
+ if (
1284
+ typeof error === "object"
1285
+ && error !== null
1286
+ && "code" in error
1287
+ && error.code === "ENOENT"
1288
+ ) return null;
1289
+ throw new Error(
1290
+ "WhatsApp contact projection fixed helper files failed validation",
1291
+ );
1292
+ }
1293
+ }
1294
+
1295
+ async function resolveFixedContactProjectionFiles(
1296
+ helperName = "whatsapp-contact-projection-helper.ts",
1297
+ ): Promise<
1298
+ FixedContactProjectionFiles
1299
+ > {
1300
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
1301
+ const candidates = [
1302
+ {
1303
+ helper: resolve(
1304
+ moduleDirectory,
1305
+ helperName,
1306
+ ),
1307
+ config: resolve(moduleDirectory, "../state-helper.bunfig.toml"),
1308
+ },
1309
+ {
1310
+ helper: resolve(
1311
+ moduleDirectory,
1312
+ `../src/providers/${helperName}`,
1313
+ ),
1314
+ config: resolve(
1315
+ moduleDirectory,
1316
+ "../src/state-helper.bunfig.toml",
1317
+ ),
1318
+ },
1319
+ ] as const;
1320
+ for (const candidate of candidates) {
1321
+ const [helper, config] = await Promise.all([
1322
+ fixedContactProjectionFile(candidate.helper),
1323
+ fixedContactProjectionFile(candidate.config),
1324
+ ]);
1325
+ if (helper === null && config === null) continue;
1326
+ if (helper === null || config === null) {
1327
+ throw new Error(
1328
+ "WhatsApp contact projection fixed helper installation is incomplete",
1329
+ );
1330
+ }
1331
+ return Object.freeze({ helper, config });
1332
+ }
1333
+ throw new Error(
1334
+ "WhatsApp contact projection fixed helper is not installed",
1335
+ );
1336
+ }
1337
+
1338
+ function contactProjectionEnvironment(): Readonly<Record<string, string>> {
1339
+ return Object.freeze({
1340
+ PATH: "/usr/bin:/bin",
1341
+ LANG: "C.UTF-8",
1342
+ LC_ALL: "C.UTF-8",
1343
+ TZ: "UTC",
1344
+ });
1345
+ }
1346
+
1347
+ export async function runWhatsAppContactProjectionHelperChild(
1348
+ invocation: WhatsAppContactProjectionHelperInvocation,
1349
+ ): Promise<WhatsAppContactProjectionHelperResult> {
1350
+ const isAborted = (): boolean => invocation.signal?.aborted === true;
1351
+ if (isAborted()) {
1352
+ throw new Error("WhatsApp contact projection helper was cancelled");
1353
+ }
1354
+ let child: Bun.Subprocess<"pipe", "pipe", "pipe">;
1355
+ try {
1356
+ child = Bun.spawn([...invocation.command], {
1357
+ cwd: invocation.cwd,
1358
+ env: { ...invocation.environment },
1359
+ stdin: "pipe",
1360
+ stdout: "pipe",
1361
+ stderr: "pipe",
1362
+ detached: process.platform !== "win32",
1363
+ });
1364
+ } catch {
1365
+ throw new Error("WhatsApp contact projection helper could not start");
1366
+ }
1367
+
1368
+ if (invocation.onSpawned !== undefined) {
1369
+ try {
1370
+ invocation.onSpawned(child.pid);
1371
+ } catch (error) {
1372
+ try {
1373
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL");
1374
+ else child.kill("SIGKILL");
1375
+ } catch {
1376
+ // child.exited below is still the exact cleanup proof.
1377
+ }
1378
+ try {
1379
+ await child.exited;
1380
+ } catch {
1381
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1382
+ }
1383
+ throw error;
1384
+ }
1385
+ }
1386
+
1387
+ let timedOut = false;
1388
+ let cancelled = false;
1389
+ let forceKill: ReturnType<typeof setTimeout> | undefined;
1390
+ let terminationStarted = false;
1391
+ const signalChild = (signal: "SIGTERM" | "SIGKILL"): void => {
1392
+ if (process.platform !== "win32") {
1393
+ try {
1394
+ process.kill(-child.pid, signal);
1395
+ return;
1396
+ } catch (error) {
1397
+ if (
1398
+ typeof error === "object"
1399
+ && error !== null
1400
+ && "code" in error
1401
+ && error.code === "ESRCH"
1402
+ ) return;
1403
+ }
1404
+ }
1405
+ try {
1406
+ child.kill(signal);
1407
+ } catch {
1408
+ // child.exited remains the cleanup proof. If it cannot settle, the
1409
+ // registered cleanup barrier reaches the kernel's unsafe bounded join.
1410
+ }
1411
+ };
1412
+ const terminate = (): void => {
1413
+ if (!terminationStarted) {
1414
+ terminationStarted = true;
1415
+ signalChild("SIGTERM");
1416
+ }
1417
+ forceKill ??= setTimeout(
1418
+ () => signalChild("SIGKILL"),
1419
+ CONTACT_PROJECTION_FORCE_KILL_DELAY_MS,
1420
+ );
1421
+ };
1422
+ const onAbort = (): void => {
1423
+ cancelled = true;
1424
+ terminate();
1425
+ };
1426
+ invocation.signal?.addEventListener("abort", onAbort, { once: true });
1427
+ if (isAborted()) onAbort();
1428
+ const timeout = setTimeout(() => {
1429
+ timedOut = true;
1430
+ terminate();
1431
+ }, invocation.timeoutMs);
1432
+
1433
+ const guarded = <T>(promise: Promise<T>): Promise<T> =>
1434
+ promise.catch((error: unknown) => {
1435
+ terminate();
1436
+ throw error;
1437
+ });
1438
+ const stdin = guarded((async () => {
1439
+ await child.stdin.write(invocation.stdin);
1440
+ await child.stdin.end();
1441
+ })());
1442
+ const stdout = guarded(readBoundedStream(
1443
+ child.stdout,
1444
+ invocation.maxOutputBytes,
1445
+ ));
1446
+ const stderr = guarded(readBoundedStream(
1447
+ child.stderr,
1448
+ invocation.maxStderrBytes,
1449
+ ));
1450
+ const exited = child.exited;
1451
+ try {
1452
+ const [stdinResult, stdoutResult, stderrResult, exitResult] =
1453
+ await Promise.allSettled([stdin, stdout, stderr, exited]);
1454
+ if (exitResult.status === "rejected") {
1455
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1456
+ }
1457
+ if (
1458
+ stdinResult.status === "rejected"
1459
+ || stdoutResult.status === "rejected"
1460
+ || stderrResult.status === "rejected"
1461
+ ) {
1462
+ throw new Error(
1463
+ "WhatsApp contact projection helper stream failed within its bound",
1464
+ );
1465
+ }
1466
+ if (cancelled) {
1467
+ throw new Error("WhatsApp contact projection helper was cancelled");
1468
+ }
1469
+ if (timedOut) {
1470
+ throw new Error("WhatsApp contact projection helper timed out");
1471
+ }
1472
+ return Object.freeze({
1473
+ exitCode: exitResult.value,
1474
+ stdout: stdoutResult.value,
1475
+ stderr: stderrResult.value,
1476
+ });
1477
+ } finally {
1478
+ clearTimeout(timeout);
1479
+ if (forceKill !== undefined) clearTimeout(forceKill);
1480
+ invocation.signal?.removeEventListener("abort", onAbort);
1481
+ }
1482
+ }
1483
+
1484
+ class CanonicalSessionFrameDecoder {
1485
+ readonly #maximumFrameBytes: number;
1486
+ readonly #onFrame: (frame: WhatsAppMessageExportSessionCanonicalFrame) => void;
1487
+ #chunks: Buffer[] = [];
1488
+ #pendingBytes = 0;
1489
+ #frameCount = 0;
1490
+
1491
+ constructor(
1492
+ maximumFrameBytes: number,
1493
+ onFrame: (frame: WhatsAppMessageExportSessionCanonicalFrame) => void,
1494
+ ) {
1495
+ this.#maximumFrameBytes = maximumFrameBytes;
1496
+ this.#onFrame = onFrame;
1497
+ }
1498
+
1499
+ get frameCount(): number {
1500
+ return this.#frameCount;
1501
+ }
1502
+
1503
+ #append(bytes: Uint8Array): void {
1504
+ if (bytes.byteLength === 0) return;
1505
+ this.#pendingBytes += bytes.byteLength;
1506
+ if (this.#pendingBytes + 1 > this.#maximumFrameBytes) {
1507
+ throw new Error("WhatsApp projection session frame exceeded its bound");
1508
+ }
1509
+ // Copy bounded pieces so replay may safely reuse its fixed read buffer.
1510
+ this.#chunks.push(Buffer.from(bytes));
1511
+ }
1512
+
1513
+ #finishLine(): void {
1514
+ if (
1515
+ this.#pendingBytes < 2
1516
+ || this.#pendingBytes + 1 > this.#maximumFrameBytes
1517
+ || this.#frameCount >= MESSAGE_EXPORT_SESSION_MAX_FRAMES
1518
+ ) throw new Error("WhatsApp projection session frame exceeded its bound");
1519
+ const bytes = this.#chunks.length === 1
1520
+ ? this.#chunks[0]!
1521
+ : Buffer.concat(this.#chunks, this.#pendingBytes);
1522
+ this.#chunks = [];
1523
+ this.#pendingBytes = 0;
1524
+ let line: string;
1525
+ try {
1526
+ line = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
1527
+ } catch {
1528
+ throw new Error("WhatsApp projection session frame was malformed");
1529
+ }
1530
+ if (line.includes("\r")) {
1531
+ throw new Error("WhatsApp projection session frame exceeded its bound");
1532
+ }
1533
+ let value: unknown;
1534
+ try {
1535
+ value = JSON.parse(line) as unknown;
1536
+ } catch {
1537
+ throw new Error("WhatsApp projection session frame was malformed");
1538
+ }
1539
+ const canonical = canonicalJson(value);
1540
+ if (canonical !== line || !Buffer.from(canonical, "utf8").equals(bytes)) {
1541
+ throw new Error("WhatsApp projection session frame was not canonical");
1542
+ }
1543
+ this.#frameCount += 1;
1544
+ this.#onFrame(Object.freeze({
1545
+ index: this.#frameCount,
1546
+ canonical,
1547
+ value,
1548
+ }));
1549
+ }
1550
+
1551
+ push(bytes: Uint8Array): void {
1552
+ let start = 0;
1553
+ for (let index = 0; index < bytes.byteLength; index += 1) {
1554
+ if (bytes[index] !== 0x0a) continue;
1555
+ this.#append(bytes.subarray(start, index));
1556
+ this.#finishLine();
1557
+ start = index + 1;
1558
+ }
1559
+ this.#append(bytes.subarray(start));
1560
+ }
1561
+
1562
+ finish(): void {
1563
+ if (this.#pendingBytes !== 0 || this.#chunks.length !== 0) {
1564
+ throw new Error("WhatsApp projection session ended inside a frame");
1565
+ }
1566
+ }
1567
+ }
1568
+
1569
+ type MutableWhatsAppMessageExportSessionSpool = Readonly<{
1570
+ handle: FileHandle;
1571
+ capture: (
1572
+ stream: ReadableStream<Uint8Array>,
1573
+ maximumFrameBytes: number,
1574
+ onFrame: ((frame: WhatsAppMessageExportSessionCanonicalFrame) => void) | undefined,
1575
+ ) => Readonly<{ promise: Promise<void>; cancel: () => Promise<void> }>;
1576
+ publicSpool: WhatsAppMessageExportSessionSpool;
1577
+ }>;
1578
+
1579
+ function sameSpoolDirectory(left: BigIntStats, right: BigIntStats): boolean {
1580
+ return left.dev === right.dev
1581
+ && left.ino === right.ino
1582
+ && left.uid === right.uid
1583
+ && left.mode === right.mode
1584
+ && left.birthtimeNs === right.birthtimeNs;
1585
+ }
1586
+
1587
+ function assertPrivateSpoolDirectory(stats: BigIntStats): void {
1588
+ const uid = process.getuid?.();
1589
+ if (
1590
+ uid === undefined
1591
+ || !stats.isDirectory()
1592
+ || stats.isSymbolicLink()
1593
+ || stats.uid !== BigInt(uid)
1594
+ || (stats.mode & 0o777n) !== BigInt(MESSAGE_EXPORT_SESSION_PRIVATE_DIRECTORY_MODE)
1595
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1596
+ }
1597
+
1598
+ function assertPrivateSpoolFile(
1599
+ stats: BigIntStats,
1600
+ identity: Readonly<{ dev: bigint; ino: bigint }>,
1601
+ expectedBytes?: number,
1602
+ ): void {
1603
+ const uid = process.getuid?.();
1604
+ if (
1605
+ !stats.isFile()
1606
+ || stats.isSymbolicLink()
1607
+ || uid === undefined
1608
+ || stats.uid !== BigInt(uid)
1609
+ || (stats.mode & 0o777n) !== BigInt(MESSAGE_EXPORT_SESSION_PRIVATE_FILE_MODE)
1610
+ || stats.nlink !== 0n
1611
+ || stats.dev !== identity.dev
1612
+ || stats.ino !== identity.ino
1613
+ || (expectedBytes !== undefined && stats.size !== BigInt(expectedBytes))
1614
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1615
+ }
1616
+
1617
+ async function writeSpoolBytes(
1618
+ handle: FileHandle,
1619
+ bytes: Uint8Array,
1620
+ position: number,
1621
+ ): Promise<void> {
1622
+ let offset = 0;
1623
+ while (offset < bytes.byteLength) {
1624
+ const written = await handle.write(
1625
+ bytes,
1626
+ offset,
1627
+ bytes.byteLength - offset,
1628
+ position + offset,
1629
+ );
1630
+ if (written.bytesWritten < 1) {
1631
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1632
+ }
1633
+ offset += written.bytesWritten;
1634
+ }
1635
+ }
1636
+
1637
+ async function createWhatsAppMessageExportSessionSpool(
1638
+ beforeReadyForTest?: () => Promise<void>,
1639
+ ): Promise<
1640
+ MutableWhatsAppMessageExportSessionSpool
1641
+ > {
1642
+ let createdDirectory: string;
1643
+ try {
1644
+ createdDirectory = await mkdtemp(join(tmpdir(), "wrench-whatsapp-stdout-"));
1645
+ } catch {
1646
+ throw new Error("WhatsApp projection session private spool could not be created");
1647
+ }
1648
+ let directory: string;
1649
+ try {
1650
+ directory = await realpath(createdDirectory);
1651
+ } catch {
1652
+ // A created directory whose physical identity cannot be established cannot
1653
+ // be removed with proof, so retain the durable cleanup-unsafe boundary.
1654
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1655
+ }
1656
+ const path = join(directory, "stdout.ndjson");
1657
+ let handle: FileHandle | undefined;
1658
+ let directoryHandle: FileHandle | undefined;
1659
+ let cleanupDirectoryIdentity: BigIntStats | undefined;
1660
+ try {
1661
+ await chmod(directory, MESSAGE_EXPORT_SESSION_PRIVATE_DIRECTORY_MODE);
1662
+ const directoryFlags = fsConstants.O_RDONLY
1663
+ | fsConstants.O_NOFOLLOW
1664
+ | (typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0);
1665
+ directoryHandle = await open(directory, directoryFlags);
1666
+ const directoryIdentity = await directoryHandle.stat({ bigint: true });
1667
+ cleanupDirectoryIdentity = directoryIdentity;
1668
+ assertPrivateSpoolDirectory(directoryIdentity);
1669
+ const directoryPathIdentity = await lstat(directory, { bigint: true });
1670
+ assertPrivateSpoolDirectory(directoryPathIdentity);
1671
+ if (!sameSpoolDirectory(directoryIdentity, directoryPathIdentity)) {
1672
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1673
+ }
1674
+ const assertDirectoryBinding = async (): Promise<void> => {
1675
+ const [descriptor, current] = await Promise.all([
1676
+ directoryHandle!.stat({ bigint: true }),
1677
+ lstat(directory, { bigint: true }),
1678
+ ]);
1679
+ assertPrivateSpoolDirectory(descriptor);
1680
+ assertPrivateSpoolDirectory(current);
1681
+ if (
1682
+ !sameSpoolDirectory(directoryIdentity, descriptor)
1683
+ || !sameSpoolDirectory(directoryIdentity, current)
1684
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1685
+ };
1686
+ const uid = process.getuid?.();
1687
+ if (uid === undefined) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1688
+ handle = await open(
1689
+ path,
1690
+ fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
1691
+ MESSAGE_EXPORT_SESSION_PRIVATE_FILE_MODE,
1692
+ );
1693
+ await handle.chmod(MESSAGE_EXPORT_SESSION_PRIVATE_FILE_MODE);
1694
+ const linked = await handle.stat({ bigint: true });
1695
+ const entry = await lstat(path, { bigint: true });
1696
+ if (
1697
+ !linked.isFile()
1698
+ || linked.isSymbolicLink()
1699
+ || linked.nlink !== 1n
1700
+ || linked.dev !== entry.dev
1701
+ || linked.ino !== entry.ino
1702
+ || linked.uid !== BigInt(uid)
1703
+ || (linked.mode & 0o777n) !== BigInt(MESSAGE_EXPORT_SESSION_PRIVATE_FILE_MODE)
1704
+ ) throw new Error("WhatsApp projection session private spool file was invalid");
1705
+ const fileIdentity = Object.freeze({ dev: linked.dev, ino: linked.ino });
1706
+ await assertDirectoryBinding();
1707
+ await unlink(path);
1708
+ assertPrivateSpoolFile(await handle.stat({ bigint: true }), fileIdentity, 0);
1709
+ await assertDirectoryBinding();
1710
+
1711
+ let sealed: Readonly<{
1712
+ frameCount: number;
1713
+ totalBytes: number;
1714
+ stdoutSha256: string;
1715
+ maximumFrameBytes: number;
1716
+ metadata: BigIntStats;
1717
+ }> | undefined;
1718
+ let closed = false;
1719
+ let replayStarted = false;
1720
+ let replayActive = false;
1721
+
1722
+ const capture = (
1723
+ stream: ReadableStream<Uint8Array>,
1724
+ maximumFrameBytes: number,
1725
+ onFrame: ((frame: WhatsAppMessageExportSessionCanonicalFrame) => void) | undefined,
1726
+ ) => {
1727
+ const reader = stream.getReader();
1728
+ const digest = createHash("sha256");
1729
+ const decoder = new CanonicalSessionFrameDecoder(
1730
+ maximumFrameBytes,
1731
+ onFrame ?? (() => undefined),
1732
+ );
1733
+ let totalBytes = 0;
1734
+ const promise = (async (): Promise<void> => {
1735
+ try {
1736
+ for (;;) {
1737
+ const item = await reader.read();
1738
+ if (item.done) break;
1739
+ const nextTotal = totalBytes + item.value.byteLength;
1740
+ if (nextTotal > MESSAGE_EXPORT_SESSION_MAX_TOTAL_STDOUT_BYTES) {
1741
+ throw new Error("WhatsApp projection session output exceeded its total bound");
1742
+ }
1743
+ await writeSpoolBytes(handle!, item.value, totalBytes);
1744
+ digest.update(item.value);
1745
+ decoder.push(item.value);
1746
+ totalBytes = nextTotal;
1747
+ }
1748
+ decoder.finish();
1749
+ await assertDirectoryBinding();
1750
+ await handle!.sync();
1751
+ const metadata = await handle!.stat({ bigint: true });
1752
+ assertPrivateSpoolFile(metadata, fileIdentity, totalBytes);
1753
+ sealed = Object.freeze({
1754
+ frameCount: decoder.frameCount,
1755
+ totalBytes,
1756
+ stdoutSha256: digest.digest("hex"),
1757
+ maximumFrameBytes,
1758
+ metadata,
1759
+ });
1760
+ } finally {
1761
+ reader.releaseLock();
1762
+ }
1763
+ })().catch((error: unknown) => {
1764
+ if (
1765
+ containsWhatsAppContactProjectionCleanupUnverified(error)
1766
+ || errnoCode(error) !== undefined
1767
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1768
+ throw error;
1769
+ });
1770
+ return Object.freeze({
1771
+ promise,
1772
+ cancel: async () => {
1773
+ try { await reader.cancel(); } catch { /* settlement remains authoritative */ }
1774
+ },
1775
+ });
1776
+ };
1777
+
1778
+ const replay = async function* <Value>(
1779
+ project: (frame: WhatsAppMessageExportSessionCanonicalFrame) => Value,
1780
+ ): AsyncGenerator<Value> {
1781
+ if (closed || sealed === undefined || replayStarted || replayActive) {
1782
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1783
+ }
1784
+ replayStarted = true;
1785
+ replayActive = true;
1786
+ const expected = sealed;
1787
+ const digest = createHash("sha256");
1788
+ const decoded: WhatsAppMessageExportSessionCanonicalFrame[] = [];
1789
+ const decoder = new CanonicalSessionFrameDecoder(
1790
+ expected.maximumFrameBytes,
1791
+ (frame) => decoded.push(frame),
1792
+ );
1793
+ const buffer = Buffer.allocUnsafe(MESSAGE_EXPORT_SESSION_SPOOL_CHUNK_BYTES);
1794
+ let position = 0;
1795
+ try {
1796
+ await assertDirectoryBinding();
1797
+ assertPrivateSpoolFile(await handle!.stat({ bigint: true }), fileIdentity, expected.totalBytes);
1798
+ while (position < expected.totalBytes) {
1799
+ const length = Math.min(buffer.byteLength, expected.totalBytes - position);
1800
+ const item = await handle!.read(buffer, 0, length, position);
1801
+ if (item.bytesRead < 1) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1802
+ const bytes = buffer.subarray(0, item.bytesRead);
1803
+ digest.update(bytes);
1804
+ decoder.push(bytes);
1805
+ position += item.bytesRead;
1806
+ while (decoded.length > 0) yield project(decoded.shift()!);
1807
+ }
1808
+ decoder.finish();
1809
+ if (
1810
+ position !== expected.totalBytes
1811
+ || decoder.frameCount !== expected.frameCount
1812
+ || digest.digest("hex") !== expected.stdoutSha256
1813
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1814
+ await assertDirectoryBinding();
1815
+ const after = await handle!.stat({ bigint: true });
1816
+ assertPrivateSpoolFile(after, fileIdentity, expected.totalBytes);
1817
+ if (
1818
+ after.mtimeNs !== expected.metadata.mtimeNs
1819
+ || after.ctimeNs !== expected.metadata.ctimeNs
1820
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1821
+ } catch (error) {
1822
+ if (
1823
+ containsWhatsAppContactProjectionCleanupUnverified(error)
1824
+ || errnoCode(error) !== undefined
1825
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1826
+ throw error;
1827
+ } finally {
1828
+ replayActive = false;
1829
+ }
1830
+ };
1831
+
1832
+ const close = async (): Promise<void> => {
1833
+ if (closed) return;
1834
+ closed = true;
1835
+ let failure: unknown;
1836
+ try {
1837
+ await handle!.close();
1838
+ } catch (error) {
1839
+ failure = error;
1840
+ }
1841
+ try {
1842
+ const descriptor = await directoryHandle!.stat({ bigint: true });
1843
+ const current = await lstat(directory, { bigint: true });
1844
+ const entries = await readdir(directory);
1845
+ assertPrivateSpoolDirectory(descriptor);
1846
+ assertPrivateSpoolDirectory(current);
1847
+ if (
1848
+ !sameSpoolDirectory(directoryIdentity, descriptor)
1849
+ || !sameSpoolDirectory(directoryIdentity, current)
1850
+ || entries.length !== 0
1851
+ ) {
1852
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
1853
+ }
1854
+ await rmdir(directory);
1855
+ } catch (error) {
1856
+ failure = failure === undefined
1857
+ ? error
1858
+ : new AggregateError([failure, error], "WhatsApp projection spool cleanup failed");
1859
+ }
1860
+ try {
1861
+ await directoryHandle!.close();
1862
+ } catch (error) {
1863
+ failure = failure === undefined
1864
+ ? error
1865
+ : new AggregateError([failure, error], "WhatsApp projection spool cleanup failed");
1866
+ }
1867
+ if (failure !== undefined) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1868
+ };
1869
+
1870
+ const publicSpool: WhatsAppMessageExportSessionSpool = Object.freeze({
1871
+ get frameCount() { return sealed?.frameCount ?? 0; },
1872
+ get totalBytes() { return sealed?.totalBytes ?? 0; },
1873
+ get stdoutSha256() { return sealed?.stdoutSha256 ?? ""; },
1874
+ replay,
1875
+ close,
1876
+ });
1877
+ await beforeReadyForTest?.();
1878
+ return Object.freeze({ handle, capture, publicSpool });
1879
+ } catch (error) {
1880
+ let cleanupVerified = true;
1881
+ try { await handle?.close(); } catch { cleanupVerified = false; }
1882
+ let exactDirectory = false;
1883
+ if (directoryHandle !== undefined && cleanupDirectoryIdentity !== undefined) {
1884
+ try {
1885
+ const [descriptor, current] = await Promise.all([
1886
+ directoryHandle.stat({ bigint: true }),
1887
+ lstat(directory, { bigint: true }),
1888
+ ]);
1889
+ exactDirectory = sameSpoolDirectory(cleanupDirectoryIdentity, descriptor)
1890
+ && sameSpoolDirectory(cleanupDirectoryIdentity, current);
1891
+ } catch {
1892
+ exactDirectory = false;
1893
+ }
1894
+ }
1895
+ if (exactDirectory) {
1896
+ try {
1897
+ await unlink(path);
1898
+ } catch (unlinkError) {
1899
+ if (errnoCode(unlinkError) !== "ENOENT") cleanupVerified = false;
1900
+ }
1901
+ try {
1902
+ if ((await readdir(directory)).length !== 0) {
1903
+ cleanupVerified = false;
1904
+ } else {
1905
+ await rmdir(directory);
1906
+ }
1907
+ } catch {
1908
+ cleanupVerified = false;
1909
+ }
1910
+ } else {
1911
+ cleanupVerified = false;
1912
+ }
1913
+ try { await directoryHandle?.close(); } catch { cleanupVerified = false; }
1914
+ if (
1915
+ !cleanupVerified
1916
+ || containsWhatsAppContactProjectionCleanupUnverified(error)
1917
+ ) throw new WhatsAppContactProjectionCleanupUnverifiedError();
1918
+ throw new Error("WhatsApp projection session private spool could not be created");
1919
+ }
1920
+ }
1921
+
1922
+ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
1923
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
1924
+ (timer as { unref: () => void }).unref();
1925
+ }
1926
+ }
1927
+
1928
+ function errnoCode(error: unknown): string | undefined {
1929
+ return typeof error === "object" && error !== null && "code" in error
1930
+ ? String(error.code)
1931
+ : undefined;
1932
+ }
1933
+
1934
+ /**
1935
+ * Runs the one-process Message Like Me projection session. Its anonymous
1936
+ * parent-owned stdout spool is withheld until stdout EOF, bounded stderr,
1937
+ * exact child exit, and process-group absence all settle. Any unproved reap is
1938
+ * a durable cleanup-boundary failure.
1939
+ */
1940
+ export async function runWhatsAppMessageExportSessionHelperChild(
1941
+ invocation: WhatsAppContactProjectionHelperInvocation,
1942
+ ): Promise<WhatsAppMessageExportSessionHelperResult> {
1943
+ if (
1944
+ (invocation.processGroupIsAbsentForTest !== undefined
1945
+ || invocation.onProcessGroupPollForTest !== undefined
1946
+ || invocation.beforeSpoolReadyForTest !== undefined)
1947
+ && process.env.NODE_ENV !== "test"
1948
+ ) throw new Error("WhatsApp projection process-group injection is test-only");
1949
+ const runnerDeadline = new OperationDeadline(invocation.timeoutMs, {
1950
+ ...(invocation.signal === undefined ? {} : { signal: invocation.signal }),
1951
+ });
1952
+ const deadlineFailure = (): Error => new Error(
1953
+ invocation.signal?.aborted === true
1954
+ ? "WhatsApp projection session helper was cancelled"
1955
+ : "WhatsApp projection session helper timed out",
1956
+ );
1957
+ const assertRunnerAvailable = (): void => {
1958
+ try {
1959
+ runnerDeadline.throwIfUnavailable("WhatsApp projection session helper");
1960
+ } catch {
1961
+ throw deadlineFailure();
1962
+ }
1963
+ };
1964
+ try {
1965
+ assertRunnerAvailable();
1966
+ } catch (error) {
1967
+ runnerDeadline.dispose();
1968
+ throw error;
1969
+ }
1970
+ let spool: MutableWhatsAppMessageExportSessionSpool;
1971
+ try {
1972
+ spool = await createWhatsAppMessageExportSessionSpool(
1973
+ invocation.beforeSpoolReadyForTest,
1974
+ );
1975
+ } catch (error) {
1976
+ runnerDeadline.dispose();
1977
+ throw error;
1978
+ }
1979
+ try {
1980
+ assertRunnerAvailable();
1981
+ } catch (error) {
1982
+ try {
1983
+ await spool.publicSpool.close();
1984
+ } catch (cleanupError) {
1985
+ runnerDeadline.dispose();
1986
+ throw new AggregateError(
1987
+ [error, cleanupError],
1988
+ "WhatsApp projection session deadline and spool cleanup both failed",
1989
+ );
1990
+ }
1991
+ runnerDeadline.dispose();
1992
+ throw error;
1993
+ }
1994
+ let child: Bun.Subprocess<"pipe", "pipe", "pipe">;
1995
+ try {
1996
+ child = Bun.spawn([...invocation.command], {
1997
+ cwd: invocation.cwd,
1998
+ env: { ...invocation.environment },
1999
+ stdin: "pipe",
2000
+ stdout: "pipe",
2001
+ stderr: "pipe",
2002
+ detached: process.platform !== "win32",
2003
+ });
2004
+ } catch {
2005
+ const launchUncertainty = new WhatsAppContactProjectionCleanupUnverifiedError();
2006
+ try {
2007
+ await spool.publicSpool.close();
2008
+ } catch (cleanupError) {
2009
+ runnerDeadline.dispose();
2010
+ throw new AggregateError(
2011
+ [launchUncertainty, cleanupError],
2012
+ "WhatsApp projection session spawn and spool cleanup both failed",
2013
+ );
2014
+ }
2015
+ runnerDeadline.dispose();
2016
+ throw launchUncertainty;
2017
+ }
2018
+
2019
+ let childExited = false;
2020
+ const signalChild = (signal: "SIGTERM" | "SIGKILL"): void => {
2021
+ if (process.platform !== "win32") {
2022
+ try {
2023
+ process.kill(-child.pid, signal);
2024
+ return;
2025
+ } catch (error) {
2026
+ if (errnoCode(error) === "ESRCH") return;
2027
+ // Fall through to the exact child handle when group signalling is not
2028
+ // available; child.exited and the post-join group probe remain proof.
2029
+ }
2030
+ }
2031
+ try { child.kill(signal); } catch { /* exact child.exited remains authoritative */ }
2032
+ };
2033
+ let terminationStarted = false;
2034
+ let forceKill: ReturnType<typeof setTimeout> | undefined;
2035
+ let reapTimer: ReturnType<typeof setTimeout> | undefined;
2036
+ let rejectCleanupDeadline: ((error: unknown) => void) | undefined;
2037
+ let cleanupDeadlineExpired = false;
2038
+ const cleanupDeadline = new Promise<never>((_resolve, reject) => {
2039
+ rejectCleanupDeadline = reject;
2040
+ });
2041
+ const terminate = (): void => {
2042
+ if (!terminationStarted) {
2043
+ terminationStarted = true;
2044
+ signalChild("SIGTERM");
2045
+ forceKill = setTimeout(() => {
2046
+ signalChild("SIGKILL");
2047
+ }, CONTACT_PROJECTION_FORCE_KILL_DELAY_MS);
2048
+ reapTimer = setTimeout(
2049
+ () => {
2050
+ cleanupDeadlineExpired = true;
2051
+ rejectCleanupDeadline?.(new WhatsAppContactProjectionCleanupUnverifiedError());
2052
+ },
2053
+ CONTACT_PROJECTION_FORCE_KILL_DELAY_MS * 2,
2054
+ );
2055
+ unrefTimer(forceKill);
2056
+ unrefTimer(reapTimer);
2057
+ }
2058
+ };
2059
+ let timedOut = false;
2060
+ let cancelled = false;
2061
+ const onAbort = (): void => {
2062
+ cancelled = invocation.signal?.aborted === true;
2063
+ timedOut = !cancelled;
2064
+ terminate();
2065
+ };
2066
+ runnerDeadline.signal.addEventListener("abort", onAbort, { once: true });
2067
+ if (runnerDeadline.signal.aborted) onAbort();
2068
+ const guarded = <T>(promise: Promise<T>): Promise<T> => promise.catch((error: unknown) => {
2069
+ terminate();
2070
+ throw error;
2071
+ });
2072
+ const capture = spool.capture(
2073
+ child.stdout,
2074
+ invocation.maxOutputBytes,
2075
+ invocation.onCanonicalFrame,
2076
+ );
2077
+ const frames = guarded(capture.promise);
2078
+ const stderrRead = readBoundedStreamControlled(child.stderr, invocation.maxStderrBytes);
2079
+ const stderr = guarded(stderrRead.promise);
2080
+ const exited = child.exited.then(
2081
+ (exitCode) => {
2082
+ childExited = true;
2083
+ return exitCode;
2084
+ },
2085
+ (error: unknown) => {
2086
+ terminate();
2087
+ throw error;
2088
+ },
2089
+ );
2090
+ let callbackFailure: unknown;
2091
+ try {
2092
+ invocation.onSpawned?.(child.pid);
2093
+ } catch (error) {
2094
+ callbackFailure = error;
2095
+ terminate();
2096
+ }
2097
+ const stdin = callbackFailure === undefined
2098
+ ? guarded((async () => {
2099
+ await child.stdin.write(invocation.stdin);
2100
+ await child.stdin.end();
2101
+ })())
2102
+ : guarded((async () => {
2103
+ await child.stdin.end();
2104
+ })());
2105
+
2106
+ const processGroupIsAbsent = (): boolean => {
2107
+ if (invocation.processGroupIsAbsentForTest !== undefined) {
2108
+ return invocation.processGroupIsAbsentForTest();
2109
+ }
2110
+ if (process.platform === "win32") return true;
2111
+ try {
2112
+ process.kill(-child.pid, 0);
2113
+ return false;
2114
+ } catch (error) {
2115
+ if (errnoCode(error) === "ESRCH") return true;
2116
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
2117
+ }
2118
+ };
2119
+ let unexpectedDescendantObserved = false;
2120
+ let groupPollTimer: ReturnType<typeof setTimeout> | undefined;
2121
+ let resolveGroupPoll: (() => void) | undefined;
2122
+ const cancelGroupPoll = (): void => {
2123
+ if (groupPollTimer !== undefined) clearTimeout(groupPollTimer);
2124
+ groupPollTimer = undefined;
2125
+ const resolve = resolveGroupPoll;
2126
+ resolveGroupPoll = undefined;
2127
+ resolve?.();
2128
+ };
2129
+ const waitForGroupPoll = (): Promise<void> => new Promise<void>((resolve) => {
2130
+ invocation.onProcessGroupPollForTest?.();
2131
+ resolveGroupPoll = resolve;
2132
+ groupPollTimer = setTimeout(() => {
2133
+ groupPollTimer = undefined;
2134
+ resolveGroupPoll = undefined;
2135
+ resolve();
2136
+ }, 20);
2137
+ unrefTimer(groupPollTimer);
2138
+ });
2139
+ const joinProcessGroup = async (): Promise<void> => {
2140
+ if (processGroupIsAbsent()) return;
2141
+ unexpectedDescendantObserved = true;
2142
+ terminate();
2143
+ for (;;) {
2144
+ if (cleanupDeadlineExpired) {
2145
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
2146
+ }
2147
+ if (processGroupIsAbsent()) return;
2148
+ await waitForGroupPoll();
2149
+ }
2150
+ };
2151
+ let transferred = false;
2152
+ try {
2153
+ const settled = await Promise.race([
2154
+ Promise.allSettled([stdin, frames, stderr, exited]),
2155
+ cleanupDeadline,
2156
+ ]);
2157
+ const [stdinResult, framesResult, stderrResult, exitResult] = settled;
2158
+ await Promise.race([joinProcessGroup(), cleanupDeadline]);
2159
+ if (exitResult.status === "rejected") {
2160
+ throw new WhatsAppContactProjectionCleanupUnverifiedError();
2161
+ }
2162
+ if (unexpectedDescendantObserved) {
2163
+ throw new Error("WhatsApp projection session helper left an unexpected descendant");
2164
+ }
2165
+ if (callbackFailure !== undefined) throw callbackFailure;
2166
+ if (cancelled) throw new Error("WhatsApp projection session helper was cancelled");
2167
+ if (timedOut) throw new Error("WhatsApp projection session helper timed out");
2168
+ if (
2169
+ stdinResult.status === "rejected"
2170
+ || framesResult.status === "rejected"
2171
+ || stderrResult.status === "rejected"
2172
+ ) {
2173
+ const failure = framesResult.status === "rejected"
2174
+ ? framesResult.reason
2175
+ : stderrResult.status === "rejected"
2176
+ ? stderrResult.reason
2177
+ : stdinResult.status === "rejected"
2178
+ ? stdinResult.reason
2179
+ : undefined;
2180
+ throw failure instanceof Error
2181
+ ? failure
2182
+ : new Error("WhatsApp projection session stream failed within its bound");
2183
+ }
2184
+ transferred = true;
2185
+ return Object.freeze({
2186
+ exitCode: exitResult.value,
2187
+ spool: spool.publicSpool,
2188
+ stderr: stderrResult.value,
2189
+ });
2190
+ } catch (error) {
2191
+ const detach = (): void => {
2192
+ void capture.cancel();
2193
+ void stderrRead.cancel();
2194
+ void Promise.resolve(child.stdin.end()).catch(() => undefined);
2195
+ if (!childExited) child.unref();
2196
+ };
2197
+ detach();
2198
+ if (cleanupDeadlineExpired) {
2199
+ void spool.publicSpool.close().catch(() => undefined);
2200
+ throw error;
2201
+ }
2202
+ await Promise.allSettled([
2203
+ capture.promise,
2204
+ stderrRead.promise,
2205
+ child.stdin.end(),
2206
+ ]);
2207
+ try {
2208
+ await spool.publicSpool.close();
2209
+ } catch (cleanupError) {
2210
+ throw new AggregateError(
2211
+ [error, cleanupError],
2212
+ "WhatsApp projection session operation and spool cleanup both failed",
2213
+ );
2214
+ }
2215
+ throw error;
2216
+ } finally {
2217
+ cancelGroupPoll();
2218
+ if (reapTimer !== undefined) clearTimeout(reapTimer);
2219
+ if (forceKill !== undefined) clearTimeout(forceKill);
2220
+ runnerDeadline.signal.removeEventListener("abort", onAbort);
2221
+ runnerDeadline.dispose();
2222
+ if (!transferred && !childExited) child.unref();
2223
+ }
2224
+ }
2225
+
2226
+ /**
2227
+ * Ordinary projection and schema errors have no live-resource consequence.
2228
+ * Only the dedicated process-cleanup uncertainty crosses the cleanup barrier.
2229
+ */
2230
+ export function whatsappContactProjectionCleanupBarrier(
2231
+ operation: Promise<unknown>,
2232
+ ): Promise<void> {
2233
+ return operation.then(
2234
+ () => undefined,
2235
+ (error: unknown) => {
2236
+ if (containsWhatsAppContactProjectionCleanupUnverified(error)) {
2237
+ throw error;
2238
+ }
2239
+ },
2240
+ );
2241
+ }
2242
+
2243
+ function unavailableWhatsAppDirectionStats() {
2244
+ return Object.freeze({
2245
+ count: null,
2246
+ complete: false,
2247
+ lowerBound: false,
2248
+ truncated: false,
2249
+ lastAt: null,
2250
+ lastAtComplete: false,
2251
+ lastAtBasis: "unavailable" as const,
2252
+ incompleteReasons: Object.freeze([
2253
+ "whatsapp-message-store-account-owner-unavailable",
2254
+ ]),
2255
+ });
2256
+ }
2257
+
2258
+ async function projectWhatsAppLocalContacts(
2259
+ auth: WhatsAppAuth,
2260
+ recipe: WebSessionRecipe,
2261
+ input: OperationInput,
2262
+ dependencies: WhatsAppWebRuntimeDependencies | undefined,
2263
+ operationDeadline: WebSessionOperationDeadline | undefined,
2264
+ registerCleanupBarrier: WebSessionCleanupBarrierRegistrar | undefined,
2265
+ ): Promise<WebSessionExecution> {
2266
+ const parsedInput = exactContactInput(input);
2267
+ if (parsedInput.collection !== "contacts") {
2268
+ throw new Error("WhatsApp contact projection requires collection=contacts");
2269
+ }
2270
+ if (auth.subject === undefined) {
2271
+ throw new Error(
2272
+ "WhatsApp linked-device auth must be bound to its current account before private reads",
2273
+ );
2274
+ }
2275
+ let accountSubject: string;
2276
+ try {
2277
+ accountSubject = parseWhatsAppContactProjectionSubject(
2278
+ auth.subject,
2279
+ ).subject;
2280
+ } catch {
2281
+ throw new Error(
2282
+ "WhatsApp linked-device auth subject is not a PN or LID account",
2283
+ );
2284
+ }
2285
+ const rawOperation = startWebSessionCleanupTrackedOperation(
2286
+ registerCleanupBarrier,
2287
+ async () => {
2288
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
2289
+ const store = await validateWhatsAppStoreDirectory(
2290
+ auth.path,
2291
+ "contact-projection",
2292
+ );
2293
+ const initial = await captureContactProjectionParentIdentity(store);
2294
+ const fixed = await resolveFixedContactProjectionFiles();
2295
+ const request = Object.freeze({
2296
+ schemaVersion: WHATSAPP_CONTACT_PROJECTION_PROTOCOL_VERSION,
2297
+ operation: "contacts.list",
2298
+ accountSubject,
2299
+ cursor: parsedInput.cursor,
2300
+ limit: parsedInput.limit,
2301
+ storeIdentity: Object.freeze({
2302
+ dev: initial.store.dev.toString(),
2303
+ ino: initial.store.ino.toString(),
2304
+ }),
2305
+ sessionIdentity: Object.freeze({
2306
+ dev: initial.session.dev.toString(),
2307
+ ino: initial.session.ino.toString(),
2308
+ }),
2309
+ }) satisfies WhatsAppContactProjectionRequest;
2310
+ const timeoutMs = remainingTimeoutMs(
2311
+ recipe.timeoutMs,
2312
+ operationDeadline,
2313
+ );
2314
+ const invocation = Object.freeze({
2315
+ command: Object.freeze([
2316
+ process.execPath,
2317
+ "--no-env-file",
2318
+ "--no-install",
2319
+ "--no-macros",
2320
+ "--no-addons",
2321
+ `--config=${fixed.config}`,
2322
+ fixed.helper,
2323
+ ]),
2324
+ cwd: store,
2325
+ environment: contactProjectionEnvironment(),
2326
+ stdin: `${JSON.stringify(request)}\n`,
2327
+ timeoutMs,
2328
+ maxOutputBytes: Math.min(
2329
+ recipe.maxOutputBytes,
2330
+ WHATSAPP_CONTACT_PROJECTION_MAX_STDOUT_BYTES,
2331
+ ),
2332
+ maxStderrBytes: MAX_CONTACT_PROJECTION_STDERR_BYTES,
2333
+ ...(operationDeadline === undefined
2334
+ ? {}
2335
+ : { signal: operationDeadline.signal }),
2336
+ }) satisfies WhatsAppContactProjectionHelperInvocation;
2337
+ const run = dependencies?.runContactProjectionHelper
2338
+ ?? runWhatsAppContactProjectionHelperChild;
2339
+ let childResult: WhatsAppContactProjectionHelperResult | undefined;
2340
+ let helperFailure: unknown;
2341
+ try {
2342
+ childResult = await run(invocation);
2343
+ } catch (error) {
2344
+ helperFailure = error;
2345
+ }
2346
+ let identityFailure: unknown;
2347
+ try {
2348
+ await revalidateContactProjectionParentIdentity(store, initial);
2349
+ } catch (error) {
2350
+ identityFailure = error;
2351
+ }
2352
+ if (
2353
+ helperFailure
2354
+ instanceof WhatsAppContactProjectionCleanupUnverifiedError
2355
+ ) {
2356
+ throw helperFailure;
2357
+ }
2358
+ if (identityFailure !== undefined) {
2359
+ throw identityFailure instanceof Error
2360
+ ? identityFailure
2361
+ : new Error(
2362
+ "WhatsApp contact projection parent binding became unverifiable",
2363
+ );
2364
+ }
2365
+ if (helperFailure !== undefined) {
2366
+ throw helperFailure instanceof Error
2367
+ ? helperFailure
2368
+ : new Error("WhatsApp contact projection helper failed");
2369
+ }
2370
+ if (childResult === undefined) {
2371
+ throw new Error("WhatsApp contact projection helper omitted its result");
2372
+ }
2373
+ if (childResult.exitCode !== 0 || childResult.stderr.length !== 0) {
2374
+ throw new Error(
2375
+ "WhatsApp contact projection helper failed before reviewed output",
2376
+ );
2377
+ }
2378
+ let parsed: unknown;
2379
+ try {
2380
+ parsed = JSON.parse(childResult.stdout.trim()) as unknown;
2381
+ } catch {
2382
+ throw new Error(
2383
+ "WhatsApp contact projection helper returned malformed output",
2384
+ );
2385
+ }
2386
+ let response: ReturnType<typeof parseWhatsAppContactProjectionResponse>;
2387
+ try {
2388
+ response = parseWhatsAppContactProjectionResponse(parsed, request);
2389
+ } catch {
2390
+ throw new Error(
2391
+ "WhatsApp contact projection helper returned unsupported output",
2392
+ );
2393
+ }
2394
+ if (response.status === "failed") {
2395
+ throw new Error(
2396
+ `WhatsApp contact projection helper rejected the local store (${response.errorCode})`,
2397
+ );
2398
+ }
2399
+ return response;
2400
+ },
2401
+ whatsappContactProjectionCleanupBarrier,
2402
+ );
2403
+ const response = operationDeadline === undefined
2404
+ ? await rawOperation
2405
+ : await operationDeadline.run(
2406
+ () => rawOperation,
2407
+ WEB_SESSION_OPERATION_LABEL,
2408
+ );
2409
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
2410
+
2411
+ const directionStats = unavailableWhatsAppDirectionStats();
2412
+ const projectedStats = projectContactDirectionStats(
2413
+ directionStats,
2414
+ directionStats,
2415
+ );
2416
+ const output = Object.freeze({
2417
+ provider: "whatsapp",
2418
+ operation: "contacts.list",
2419
+ accountSubject,
2420
+ projection: "quiescent-account-bound-session-store",
2421
+ contacts: Object.freeze(response.contacts.map((contact) => Object.freeze({
2422
+ ...contact,
2423
+ alias: null,
2424
+ tags: Object.freeze([]),
2425
+ updatedAt: null,
2426
+ localProjectionStatsComplete: false,
2427
+ ...projectedStats,
2428
+ }))),
2429
+ nextCursor: response.nextCursor,
2430
+ localContactTablePageComplete: response.localContactTablePageComplete,
2431
+ remoteContactSetComplete: false,
2432
+ contactSetIncompleteReasons: Object.freeze([
2433
+ "linked-device-contact-sync-coverage-unknown",
2434
+ ]),
2435
+ statsScope: "unavailable",
2436
+ statsCompleteness: "unavailable",
2437
+ });
2438
+ return {
2439
+ status: "succeeded",
2440
+ output: outputWithinBound(output, recipe.maxOutputBytes),
2441
+ finalUrl: WHATSAPP_ORIGIN,
2442
+ dispatchStarted: false,
2443
+ dispatch: { planned: 0, started: 0, verified: 0 },
2444
+ };
2445
+ }
2446
+
2447
+ async function projectWhatsAppLocalInteractions(
2448
+ auth: WhatsAppAuth,
2449
+ recipe: WebSessionRecipe,
2450
+ parsedInput: Extract<ExactContactInput, { readonly collection: "interactions" }>,
2451
+ dependencies: WhatsAppWebRuntimeDependencies | undefined,
2452
+ operationDeadline: WebSessionOperationDeadline | undefined,
2453
+ registerCleanupBarrier: WebSessionCleanupBarrierRegistrar | undefined,
2454
+ ): Promise<WebSessionExecution> {
2455
+ if (auth.subject === undefined) {
2456
+ throw new Error(
2457
+ "WhatsApp linked-device auth must be bound to its current account before private reads",
2458
+ );
2459
+ }
2460
+ const accountSubject = auth.subject;
2461
+ const rawOperation = startWebSessionCleanupTrackedOperation(
2462
+ registerCleanupBarrier,
2463
+ async () => {
2464
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
2465
+ const store = await validateWhatsAppStoreDirectory(auth.path, "projection");
2466
+ const initial = await captureInteractionProjectionParentIdentity(store);
2467
+ const fixed = await resolveFixedContactProjectionFiles(
2468
+ "whatsapp-interaction-projection-helper.ts",
2469
+ );
2470
+ const request = parseWhatsAppInteractionProjectionRequest({
2471
+ schemaVersion: WHATSAPP_INTERACTION_PROJECTION_PROTOCOL_VERSION,
2472
+ operation: "contacts.interactions.list",
2473
+ accountSubject,
2474
+ cursor: parsedInput.cursor,
2475
+ cursorAnchor: parsedInput.cursorAnchor,
2476
+ limit: parsedInput.limit,
2477
+ storeIdentity: {
2478
+ dev: initial.store.dev.toString(),
2479
+ ino: initial.store.ino.toString(),
2480
+ },
2481
+ sessionIdentity: {
2482
+ dev: initial.session.dev.toString(),
2483
+ ino: initial.session.ino.toString(),
2484
+ },
2485
+ messageStoreIdentity: {
2486
+ dev: initial.messageStore.dev.toString(),
2487
+ ino: initial.messageStore.ino.toString(),
2488
+ },
2489
+ }) satisfies WhatsAppInteractionProjectionRequest;
2490
+ const timeoutMs = remainingTimeoutMs(recipe.timeoutMs, operationDeadline);
2491
+ const invocation = Object.freeze({
2492
+ command: Object.freeze([
2493
+ process.execPath,
2494
+ "--no-env-file",
2495
+ "--no-install",
2496
+ "--no-macros",
2497
+ "--no-addons",
2498
+ `--config=${fixed.config}`,
2499
+ fixed.helper,
2500
+ ]),
2501
+ cwd: store,
2502
+ environment: contactProjectionEnvironment(),
2503
+ stdin: `${JSON.stringify(request)}\n`,
2504
+ timeoutMs,
2505
+ maxOutputBytes: Math.min(
2506
+ recipe.maxOutputBytes,
2507
+ WHATSAPP_INTERACTION_PROJECTION_MAX_STDOUT_BYTES,
2508
+ ),
2509
+ maxStderrBytes: MAX_CONTACT_PROJECTION_STDERR_BYTES,
2510
+ ...(operationDeadline === undefined ? {} : { signal: operationDeadline.signal }),
2511
+ }) satisfies WhatsAppContactProjectionHelperInvocation;
2512
+ const run = dependencies?.runInteractionProjectionHelper
2513
+ ?? runWhatsAppContactProjectionHelperChild;
2514
+ let childResult: WhatsAppContactProjectionHelperResult | undefined;
2515
+ let helperFailure: unknown;
2516
+ try {
2517
+ childResult = await run(invocation);
2518
+ } catch (error) {
2519
+ helperFailure = error;
2520
+ }
2521
+ let identityFailure: unknown;
2522
+ try {
2523
+ await revalidateInteractionProjectionParentIdentity(store, initial);
2524
+ } catch (error) {
2525
+ identityFailure = error;
2526
+ }
2527
+ if (helperFailure instanceof WhatsAppContactProjectionCleanupUnverifiedError) {
2528
+ throw helperFailure;
2529
+ }
2530
+ if (identityFailure !== undefined) {
2531
+ throw identityFailure instanceof Error
2532
+ ? identityFailure
2533
+ : new Error("WhatsApp interaction projection parent binding became unverifiable");
2534
+ }
2535
+ if (helperFailure !== undefined) {
2536
+ throw helperFailure instanceof Error
2537
+ ? helperFailure
2538
+ : new Error("WhatsApp interaction projection helper failed");
2539
+ }
2540
+ if (
2541
+ childResult === undefined
2542
+ || childResult.exitCode !== 0
2543
+ || childResult.stderr.length !== 0
2544
+ ) {
2545
+ throw new Error(
2546
+ "WhatsApp interaction projection helper failed before reviewed output",
2547
+ );
2548
+ }
2549
+ let parsed: unknown;
2550
+ try {
2551
+ parsed = JSON.parse(childResult.stdout.trim()) as unknown;
2552
+ } catch {
2553
+ throw new Error("WhatsApp interaction projection helper returned malformed output");
2554
+ }
2555
+ let response: ReturnType<typeof parseWhatsAppInteractionProjectionResponse>;
2556
+ try {
2557
+ response = parseWhatsAppInteractionProjectionResponse(parsed, request);
2558
+ } catch {
2559
+ throw new Error("WhatsApp interaction projection helper returned unsupported output");
2560
+ }
2561
+ if (response.status === "failed") {
2562
+ throw new Error(
2563
+ `WhatsApp interaction projection helper rejected the local store (${response.errorCode})`,
2564
+ );
2565
+ }
2566
+ return response;
2567
+ },
2568
+ whatsappContactProjectionCleanupBarrier,
2569
+ );
2570
+ const response = operationDeadline === undefined
2571
+ ? await rawOperation
2572
+ : await operationDeadline.run(() => rawOperation, WEB_SESSION_OPERATION_LABEL);
2573
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
2574
+ const output = Object.freeze({
2575
+ provider: "whatsapp",
2576
+ operation: "contacts.list",
2577
+ accountSubject,
2578
+ contactCollection: "interactions",
2579
+ projection: "quiescent-account-bound-local-message-inserts",
2580
+ projectionGeneration: response.projectionGeneration,
2581
+ interactions: response.interactions,
2582
+ nextCursor: response.nextCursor,
2583
+ localInsertPageComplete: response.localInsertPageComplete,
2584
+ checkpoint: response.checkpoint,
2585
+ remoteHistoryComplete: false,
2586
+ incompleteReasons: Object.freeze([
2587
+ "linked-device-history-coverage-unknown",
2588
+ "rowid-cursor-discovers-inserts-only",
2589
+ ]),
2590
+ });
2591
+ return {
2592
+ status: "succeeded",
2593
+ output: outputWithinBound(output, recipe.maxOutputBytes),
2594
+ finalUrl: WHATSAPP_ORIGIN,
2595
+ dispatchStarted: false,
2596
+ dispatch: { planned: 0, started: 0, verified: 0 },
2597
+ };
2598
+ }
2599
+ export type WhatsAppReadPlan =
2600
+ | {
2601
+ readonly action: "messaging.list";
2602
+ readonly command: readonly string[];
2603
+ readonly limit: number;
2604
+ readonly folder: "all" | "active" | "archived" | "unread";
2605
+ }
2606
+ | {
2607
+ readonly action: "messaging.read";
2608
+ readonly command: readonly string[];
2609
+ readonly limit: number;
2610
+ readonly conversationJid: string;
2611
+ }
2612
+ | {
2613
+ readonly action: "media.read";
2614
+ readonly command: readonly string[];
2615
+ readonly conversationJid: string;
2616
+ readonly messageId: string;
2617
+ };
2618
+
2619
+ export function planWhatsAppReadCommand(
2620
+ action: "messaging.list",
2621
+ input: OperationInput,
2622
+ ): Extract<WhatsAppReadPlan, { readonly action: "messaging.list" }>;
2623
+ export function planWhatsAppReadCommand(
2624
+ action: "messaging.read",
2625
+ input: OperationInput,
2626
+ ): Extract<WhatsAppReadPlan, { readonly action: "messaging.read" }>;
2627
+ export function planWhatsAppReadCommand(
2628
+ action: "media.read",
2629
+ input: OperationInput,
2630
+ ): Extract<WhatsAppReadPlan, { readonly action: "media.read" }>;
2631
+ export function planWhatsAppReadCommand(
2632
+ action: WhatsAppReadPlan["action"],
2633
+ input: OperationInput,
2634
+ ): WhatsAppReadPlan;
2635
+ export function planWhatsAppReadCommand(
2636
+ action: WhatsAppReadPlan["action"],
2637
+ input: OperationInput,
2638
+ ): WhatsAppReadPlan {
2639
+ if (action === "messaging.list") {
2640
+ const limit = inputInteger(input, "limit", DEFAULT_LIMIT, 100);
2641
+ const folder = inputFolder(input);
2642
+ const flags = folder === "active"
2643
+ ? ["--no-archived"]
2644
+ : folder === "archived"
2645
+ ? ["--archived"]
2646
+ : folder === "unread"
2647
+ ? ["--unread"]
2648
+ : [];
2649
+ return Object.freeze({
2650
+ action,
2651
+ limit,
2652
+ folder,
2653
+ command: Object.freeze([
2654
+ "chats",
2655
+ "list",
2656
+ "--limit",
2657
+ String(limit),
2658
+ ...flags,
2659
+ ]),
2660
+ });
2661
+ }
2662
+ const conversationJid = whatsappTargetJid(
2663
+ input.conversation_jid,
2664
+ "input.conversation_jid",
2665
+ );
2666
+ if (action === "messaging.read") {
2667
+ const limit = inputInteger(input, "limit", DEFAULT_LIMIT, 200);
2668
+ return Object.freeze({
2669
+ action,
2670
+ limit,
2671
+ conversationJid,
2672
+ command: Object.freeze([
2673
+ "messages",
2674
+ "list",
2675
+ "--chat",
2676
+ conversationJid,
2677
+ "--limit",
2678
+ String(limit),
2679
+ ]),
2680
+ });
2681
+ }
2682
+ const messageId = whatsappMessageId(
2683
+ input.message_id,
2684
+ "input.message_id",
2685
+ );
2686
+ return Object.freeze({
2687
+ action,
2688
+ conversationJid,
2689
+ messageId,
2690
+ command: Object.freeze([
2691
+ "messages",
2692
+ "show",
2693
+ "--chat",
2694
+ conversationJid,
2695
+ "--id",
2696
+ messageId,
2697
+ ]),
2698
+ });
2699
+ }
2700
+
2701
+ function outputWithinBound(value: unknown, maximum: number): unknown {
2702
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > maximum) {
2703
+ throw new Error("WhatsApp projected output exceeded its reviewed byte limit");
2704
+ }
2705
+ return value;
2706
+ }
2707
+
2708
+ async function executeLocalProjection(
2709
+ runtime: Awaited<ReturnType<typeof boundRuntime>>,
2710
+ recipe: WebSessionRecipe,
2711
+ input: OperationInput,
2712
+ dependencies: WhatsAppWebRuntimeDependencies | undefined,
2713
+ operationDeadline?: WebSessionOperationDeadline,
2714
+ ): Promise<WebSessionExecution> {
2715
+ let output: unknown;
2716
+ if (recipe.action === "messaging.list") {
2717
+ const plan = planWhatsAppReadCommand(recipe.action, input);
2718
+ const timeoutMs = remainingTimeoutMs(
2719
+ recipe.timeoutMs,
2720
+ operationDeadline,
2721
+ );
2722
+ const raw = await checkedRun(
2723
+ runtime.binary,
2724
+ readOnlyArguments(runtime.store, timeoutMs, plan.command),
2725
+ {
2726
+ timeoutMs,
2727
+ maxOutputBytes: recipe.maxOutputBytes,
2728
+ readOnly: true,
2729
+ ...(dependencies === undefined ? {} : { dependencies }),
2730
+ ...(operationDeadline === undefined
2731
+ ? {}
2732
+ : { operationDeadline }),
2733
+ },
2734
+ );
2735
+ output = Object.freeze({
2736
+ accountSubject: runtime.subject,
2737
+ projection: "local-store",
2738
+ completeness: "bounded-current-local-projection",
2739
+ chats: projectWhatsAppChatsEnvelope(raw, plan.limit),
2740
+ });
2741
+ } else if (recipe.action === "messaging.read") {
2742
+ const plan = planWhatsAppReadCommand(recipe.action, input);
2743
+ const timeoutMs = remainingTimeoutMs(
2744
+ recipe.timeoutMs,
2745
+ operationDeadline,
2746
+ );
2747
+ const raw = await checkedRun(
2748
+ runtime.binary,
2749
+ readOnlyArguments(runtime.store, timeoutMs, plan.command),
2750
+ {
2751
+ timeoutMs,
2752
+ maxOutputBytes: recipe.maxOutputBytes,
2753
+ readOnly: true,
2754
+ ...(dependencies === undefined ? {} : { dependencies }),
2755
+ ...(operationDeadline === undefined
2756
+ ? {}
2757
+ : { operationDeadline }),
2758
+ },
2759
+ );
2760
+ output = Object.freeze({
2761
+ accountSubject: runtime.subject,
2762
+ projection: "local-store",
2763
+ completeness: "bounded-current-local-projection",
2764
+ conversationJid: plan.conversationJid,
2765
+ ...projectWhatsAppMessagesEnvelope(
2766
+ raw,
2767
+ plan.conversationJid,
2768
+ plan.limit,
2769
+ ),
2770
+ });
2771
+ } else if (recipe.action === "media.read") {
2772
+ const plan = planWhatsAppReadCommand(recipe.action, input);
2773
+ const timeoutMs = remainingTimeoutMs(
2774
+ recipe.timeoutMs,
2775
+ operationDeadline,
2776
+ );
2777
+ const raw = await checkedRun(
2778
+ runtime.binary,
2779
+ readOnlyArguments(runtime.store, timeoutMs, plan.command),
2780
+ {
2781
+ timeoutMs,
2782
+ maxOutputBytes: recipe.maxOutputBytes,
2783
+ readOnly: true,
2784
+ ...(dependencies === undefined ? {} : { dependencies }),
2785
+ ...(operationDeadline === undefined
2786
+ ? {}
2787
+ : { operationDeadline }),
2788
+ },
2789
+ );
2790
+ const message = projectWhatsAppMessageEnvelope(
2791
+ raw,
2792
+ plan.conversationJid,
2793
+ plan.messageId,
2794
+ );
2795
+ output = Object.freeze({
2796
+ accountSubject: runtime.subject,
2797
+ projection: "local-store",
2798
+ completeness: "one-local-message",
2799
+ conversationJid: plan.conversationJid,
2800
+ messageId: plan.messageId,
2801
+ media: message.media,
2802
+ });
2803
+ } else {
2804
+ throw new Error("WhatsApp operation has no local projection");
2805
+ }
2806
+ return {
2807
+ status: "succeeded",
2808
+ output: outputWithinBound(output, recipe.maxOutputBytes),
2809
+ finalUrl: WHATSAPP_ORIGIN,
2810
+ dispatchStarted: false,
2811
+ dispatch: { planned: 0, started: 0, verified: 0 },
2812
+ };
2813
+ }
2814
+
2815
+ export async function executeWhatsAppWebOperation(
2816
+ recipe: WebSessionRecipe,
2817
+ input: OperationInput,
2818
+ auth: GhostgetAuth,
2819
+ options: {
2820
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
2821
+ readonly environment?: Readonly<Record<string, string | undefined>>;
2822
+ readonly operationDeadline?: WebSessionOperationDeadline;
2823
+ readonly registerCleanupBarrier?: WebSessionCleanupBarrierRegistrar;
2824
+ } = {},
2825
+ ): Promise<WebSessionExecution> {
2826
+ if (
2827
+ recipe.site !== "whatsapp"
2828
+ || (recipe.action === "contacts.list"
2829
+ ? recipe.contractVersion !== 2
2830
+ : recipe.contractVersion !== 1)
2831
+ || !isWhatsAppOperation(recipe.action)
2832
+ ) throw new Error("WhatsApp linked-device recipe is not installed");
2833
+ const contract = WHATSAPP_WEB_OPERATIONS[recipe.action];
2834
+ if (contract.state !== "observed") {
2835
+ throw new Error(
2836
+ `WhatsApp linked-device operation ${recipe.action} is capture-required: ${contract.reason}`,
2837
+ );
2838
+ }
2839
+ const localProjection = recipe.action === "contacts.list"
2840
+ || recipe.action === "messaging.list"
2841
+ || recipe.action === "messaging.read"
2842
+ || recipe.action === "media.read";
2843
+ if (!localProjection) {
2844
+ throw new Error(
2845
+ `WhatsApp linked-device operation ${recipe.action} has no executable local projection`,
2846
+ );
2847
+ }
2848
+ options.operationDeadline?.throwIfUnavailable(
2849
+ WEB_SESSION_OPERATION_LABEL,
2850
+ );
2851
+ if (recipe.action === "contacts.list") {
2852
+ const parsedInput = exactContactInput(input);
2853
+ if (parsedInput.collection === "interactions") {
2854
+ return projectWhatsAppLocalInteractions(
2855
+ requireWhatsAppAuth(auth),
2856
+ recipe,
2857
+ parsedInput,
2858
+ options.dependencies,
2859
+ options.operationDeadline,
2860
+ options.registerCleanupBarrier,
2861
+ );
2862
+ }
2863
+ return projectWhatsAppLocalContacts(
2864
+ requireWhatsAppAuth(auth),
2865
+ recipe,
2866
+ input,
2867
+ options.dependencies,
2868
+ options.operationDeadline,
2869
+ options.registerCleanupBarrier,
2870
+ );
2871
+ }
2872
+ const runtime = await boundRuntime(
2873
+ auth,
2874
+ "projection",
2875
+ recipe.timeoutMs,
2876
+ options.dependencies,
2877
+ options.environment ?? process.env,
2878
+ options.operationDeadline,
2879
+ );
2880
+ if (localProjection) {
2881
+ return executeLocalProjection(
2882
+ runtime,
2883
+ recipe,
2884
+ input,
2885
+ options.dependencies,
2886
+ options.operationDeadline,
2887
+ );
2888
+ }
2889
+ throw new Error("WhatsApp local projection classification changed during execution");
2890
+ }
2891
+
2892
+ export type WhatsAppPairingPlan = {
2893
+ readonly binary: string;
2894
+ readonly store: string;
2895
+ readonly arguments: readonly string[];
2896
+ readonly environment: Readonly<Record<string, string>>;
2897
+ };
2898
+
2899
+ export async function planWhatsAppPairing(
2900
+ auth: GhostgetAuth,
2901
+ options: {
2902
+ readonly phone?: string;
2903
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
2904
+ readonly environment?: Readonly<Record<string, string | undefined>>;
2905
+ } = {},
2906
+ ): Promise<WhatsAppPairingPlan> {
2907
+ const linked = requireWhatsAppAuth(auth);
2908
+ const store = await validateWhatsAppStoreDirectory(linked.path, "pair");
2909
+ const binary = await runtimeBinary(
2910
+ options.dependencies,
2911
+ options.environment ?? process.env,
2912
+ );
2913
+ const phone = options.phone;
2914
+ if (
2915
+ phone !== undefined
2916
+ && !/^\+?[0-9]{5,20}$/u.test(phone)
2917
+ ) throw new Error("WhatsApp pairing phone must be one international number");
2918
+ return Object.freeze({
2919
+ binary,
2920
+ store,
2921
+ arguments: Object.freeze([
2922
+ "--store",
2923
+ store,
2924
+ "--timeout",
2925
+ "10m",
2926
+ "auth",
2927
+ "--idle-exit",
2928
+ "30s",
2929
+ "--qr-format",
2930
+ "terminal",
2931
+ ...(phone === undefined ? [] : ["--phone", phone]),
2932
+ ]),
2933
+ environment: Object.freeze({
2934
+ ...wacliEnvironment(false),
2935
+ WACLI_SYNC_MAX_MESSAGES: String(MAX_SYNC_MESSAGES),
2936
+ WACLI_SYNC_MAX_DB_SIZE: MAX_SYNC_DB_SIZE,
2937
+ }),
2938
+ });
2939
+ }
2940
+
2941
+ async function runInteractivePairing(
2942
+ plan: WhatsAppPairingPlan,
2943
+ ): Promise<number> {
2944
+ const child = Bun.spawn(
2945
+ [plan.binary, ...plan.arguments],
2946
+ {
2947
+ env: { ...plan.environment },
2948
+ stdin: "inherit",
2949
+ stdout: "inherit",
2950
+ stderr: "inherit",
2951
+ },
2952
+ );
2953
+ return child.exited;
2954
+ }
2955
+
2956
+ export async function pairWhatsAppAuth(
2957
+ auth: GhostgetAuth,
2958
+ options: {
2959
+ readonly phone?: string;
2960
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
2961
+ readonly environment?: Readonly<Record<string, string | undefined>>;
2962
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
2963
+ },
2964
+ ): Promise<string> {
2965
+ const plan = await planWhatsAppPairing(auth, options);
2966
+ const run = options.dependencies?.runInteractive ?? runInteractivePairing;
2967
+ await options.attempt.beforeExternalBegin();
2968
+ if (await run(plan) !== 0) {
2969
+ throw new Error("WhatsApp linked-device pairing did not complete");
2970
+ }
2971
+ return probeWhatsAppWebSubject(auth, {
2972
+ ...(options.dependencies === undefined
2973
+ ? {}
2974
+ : { dependencies: options.dependencies }),
2975
+ ...(options.environment === undefined
2976
+ ? {}
2977
+ : { environment: options.environment }),
2978
+ timeoutMs: 10_000,
2979
+ });
2980
+ }
2981
+
2982
+ export type WhatsAppSyncPlan = {
2983
+ readonly binary: string;
2984
+ readonly store: string;
2985
+ readonly arguments: readonly string[];
2986
+ readonly environment: Readonly<Record<string, string>>;
2987
+ readonly emitsProtocolAcknowledgements: true;
2988
+ };
2989
+
2990
+ export async function planWhatsAppSyncOnce(
2991
+ auth: GhostgetAuth,
2992
+ options: {
2993
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
2994
+ readonly environment?: Readonly<Record<string, string | undefined>>;
2995
+ } = {},
2996
+ ): Promise<WhatsAppSyncPlan> {
2997
+ const linked = requireWhatsAppAuth(auth);
2998
+ const store = await validateWhatsAppStoreDirectory(linked.path, "sync");
2999
+ const binary = await runtimeBinary(
3000
+ options.dependencies,
3001
+ options.environment ?? process.env,
3002
+ );
3003
+ return Object.freeze({
3004
+ binary,
3005
+ store,
3006
+ arguments: Object.freeze([
3007
+ "--store",
3008
+ store,
3009
+ "--json",
3010
+ "--full",
3011
+ "--timeout",
3012
+ "5m",
3013
+ "sync",
3014
+ "--once",
3015
+ "--presence-mode",
3016
+ "quiet",
3017
+ "--idle-exit",
3018
+ "30s",
3019
+ "--max-reconnect",
3020
+ "1m",
3021
+ "--max-messages",
3022
+ String(MAX_SYNC_MESSAGES),
3023
+ "--max-db-size",
3024
+ MAX_SYNC_DB_SIZE,
3025
+ ]),
3026
+ environment: wacliEnvironment(false),
3027
+ emitsProtocolAcknowledgements: true,
3028
+ });
3029
+ }
3030
+
3031
+ export async function syncWhatsAppAuthOnce(
3032
+ auth: GhostgetAuth,
3033
+ options: {
3034
+ readonly dependencies?: WhatsAppWebRuntimeDependencies;
3035
+ readonly environment?: Readonly<Record<string, string | undefined>>;
3036
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
3037
+ },
3038
+ ): Promise<{ readonly messagesStored: number }> {
3039
+ const linked = requireWhatsAppAuth(auth);
3040
+ if (linked.subject === undefined) {
3041
+ throw new Error("WhatsApp linked-device auth must be account-bound before sync");
3042
+ }
3043
+ const currentSubject = await probeWhatsAppWebSubject(linked, {
3044
+ ...(options.dependencies === undefined
3045
+ ? {}
3046
+ : { dependencies: options.dependencies }),
3047
+ ...(options.environment === undefined
3048
+ ? {}
3049
+ : { environment: options.environment }),
3050
+ timeoutMs: 10_000,
3051
+ });
3052
+ if (currentSubject !== linked.subject) {
3053
+ throw new Error("WhatsApp sync account did not match the bound auth realm");
3054
+ }
3055
+ const plan = await planWhatsAppSyncOnce(auth, options);
3056
+ const run = options.dependencies?.run ?? runWacli;
3057
+ await options.attempt.beforeExternalBegin();
3058
+ const result = await run({
3059
+ binary: plan.binary,
3060
+ arguments: plan.arguments,
3061
+ environment: plan.environment,
3062
+ timeoutMs: 5 * 60_000,
3063
+ maxOutputBytes: 1024 * 1024,
3064
+ });
3065
+ if (result.exitCode !== 0) {
3066
+ throw new Error("WhatsApp linked-device synchronization failed");
3067
+ }
3068
+ let parsed: unknown;
3069
+ try {
3070
+ parsed = JSON.parse(result.stdout.trim()) as unknown;
3071
+ } catch {
3072
+ throw new Error("WhatsApp linked-device synchronization returned malformed JSON");
3073
+ }
3074
+ if (
3075
+ typeof parsed !== "object"
3076
+ || parsed === null
3077
+ || Array.isArray(parsed)
3078
+ || Object.keys(parsed).sort().join(",") !== "data,error,success"
3079
+ || !("success" in parsed)
3080
+ || parsed.success !== true
3081
+ || !("error" in parsed)
3082
+ || parsed.error !== null
3083
+ || !("data" in parsed)
3084
+ || typeof parsed.data !== "object"
3085
+ || parsed.data === null
3086
+ || Array.isArray(parsed.data)
3087
+ ) throw new Error("WhatsApp linked-device synchronization returned an unsupported response");
3088
+ const data = parsed.data as Record<string, unknown>;
3089
+ if (
3090
+ Object.keys(data).sort().join(",") !== "messages_stored,synced"
3091
+ || data.synced !== true
3092
+ || !Number.isSafeInteger(data.messages_stored)
3093
+ || (data.messages_stored as number) < 0
3094
+ ) throw new Error("WhatsApp linked-device synchronization returned an unsupported response");
3095
+ const finalSubject = await probeWhatsAppWebSubject(linked, {
3096
+ ...(options.dependencies === undefined
3097
+ ? {}
3098
+ : { dependencies: options.dependencies }),
3099
+ ...(options.environment === undefined
3100
+ ? {}
3101
+ : { environment: options.environment }),
3102
+ timeoutMs: 10_000,
3103
+ });
3104
+ if (finalSubject !== linked.subject) {
3105
+ throw new Error("WhatsApp linked-device account changed during sync");
3106
+ }
3107
+ return Object.freeze({ messagesStored: data.messages_stored as number });
3108
+ }