@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,1012 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import type { GhostgetAuth } from "./auth";
7
+ import {
8
+ beginBeeperMessageLikeMeHelperLaunch,
9
+ bindBeeperMessageLikeMeHelperOwner,
10
+ markBeeperMessageLikeMeHelperCleanupUnsafe,
11
+ settleBeeperMessageLikeMeHelper,
12
+ type BeeperMessageLikeMeExportAdmission,
13
+ } from "./beeper-message-like-me-recovery";
14
+ import { canonicalJson, sha256 } from "./canonical-json";
15
+ import {
16
+ canonicalWhatsAppAccountSubjectJid,
17
+ canonicalWhatsAppParticipantJid,
18
+ isCanonicalWhatsAppAccountSubject,
19
+ } from "./providers/whatsapp-account-identity";
20
+ import {
21
+ containsWhatsAppContactProjectionCleanupUnverified,
22
+ runWhatsAppMessageExportSessionHelperChild,
23
+ validateWhatsAppStoreDirectory,
24
+ type WhatsAppContactProjectionHelperInvocation,
25
+ type WhatsAppContactProjectionHelperResult,
26
+ type WhatsAppMessageExportSessionCanonicalFrame,
27
+ type WhatsAppMessageExportSessionHelperResult,
28
+ } from "./providers/whatsapp-web-runtime";
29
+ import {
30
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
31
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
32
+ parseWhatsAppMessageExportProjectionRequest,
33
+ parseWhatsAppMessageExportProjectionResponse,
34
+ type WhatsAppMessageExportProjectionGeneration,
35
+ type WhatsAppMessageExportProjectionItem,
36
+ type WhatsAppMessageExportProjectionRequest,
37
+ } from "./providers/whatsapp-message-export-projection-protocol";
38
+ import {
39
+ WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
40
+ WHATSAPP_MESSAGE_BUNDLE_V2_PROVIDER,
41
+ WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
42
+ WHATSAPP_MESSAGE_BUNDLE_V2_SOURCE,
43
+ WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS,
44
+ type WhatsAppMessageBundleV2Attachment,
45
+ type WhatsAppMessageBundleV2Completion,
46
+ type WhatsAppMessageBundleV2Conversation,
47
+ type WhatsAppMessageBundleV2Record,
48
+ } from "./whatsapp-message-bundle-v2";
49
+
50
+ const PAGE_LIMIT = 500;
51
+ const PAGE_TIMEOUT_MS = 5 * 60_000;
52
+ const EXPORT_TIMEOUT_MS = 6 * 60 * 60_000;
53
+ const MAX_STDERR_BYTES = 16 * 1024;
54
+
55
+ type WhatsAppAuth = Extract<GhostgetAuth, { readonly kind: "linked-device-store" }>;
56
+
57
+ export type WhatsAppMessageLikeMeProgress =
58
+ | Readonly<{ phase: "preparing" }>
59
+ | Readonly<{ phase: "page-started"; page: number; messages: number }>
60
+ | Readonly<{ phase: "page-completed"; page: number; messages: number }>
61
+ | Readonly<{
62
+ phase: "conversion-completed";
63
+ messages: number;
64
+ conversations: number;
65
+ participants: number;
66
+ }>;
67
+
68
+ export type WhatsAppMessageLikeMeSourceDependencies = Readonly<{
69
+ helperPath?: string;
70
+ configPath?: string;
71
+ runHelper?: (
72
+ invocation: WhatsAppContactProjectionHelperInvocation,
73
+ ) => Promise<WhatsAppContactProjectionHelperResult>;
74
+ /** Test-only persistent-session process seam. */
75
+ runSessionHelper?: (
76
+ invocation: WhatsAppContactProjectionHelperInvocation,
77
+ ) => Promise<WhatsAppMessageExportSessionHelperResult>;
78
+ /** Test-only total-operation deadline override. */
79
+ totalTimeoutMs?: number;
80
+ /** Test-only monotonic clock seam. */
81
+ monotonicNow?: () => number;
82
+ /** Test-only smaller v2 aggregate-record bound. */
83
+ recordLimit?: number;
84
+ }>;
85
+
86
+ export type WhatsAppMessageLikeMeSourceRequest = Readonly<{
87
+ auth: GhostgetAuth;
88
+ stateEnvironment?: Readonly<Record<string, string | undefined>>;
89
+ /** Internal durable admission held by the native CLI. */
90
+ admission?: BeeperMessageLikeMeExportAdmission;
91
+ signal?: AbortSignal;
92
+ onProgress?: (progress: WhatsAppMessageLikeMeProgress) => void;
93
+ dependencies?: WhatsAppMessageLikeMeSourceDependencies;
94
+ }>;
95
+
96
+ export type WhatsAppMessageLikeMeExportSource = Readonly<{
97
+ descriptor: unknown;
98
+ records: AsyncIterable<unknown>;
99
+ completion: () => Promise<unknown>;
100
+ }>;
101
+
102
+ type ConversationFact = {
103
+ readonly jid: string;
104
+ readonly type: "direct" | "group";
105
+ title: string | null;
106
+ readonly participantJids: Set<string>;
107
+ startedAt: string;
108
+ lastMessageAt: string;
109
+ };
110
+
111
+ type ParticipantFact = {
112
+ readonly jid: string;
113
+ displayName: string | null;
114
+ readonly isSelf: boolean;
115
+ };
116
+
117
+ function fail(message: string): never {
118
+ throw new Error(`WhatsApp Message Like Me source: ${message}`);
119
+ }
120
+
121
+ function requireAuth(value: GhostgetAuth): WhatsAppAuth & Readonly<{ subject: string }> {
122
+ if (
123
+ value.kind !== "linked-device-store"
124
+ || value.provider !== "whatsapp"
125
+ || value.subject === undefined
126
+ || !isCanonicalWhatsAppAccountSubject(value.subject)
127
+ ) return fail(
128
+ "a bound WhatsApp linked-device-store auth locator is required; its account subject must be canonical, so re-pair a legacy noncanonical account under a new auth id",
129
+ );
130
+ return value as WhatsAppAuth & Readonly<{ subject: string }>;
131
+ }
132
+
133
+ function senderJid(
134
+ item: WhatsAppMessageExportProjectionItem,
135
+ accountJid: string,
136
+ accountJidAliases: ReadonlySet<string>,
137
+ directPeer: string | null,
138
+ ): string | null {
139
+ const explicit = item.senderJid === null
140
+ ? null
141
+ : canonicalWhatsAppParticipantJid(item.senderJid);
142
+ if (item.chatKind === "dm") {
143
+ if (directPeer === null) return fail("direct conversation peer is missing");
144
+ if (item.fromMe) {
145
+ if (explicit !== null && !accountJidAliases.has(explicit)) {
146
+ return fail("outgoing direct sender does not match the bound account");
147
+ }
148
+ return accountJid;
149
+ }
150
+ if (explicit !== null && explicit !== directPeer) {
151
+ return fail("incoming direct sender does not match the exact peer");
152
+ }
153
+ return explicit ?? directPeer;
154
+ }
155
+ if (item.fromMe) {
156
+ if (explicit !== null && !accountJidAliases.has(explicit)) {
157
+ return fail("outgoing group sender does not match the bound account");
158
+ }
159
+ return accountJid;
160
+ }
161
+ if (explicit !== null && accountJidAliases.has(explicit)) {
162
+ return fail("incoming group sender cannot be the bound account");
163
+ }
164
+ return explicit;
165
+ }
166
+
167
+ function localId(kind: string, ...coordinates: readonly string[]): string {
168
+ return `whatsapp:${kind}:${sha256(coordinates.join("\u0000"))}`;
169
+ }
170
+
171
+ function messageProviderId(item: Pick<WhatsAppMessageExportProjectionItem, "chatJid" | "messageId">): string {
172
+ return `${item.chatJid}/${item.messageId}`;
173
+ }
174
+
175
+ function participantId(accountJid: string, participantJid: string): string {
176
+ return localId("participant", accountJid, participantJid);
177
+ }
178
+
179
+ function userHandle(jid: string): string | null {
180
+ const phone = /^([1-9][0-9]{4,14})@s\.whatsapp\.net$/u.exec(jid);
181
+ return phone?.[1] === undefined ? null : `+${phone[1]}`;
182
+ }
183
+
184
+ function provenance(providerId: string, accountJid: string, observedAt: string, revision: string | null) {
185
+ return Object.freeze({
186
+ providerId,
187
+ providerRevision: revision,
188
+ observedAt,
189
+ connectedAccountProviderId: accountJid,
190
+ });
191
+ }
192
+
193
+ function attachment(item: WhatsAppMessageExportProjectionItem): readonly WhatsAppMessageBundleV2Attachment[] {
194
+ if (
195
+ item.mediaType === null
196
+ && item.mimeType === null
197
+ && item.fileName === null
198
+ && item.fileLength === null
199
+ ) return Object.freeze([]);
200
+ const source = item.mediaType?.toLowerCase() ?? "";
201
+ const kind: WhatsAppMessageBundleV2Attachment["kind"] =
202
+ source === "audio" || source === "ptt"
203
+ ? "audio"
204
+ : source === "document"
205
+ ? "document"
206
+ : source === "image"
207
+ ? "image"
208
+ : source === "sticker"
209
+ ? "sticker"
210
+ : source === "video"
211
+ ? "video"
212
+ : "unknown";
213
+ return Object.freeze([Object.freeze({
214
+ kind,
215
+ mimeType: item.mimeType,
216
+ name: item.fileName,
217
+ sizeBytes: item.fileLength,
218
+ })]);
219
+ }
220
+
221
+ function body(item: WhatsAppMessageExportProjectionItem): string | null {
222
+ if (item.revoked || item.deletedForMe || item.payloadPurgedAt !== null) return null;
223
+
224
+ const candidates = [item.text, item.mediaCaption].filter(
225
+ (candidate): candidate is string => candidate !== null && candidate.trim().length > 0,
226
+ );
227
+ const mediaType = item.mediaType?.toLowerCase();
228
+ if (mediaType === "audio" || mediaType === "ptt") {
229
+ const authoredCandidates = candidates.filter((candidate) => candidate !== "[Audio]");
230
+ if (authoredCandidates.length > 0) return authoredCandidates[0] ?? null;
231
+ if (candidates.length > 0) return null;
232
+ }
233
+
234
+ return candidates[0] ?? null;
235
+ }
236
+
237
+ function messageDeletion(item: WhatsAppMessageExportProjectionItem, observedAt: string) {
238
+ if (!item.revoked && !item.deletedForMe) return null;
239
+ return Object.freeze({
240
+ state: item.revoked && item.deletedForMe
241
+ ? "revoked-and-deleted-for-me" as const
242
+ : item.revoked
243
+ ? "revoked" as const
244
+ : "deleted-for-me" as const,
245
+ observedAt: item.deletedAt ?? observedAt,
246
+ providerRevision: item.deletedAt,
247
+ });
248
+ }
249
+
250
+ function fixedHelperFiles(
251
+ dependencies: WhatsAppMessageLikeMeSourceDependencies | undefined,
252
+ ): Readonly<{ helper: string; config: string }> {
253
+ if ((dependencies?.helperPath === undefined) !== (dependencies?.configPath === undefined)) {
254
+ return fail("test helper and config paths must be supplied together");
255
+ }
256
+ if (dependencies?.helperPath !== undefined && dependencies.configPath !== undefined) {
257
+ return Object.freeze({ helper: dependencies.helperPath, config: dependencies.configPath });
258
+ }
259
+ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
260
+ return Object.freeze({
261
+ helper: resolve(sourceDirectory, "providers/whatsapp-interaction-projection-helper.ts"),
262
+ config: resolve(sourceDirectory, "state-helper.bunfig.toml"),
263
+ });
264
+ }
265
+
266
+ async function boundIdentities(store: string) {
267
+ const [storeStats, sessionStats, messageStats] = await Promise.all([
268
+ lstat(store, { bigint: true }),
269
+ lstat(resolve(store, "session.db"), { bigint: true }),
270
+ lstat(resolve(store, "wacli.db"), { bigint: true }),
271
+ ]);
272
+ return Object.freeze({
273
+ store: Object.freeze({ dev: storeStats.dev.toString(), ino: storeStats.ino.toString() }),
274
+ session: Object.freeze({ dev: sessionStats.dev.toString(), ino: sessionStats.ino.toString() }),
275
+ message: Object.freeze({ dev: messageStats.dev.toString(), ino: messageStats.ino.toString() }),
276
+ });
277
+ }
278
+
279
+ function throwIfAborted(signal: AbortSignal | undefined): void {
280
+ if (signal?.aborted === true) return fail("export was cancelled");
281
+ }
282
+
283
+ function remainingExportTimeout(
284
+ startedAt: number,
285
+ dependencies: WhatsAppMessageLikeMeSourceDependencies | undefined,
286
+ pageBound: boolean,
287
+ ): number {
288
+ const now = dependencies?.monotonicNow ?? (() => performance.now());
289
+ const timeout = dependencies?.totalTimeoutMs ?? EXPORT_TIMEOUT_MS;
290
+ if (!Number.isFinite(timeout) || timeout < 1) {
291
+ return fail("the total export timeout is invalid");
292
+ }
293
+ const elapsed = now() - startedAt;
294
+ if (!Number.isFinite(elapsed) || elapsed < 0 || elapsed >= timeout) {
295
+ return fail("export exceeded its total deadline");
296
+ }
297
+ const remaining = Math.ceil(timeout - elapsed);
298
+ return Math.max(1, pageBound ? Math.min(PAGE_TIMEOUT_MS, remaining) : remaining);
299
+ }
300
+
301
+ function earliest(current: string | null, candidate: string): string {
302
+ return current === null || candidate < current ? candidate : current;
303
+ }
304
+
305
+ function latest(current: string | null, candidate: string): string {
306
+ return current === null || candidate > current ? candidate : current;
307
+ }
308
+
309
+ const SESSION_PN_JID_PATTERN = /^[1-9][0-9]{4,14}@s\.whatsapp\.net$/u;
310
+ const SESSION_LID_JID_PATTERN = /^[1-9][0-9]{4,19}@lid$/u;
311
+
312
+ function sessionAccountJidAliases(value: unknown): Readonly<{
313
+ selfJids: readonly string[];
314
+ pnJid: string;
315
+ lidJid: string | null;
316
+ }> {
317
+ if (
318
+ !Array.isArray(value)
319
+ || value.length < 1
320
+ || value.length > 2
321
+ || !value.every((jid) => typeof jid === "string")
322
+ || new Set(value).size !== value.length
323
+ ) return fail("one fixed projection page had unsupported owner aliases");
324
+ const selfJids = [...value].sort();
325
+ if (!value.every((jid, index) => jid === selfJids[index])) {
326
+ return fail("one fixed projection page had unsorted owner aliases");
327
+ }
328
+ const pnJids = selfJids.filter((jid) => SESSION_PN_JID_PATTERN.test(jid));
329
+ const lidJids = selfJids.filter((jid) => SESSION_LID_JID_PATTERN.test(jid));
330
+ if (
331
+ pnJids.length !== 1
332
+ || lidJids.length > 1
333
+ || pnJids.length + lidJids.length !== selfJids.length
334
+ ) return fail("one fixed projection page had unsupported owner aliases");
335
+ return Object.freeze({
336
+ selfJids: Object.freeze(selfJids),
337
+ pnJid: pnJids[0]!,
338
+ lidJid: lidJids[0] ?? null,
339
+ });
340
+ }
341
+
342
+ async function* runMessageExportSession(
343
+ initialRequest: WhatsAppMessageExportProjectionRequest,
344
+ invocationBase: Omit<WhatsAppContactProjectionHelperInvocation, "stdin" | "maxOutputBytes">,
345
+ runHelper: (
346
+ invocation: WhatsAppContactProjectionHelperInvocation,
347
+ ) => Promise<WhatsAppMessageExportSessionHelperResult>,
348
+ admission: BeeperMessageLikeMeExportAdmission | undefined,
349
+ checkDeadline: () => void,
350
+ ): AsyncGenerator<unknown> {
351
+ type SessionPage = Readonly<{
352
+ response: Extract<ReturnType<typeof parseWhatsAppMessageExportProjectionResponse>, { status: "succeeded" }>;
353
+ selfChatsExcluded: "none-detected" | "present-excluded";
354
+ }>;
355
+
356
+ class SessionValidator {
357
+ readonly #framesHash = createHash("sha256");
358
+ #request = initialRequest;
359
+ #pages = 0;
360
+ #messages = 0;
361
+ #finalCheckpoint: unknown;
362
+ #finalGeneration: unknown;
363
+ #finalSelfJids: readonly string[] | undefined;
364
+ #finalSelfChatsExcluded: "none-detected" | "present-excluded" | undefined;
365
+ #finalNonConversationChatsExcluded: boolean | undefined;
366
+ #failed = false;
367
+ #sealed = false;
368
+ #terminal = false;
369
+
370
+ accept(canonicalFrame: WhatsAppMessageExportSessionCanonicalFrame): SessionPage | null {
371
+ checkDeadline();
372
+ if (this.#failed || this.#sealed) {
373
+ return fail("the fixed projection session emitted an extra frame");
374
+ }
375
+ const frame = canonicalFrame.value;
376
+ if (
377
+ typeof frame !== "object"
378
+ || frame === null
379
+ || Array.isArray(frame)
380
+ || !("kind" in frame)
381
+ ) return fail("one fixed projection frame was malformed");
382
+ if (frame.kind === "failed") {
383
+ if (
384
+ Object.keys(frame).sort().join("\0") !== "errorCode\0kind"
385
+ || !("errorCode" in frame)
386
+ || typeof frame.errorCode !== "string"
387
+ ) return fail("the fixed projection failure frame was malformed");
388
+ const failure = parseWhatsAppMessageExportProjectionResponse({
389
+ schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
390
+ status: "failed",
391
+ errorCode: frame.errorCode,
392
+ }, this.#request);
393
+ if (failure.status !== "failed") {
394
+ return fail("the fixed projection failure frame was invalid");
395
+ }
396
+ this.#failed = true;
397
+ return null;
398
+ }
399
+ if (frame.kind === "page") {
400
+ if (
401
+ this.#terminal
402
+ || Object.keys(frame).sort().join("\0")
403
+ !== "checkpoint\0index\0kind\0messages\0nonConversationChatsExcluded\0projectionGeneration\0selfChatsExcluded\0selfJids\0terminal"
404
+ || !("index" in frame)
405
+ || frame.index !== this.#pages + 1
406
+ || !("selfChatsExcluded" in frame)
407
+ || (frame.selfChatsExcluded !== "none-detected"
408
+ && frame.selfChatsExcluded !== "present-excluded")
409
+ || !("selfJids" in frame)
410
+ || this.#pages >= 1_000
411
+ || !("terminal" in frame)
412
+ || typeof frame.terminal !== "boolean"
413
+ || !("messages" in frame)
414
+ || !Array.isArray(frame.messages)
415
+ || !("checkpoint" in frame)
416
+ || !("projectionGeneration" in frame)
417
+ || !("nonConversationChatsExcluded" in frame)
418
+ || typeof frame.nonConversationChatsExcluded !== "boolean"
419
+ ) return fail("one fixed projection page frame was unsupported");
420
+ const aliases = sessionAccountJidAliases(frame.selfJids);
421
+ const response = parseWhatsAppMessageExportProjectionResponse({
422
+ schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
423
+ status: "succeeded",
424
+ projectionGeneration: frame.projectionGeneration,
425
+ accountJidAliases: { pnJid: aliases.pnJid, lidJid: aliases.lidJid },
426
+ nonConversationChatsExcluded: frame.nonConversationChatsExcluded,
427
+ messages: frame.messages,
428
+ nextCursor: frame.terminal ? null : (frame.checkpoint as { cursor?: unknown }).cursor,
429
+ localInsertPageComplete: frame.terminal,
430
+ checkpoint: frame.checkpoint,
431
+ }, this.#request);
432
+ if (response.status !== "succeeded") {
433
+ return fail("one fixed projection page failed");
434
+ }
435
+ const provedSelfJids = new Set(aliases.selfJids);
436
+ if (response.messages.some((message) =>
437
+ message.chatKind === "dm"
438
+ && provedSelfJids.has(canonicalWhatsAppParticipantJid(message.chatJid)))) {
439
+ return fail("the fixed projection helper leaked a proved self chat");
440
+ }
441
+ if (
442
+ response.localInsertPageComplete
443
+ && response.messages.length === 0
444
+ && this.#request.cursor !== "0"
445
+ ) return fail("the fixed projection session contradicted its prior lookahead");
446
+ if (
447
+ this.#finalGeneration !== undefined
448
+ && canonicalJson(this.#finalGeneration) !== canonicalJson(response.projectionGeneration)
449
+ ) return fail("the fixed projection generation changed inside its session");
450
+ if (
451
+ this.#finalSelfJids !== undefined
452
+ && canonicalJson(this.#finalSelfJids) !== canonicalJson(aliases.selfJids)
453
+ ) return fail("the fixed projection owner aliases changed inside its session");
454
+ if (
455
+ this.#finalSelfChatsExcluded !== undefined
456
+ && this.#finalSelfChatsExcluded !== frame.selfChatsExcluded
457
+ ) return fail("the fixed projection self-chat exclusion changed inside its session");
458
+ if (
459
+ this.#finalNonConversationChatsExcluded !== undefined
460
+ && this.#finalNonConversationChatsExcluded !== frame.nonConversationChatsExcluded
461
+ ) return fail("the fixed projection non-conversation exclusion changed inside its session");
462
+ this.#framesHash.update(canonicalFrame.canonical).update("\n");
463
+ this.#messages += response.messages.length;
464
+ if (this.#messages > 500_000) {
465
+ return fail("the fixed projection session exceeded its message bound");
466
+ }
467
+ this.#pages += 1;
468
+ this.#finalCheckpoint = response.checkpoint;
469
+ this.#finalGeneration = response.projectionGeneration;
470
+ this.#finalSelfJids = aliases.selfJids;
471
+ this.#finalSelfChatsExcluded = frame.selfChatsExcluded;
472
+ this.#finalNonConversationChatsExcluded = frame.nonConversationChatsExcluded;
473
+ this.#terminal = response.localInsertPageComplete;
474
+ if (!response.localInsertPageComplete) {
475
+ this.#request = parseWhatsAppMessageExportProjectionRequest({
476
+ ...this.#request,
477
+ cursor: response.checkpoint.cursor,
478
+ cursorAnchor: response.checkpoint.anchor,
479
+ expectedGeneration: response.projectionGeneration,
480
+ });
481
+ }
482
+ checkDeadline();
483
+ return Object.freeze({ response, selfChatsExcluded: frame.selfChatsExcluded });
484
+ }
485
+ if (
486
+ frame.kind !== "seal"
487
+ || Object.keys(frame).sort().join("\0")
488
+ !== "checkpoint\0framesSha256\0integrityChecks\0kind\0messages\0pages\0projectionGeneration\0selfChatsExcluded\0selfJids"
489
+ || !("pages" in frame)
490
+ || frame.pages !== this.#pages
491
+ || !("messages" in frame)
492
+ || frame.messages !== this.#messages
493
+ || !this.#terminal
494
+ || !("integrityChecks" in frame)
495
+ || frame.integrityChecks !== 1
496
+ || !("framesSha256" in frame)
497
+ || frame.framesSha256 !== this.#framesHash.digest("hex")
498
+ || !("checkpoint" in frame)
499
+ || canonicalJson(frame.checkpoint) !== canonicalJson(this.#finalCheckpoint)
500
+ || !("projectionGeneration" in frame)
501
+ || canonicalJson(frame.projectionGeneration) !== canonicalJson(this.#finalGeneration)
502
+ || !("selfJids" in frame)
503
+ || canonicalJson(frame.selfJids) !== canonicalJson(this.#finalSelfJids)
504
+ || !("selfChatsExcluded" in frame)
505
+ || frame.selfChatsExcluded !== this.#finalSelfChatsExcluded
506
+ ) return fail("the fixed projection session seal was invalid");
507
+ this.#sealed = true;
508
+ checkDeadline();
509
+ return null;
510
+ }
511
+
512
+ finish(): void {
513
+ checkDeadline();
514
+ if (this.#failed) return fail("fixed read-only projection rejected the private store");
515
+ if (!this.#sealed || this.#pages < 1) {
516
+ return fail("the fixed projection session omitted its seal");
517
+ }
518
+ }
519
+ }
520
+
521
+ const captureValidator = new SessionValidator();
522
+ if (admission !== undefined) beginBeeperMessageLikeMeHelperLaunch(admission);
523
+ let result: WhatsAppMessageExportSessionHelperResult | undefined;
524
+ let operationFailure: unknown;
525
+ try {
526
+ checkDeadline();
527
+ result = await runHelper(Object.freeze({
528
+ ...invocationBase,
529
+ stdin: `${canonicalJson({
530
+ operation: "message-like-me.export-session",
531
+ request: initialRequest,
532
+ })}\n`,
533
+ maxOutputBytes: WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
534
+ onCanonicalFrame: (frame) => { captureValidator.accept(frame); },
535
+ ...(admission === undefined
536
+ ? {}
537
+ : { onSpawned: (pid: number) => bindBeeperMessageLikeMeHelperOwner(admission, pid) }),
538
+ }));
539
+ checkDeadline();
540
+ if (result.exitCode !== 0 || result.stderr.length !== 0) {
541
+ return fail("fixed read-only projection session failed before reviewed output");
542
+ }
543
+ captureValidator.finish();
544
+ const replayValidator = new SessionValidator();
545
+ for await (const page of result.spool.replay(
546
+ (frame) => replayValidator.accept(frame),
547
+ )) {
548
+ if (page !== null) yield page;
549
+ checkDeadline();
550
+ }
551
+ replayValidator.finish();
552
+ } catch (error) {
553
+ operationFailure = error;
554
+ } finally {
555
+ let cleanupFailure: unknown;
556
+ try {
557
+ await result?.spool.close();
558
+ } catch (error) {
559
+ cleanupFailure = error;
560
+ }
561
+ const combinedFailure = operationFailure === undefined
562
+ ? cleanupFailure
563
+ : cleanupFailure === undefined
564
+ ? operationFailure
565
+ : new AggregateError(
566
+ [operationFailure, cleanupFailure],
567
+ "WhatsApp projection session operation and spool cleanup both failed",
568
+ );
569
+ if (admission !== undefined) {
570
+ try {
571
+ if (
572
+ combinedFailure !== undefined
573
+ && containsWhatsAppContactProjectionCleanupUnverified(combinedFailure)
574
+ ) {
575
+ markBeeperMessageLikeMeHelperCleanupUnsafe(admission);
576
+ } else {
577
+ settleBeeperMessageLikeMeHelper(admission);
578
+ }
579
+ } catch (lifecycleError) {
580
+ if (combinedFailure !== undefined) {
581
+ throw new AggregateError(
582
+ [combinedFailure, lifecycleError],
583
+ "WhatsApp helper lifecycle failed closed",
584
+ );
585
+ }
586
+ throw lifecycleError;
587
+ }
588
+ }
589
+ if (cleanupFailure !== undefined) {
590
+ if (operationFailure !== undefined) {
591
+ throw new AggregateError(
592
+ [operationFailure, cleanupFailure],
593
+ "WhatsApp projection session operation and spool cleanup both failed",
594
+ );
595
+ }
596
+ throw cleanupFailure;
597
+ }
598
+ }
599
+ if (operationFailure !== undefined) throw operationFailure;
600
+ }
601
+
602
+ export function createWhatsAppMessageLikeMeSource(
603
+ request: WhatsAppMessageLikeMeSourceRequest,
604
+ ): WhatsAppMessageLikeMeExportSource {
605
+ const auth = requireAuth(request.auth);
606
+ const accountJid = canonicalWhatsAppAccountSubjectJid(auth.subject);
607
+ const accountId = localId("account", accountJid);
608
+ const selfParticipantId = participantId(accountJid, accountJid);
609
+ let consumed = false;
610
+ let completion: WhatsAppMessageBundleV2Completion | undefined;
611
+
612
+ const records = (async function* (): AsyncGenerator<WhatsAppMessageBundleV2Record> {
613
+ if (consumed) return fail("record stream is single-use");
614
+ consumed = true;
615
+ throwIfAborted(request.signal);
616
+ request.onProgress?.(Object.freeze({ phase: "preparing" }));
617
+ const observedAt = new Date().toISOString();
618
+ const monotonicNow = request.dependencies?.monotonicNow
619
+ ?? (() => performance.now());
620
+ const exportStartedAt = monotonicNow();
621
+ if (!Number.isFinite(exportStartedAt) || exportStartedAt < 0) {
622
+ return fail("the total export clock is unavailable");
623
+ }
624
+ const checkExportDeadline = (): void => {
625
+ throwIfAborted(request.signal);
626
+ remainingExportTimeout(exportStartedAt, request.dependencies, false);
627
+ };
628
+ const recordLimit = request.dependencies?.recordLimit
629
+ ?? WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.records;
630
+ if (
631
+ !Number.isSafeInteger(recordLimit)
632
+ || recordLimit < 1
633
+ || recordLimit > WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.records
634
+ ) return fail("the v2 record bound is invalid");
635
+ const store = await validateWhatsAppStoreDirectory(auth.path, "projection");
636
+ checkExportDeadline();
637
+ const identities = await boundIdentities(store);
638
+ checkExportDeadline();
639
+ const fixed = fixedHelperFiles(request.dependencies);
640
+ const run = request.dependencies?.runHelper;
641
+ const participants = new Map<string, ParticipantFact>();
642
+ participants.set(accountJid, { jid: accountJid, displayName: null, isSelf: true });
643
+ const conversations = new Map<string, ConversationFact>();
644
+ let cursor = "0";
645
+ let cursorAnchor: string | null = null;
646
+ let expectedGeneration: WhatsAppMessageExportProjectionGeneration | null = null;
647
+ let page = 0;
648
+ let messageRows = 0;
649
+ let messageRecords = 0;
650
+ let observedFrom: string | null = null;
651
+ let observedThrough: string | null = null;
652
+ let unprovenReactionStateRows = 0;
653
+ let excludedSelfChatRows = 0;
654
+ let payloadPurgedRows = 0;
655
+ let nonConversationChatsExcluded: boolean | undefined;
656
+ let accountJidAliases: ReadonlySet<string> | null = null;
657
+
658
+ yield Object.freeze({
659
+ schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
660
+ kind: "account",
661
+ id: accountId,
662
+ accountId,
663
+ network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
664
+ provenance: provenance(accountJid, accountJid, observedAt, null),
665
+ displayName: null,
666
+ handle: userHandle(accountJid),
667
+ selfParticipantId,
668
+ });
669
+ checkExportDeadline();
670
+
671
+ let sessionIterator: AsyncIterator<unknown> | undefined;
672
+ try {
673
+ for (;;) {
674
+ checkExportDeadline();
675
+ page += 1;
676
+ if (page > 1_000) return fail("the fixed projection session exceeded its page bound");
677
+ request.onProgress?.(Object.freeze({ phase: "page-started", page, messages: messageRows }));
678
+ const projectionRequest = parseWhatsAppMessageExportProjectionRequest({
679
+ schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
680
+ operation: "message-like-me.export",
681
+ accountSubject: auth.subject,
682
+ cursor,
683
+ cursorAnchor,
684
+ limit: PAGE_LIMIT,
685
+ expectedGeneration,
686
+ storeIdentity: identities.store,
687
+ sessionIdentity: identities.session,
688
+ messageStoreIdentity: identities.message,
689
+ }) satisfies WhatsAppMessageExportProjectionRequest;
690
+ const invocation = Object.freeze({
691
+ command: Object.freeze([
692
+ process.execPath,
693
+ "--no-env-file",
694
+ "--no-install",
695
+ "--no-macros",
696
+ "--no-addons",
697
+ `--config=${fixed.config}`,
698
+ fixed.helper,
699
+ ]),
700
+ cwd: store,
701
+ environment: Object.freeze({
702
+ PATH: "/usr/bin:/bin",
703
+ LANG: "C.UTF-8",
704
+ LC_ALL: "C.UTF-8",
705
+ TZ: "UTC",
706
+ }),
707
+ stdin: `${JSON.stringify(projectionRequest)}\n`,
708
+ timeoutMs: remainingExportTimeout(
709
+ exportStartedAt,
710
+ request.dependencies,
711
+ run !== undefined,
712
+ ),
713
+ maxOutputBytes: WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
714
+ maxStderrBytes: MAX_STDERR_BYTES,
715
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
716
+ }) satisfies WhatsAppContactProjectionHelperInvocation;
717
+ let response: ReturnType<typeof parseWhatsAppMessageExportProjectionResponse>;
718
+ if (run !== undefined) {
719
+ const result = await run(invocation);
720
+ throwIfAborted(request.signal);
721
+ remainingExportTimeout(exportStartedAt, request.dependencies, true);
722
+ if (result.exitCode !== 0 || result.stderr.length !== 0) {
723
+ return fail("fixed read-only projection helper failed before reviewed output");
724
+ }
725
+ let decoded: unknown;
726
+ try {
727
+ decoded = JSON.parse(result.stdout.trim()) as unknown;
728
+ } catch {
729
+ return fail("fixed read-only projection helper returned malformed output");
730
+ }
731
+ response = parseWhatsAppMessageExportProjectionResponse(decoded, projectionRequest);
732
+ } else {
733
+ sessionIterator ??= runMessageExportSession(
734
+ projectionRequest,
735
+ Object.freeze({
736
+ command: invocation.command,
737
+ cwd: invocation.cwd,
738
+ environment: invocation.environment,
739
+ timeoutMs: invocation.timeoutMs,
740
+ maxStderrBytes: invocation.maxStderrBytes,
741
+ ...(invocation.signal === undefined ? {} : { signal: invocation.signal }),
742
+ }),
743
+ request.dependencies?.runSessionHelper
744
+ ?? runWhatsAppMessageExportSessionHelperChild,
745
+ request.admission,
746
+ checkExportDeadline,
747
+ )[Symbol.asyncIterator]();
748
+ const pageResult = await sessionIterator.next();
749
+ if (pageResult.done) {
750
+ return fail("fixed read-only projection session omitted a page");
751
+ }
752
+ if (
753
+ typeof pageResult.value !== "object"
754
+ || pageResult.value === null
755
+ || Array.isArray(pageResult.value)
756
+ || Object.keys(pageResult.value).sort().join("\0") !== "response\0selfChatsExcluded"
757
+ || !("response" in pageResult.value)
758
+ || !("selfChatsExcluded" in pageResult.value)
759
+ || (pageResult.value.selfChatsExcluded !== "none-detected"
760
+ && pageResult.value.selfChatsExcluded !== "present-excluded")
761
+ ) return fail("fixed read-only projection session returned an unsupported page");
762
+ if (pageResult.value.selfChatsExcluded === "present-excluded") {
763
+ excludedSelfChatRows = Math.max(1, excludedSelfChatRows);
764
+ }
765
+ // SessionValidator already produced the one final parsed page graph.
766
+ // Re-parsing it here would retain a second full page during conversion.
767
+ response = pageResult.value.response as ReturnType<
768
+ typeof parseWhatsAppMessageExportProjectionResponse
769
+ >;
770
+ }
771
+ if (response.status === "failed") {
772
+ return fail(`fixed read-only projection rejected the store (${response.errorCode})`);
773
+ }
774
+ if (
775
+ response.localInsertPageComplete
776
+ && response.messages.length === 0
777
+ && projectionRequest.cursor !== "0"
778
+ ) return fail("the fixed projection contradicted its prior lookahead");
779
+ expectedGeneration = response.projectionGeneration;
780
+ if (nonConversationChatsExcluded === undefined) {
781
+ nonConversationChatsExcluded = response.nonConversationChatsExcluded;
782
+ } else if (nonConversationChatsExcluded !== response.nonConversationChatsExcluded) {
783
+ return fail("fixed projection non-conversation exclusion changed inside its snapshot");
784
+ }
785
+ const pageAliases = new Set([
786
+ response.accountJidAliases.pnJid,
787
+ response.accountJidAliases.lidJid,
788
+ ].filter((jid): jid is string => jid !== null));
789
+ if (accountJidAliases === null) {
790
+ accountJidAliases = pageAliases;
791
+ } else if (
792
+ accountJidAliases.size !== pageAliases.size
793
+ || [...accountJidAliases].some((jid) => !pageAliases.has(jid))
794
+ ) {
795
+ return fail("bound account aliases changed inside the fixed snapshot");
796
+ }
797
+
798
+ for (const item of response.messages) {
799
+ checkExportDeadline();
800
+ messageRows += 1;
801
+ if (messageRows > 500_000) {
802
+ return fail("the fixed projection session exceeded its message bound");
803
+ }
804
+ if (
805
+ item.chatKind === "dm"
806
+ && accountJidAliases.has(canonicalWhatsAppParticipantJid(item.chatJid))
807
+ ) {
808
+ excludedSelfChatRows += 1;
809
+ continue;
810
+ }
811
+ if (item.reactionToMessageId !== null) {
812
+ // Wacli 0.15 may retain the prior emoji when the same reaction row is
813
+ // removed, so no projected reaction row proves current active state.
814
+ unprovenReactionStateRows += 1;
815
+ continue;
816
+ }
817
+ observedFrom = earliest(observedFrom, item.timestamp);
818
+ observedThrough = latest(observedThrough, item.timestamp);
819
+ let conversation = conversations.get(item.chatJid);
820
+ if (conversation === undefined) {
821
+ conversation = {
822
+ jid: item.chatJid,
823
+ type: item.chatKind === "dm" ? "direct" : "group",
824
+ title: item.chatName,
825
+ participantJids: new Set([accountJid]),
826
+ startedAt: item.timestamp,
827
+ lastMessageAt: item.timestamp,
828
+ };
829
+ conversations.set(item.chatJid, conversation);
830
+ } else {
831
+ if (conversation.type !== (item.chatKind === "dm" ? "direct" : "group")) {
832
+ return fail("one chat changed kind inside the fixed snapshot");
833
+ }
834
+ conversation.title ??= item.chatName;
835
+ if (item.timestamp < conversation.startedAt) conversation.startedAt = item.timestamp;
836
+ if (item.timestamp > conversation.lastMessageAt) conversation.lastMessageAt = item.timestamp;
837
+ }
838
+ const directPeer = item.chatKind === "dm"
839
+ ? canonicalWhatsAppParticipantJid(item.chatJid)
840
+ : null;
841
+ if (directPeer !== null) {
842
+ conversation.participantJids.add(directPeer);
843
+ if (!participants.has(directPeer)) {
844
+ participants.set(directPeer, { jid: directPeer, displayName: item.chatName, isSelf: false });
845
+ }
846
+ }
847
+ const resolvedSenderJid = senderJid(item, accountJid, accountJidAliases, directPeer);
848
+ if (resolvedSenderJid !== null) {
849
+ conversation.participantJids.add(resolvedSenderJid);
850
+ const current = participants.get(resolvedSenderJid);
851
+ if (current === undefined) {
852
+ participants.set(resolvedSenderJid, {
853
+ jid: resolvedSenderJid,
854
+ displayName: item.senderName,
855
+ isSelf: resolvedSenderJid === accountJid,
856
+ });
857
+ } else current.displayName ??= item.senderName;
858
+ }
859
+ if (
860
+ conversation.participantJids.size
861
+ > WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.participantsPerConversation
862
+ ) return fail("one conversation exceeded its participant bound");
863
+
864
+ const providerId = messageProviderId(item);
865
+ const deletion = messageDeletion(item, observedAt);
866
+ const payloadPurged = item.payloadPurgedAt !== null;
867
+ const bodyTruncated = payloadPurged && deletion === null;
868
+ if (bodyTruncated) payloadPurgedRows += 1;
869
+ const edit = item.edited && item.editedAt !== null
870
+ ? Object.freeze({
871
+ kind: "in-place" as const,
872
+ editedAt: item.editedAt,
873
+ providerRevision: item.editedAt,
874
+ })
875
+ : null;
876
+ messageRecords += 1;
877
+ if (
878
+ 1 + messageRecords + participants.size + conversations.size
879
+ > recordLimit
880
+ ) return fail("the fixed projection exceeded the v2 record bound");
881
+ const messageRecord = Object.freeze({
882
+ schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
883
+ kind: "message",
884
+ id: localId("message", accountJid, item.chatJid, item.messageId),
885
+ accountId,
886
+ network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
887
+ provenance: provenance(
888
+ providerId,
889
+ accountJid,
890
+ observedAt,
891
+ item.editedAt ?? item.deletedAt ?? item.payloadPurgedAt,
892
+ ),
893
+ conversationId: localId("conversation", accountJid, item.chatJid),
894
+ senderParticipantId: resolvedSenderJid === null
895
+ ? null
896
+ : participantId(accountJid, resolvedSenderJid),
897
+ direction: item.fromMe ? "outgoing" : "incoming",
898
+ sentAt: item.timestamp,
899
+ sortKey: item.rowid.padStart(19, "0"),
900
+ body: payloadPurged ? null : body(item),
901
+ bodyTruncated,
902
+ replyTo: item.quotedMessageId === null
903
+ ? null
904
+ : Object.freeze({
905
+ messageId: null,
906
+ providerId: `${item.chatJid}/${item.quotedMessageId}`,
907
+ }),
908
+ edit,
909
+ deletion,
910
+ attachments: attachment(item),
911
+ });
912
+ checkExportDeadline();
913
+ yield messageRecord;
914
+ checkExportDeadline();
915
+ }
916
+ request.onProgress?.(Object.freeze({ phase: "page-completed", page, messages: messageRows }));
917
+ cursor = response.checkpoint.cursor;
918
+ cursorAnchor = response.checkpoint.anchor;
919
+ if (response.localInsertPageComplete) {
920
+ if (sessionIterator !== undefined) {
921
+ const sealed = await sessionIterator.next();
922
+ if (!sealed.done) return fail("fixed projection session emitted data after its terminal page");
923
+ }
924
+ break;
925
+ }
926
+ if (response.nextCursor === null || response.nextCursor !== cursor) {
927
+ return fail("paged projection omitted its next cursor");
928
+ }
929
+ }
930
+ } finally {
931
+ await sessionIterator?.return?.();
932
+ }
933
+
934
+ for (const participant of [...participants.values()].sort((left, right) => left.jid.localeCompare(right.jid))) {
935
+ checkExportDeadline();
936
+ const participantRecord = Object.freeze({
937
+ schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
938
+ kind: "participant",
939
+ id: participantId(accountJid, participant.jid),
940
+ accountId,
941
+ network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
942
+ provenance: provenance(participant.jid, accountJid, observedAt, null),
943
+ displayName: participant.displayName,
944
+ handle: userHandle(participant.jid),
945
+ isSelf: participant.isSelf,
946
+ });
947
+ checkExportDeadline();
948
+ yield participantRecord;
949
+ checkExportDeadline();
950
+ }
951
+ for (const conversation of [...conversations.values()].sort((left, right) => left.jid.localeCompare(right.jid))) {
952
+ checkExportDeadline();
953
+ const participantIds = [...conversation.participantJids]
954
+ .sort()
955
+ .map((jid) => participantId(accountJid, jid));
956
+ const record: WhatsAppMessageBundleV2Conversation = Object.freeze({
957
+ schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
958
+ kind: "conversation",
959
+ id: localId("conversation", accountJid, conversation.jid),
960
+ accountId,
961
+ network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
962
+ provenance: provenance(conversation.jid, accountJid, observedAt, null),
963
+ type: conversation.type,
964
+ title: conversation.title,
965
+ participantIds: Object.freeze(participantIds),
966
+ participantsComplete: conversation.type === "direct",
967
+ startedAt: conversation.startedAt,
968
+ lastMessageAt: conversation.lastMessageAt,
969
+ });
970
+ checkExportDeadline();
971
+ yield record;
972
+ checkExportDeadline();
973
+ }
974
+ if (
975
+ 1 + messageRecords + participants.size + conversations.size
976
+ > recordLimit
977
+ ) return fail("the fixed projection exceeded the v2 record bound");
978
+ const warnings = Object.freeze([
979
+ "remote-history-incomplete",
980
+ ...(unprovenReactionStateRows > 0 ? ["reaction-state-unproven"] : []),
981
+ ...(excludedSelfChatRows > 0 ? ["self-chat-excluded"] : []),
982
+ ...(payloadPurgedRows > 0 ? ["message-payload-purged"] : []),
983
+ ...(nonConversationChatsExcluded === true ? ["non-conversation-chats-excluded"] : []),
984
+ ]);
985
+ const completed = Object.freeze({
986
+ completeness: Object.freeze({
987
+ kind: "bounded-local",
988
+ reason: "local-store-coverage-unknown",
989
+ observedFrom,
990
+ observedThrough,
991
+ }),
992
+ warnings,
993
+ });
994
+ request.onProgress?.(Object.freeze({
995
+ phase: "conversion-completed",
996
+ messages: messageRows,
997
+ conversations: conversations.size,
998
+ participants: participants.size,
999
+ }));
1000
+ checkExportDeadline();
1001
+ completion = completed;
1002
+ })();
1003
+
1004
+ return Object.freeze({
1005
+ descriptor: Object.freeze({
1006
+ source: WHATSAPP_MESSAGE_BUNDLE_V2_SOURCE,
1007
+ provider: WHATSAPP_MESSAGE_BUNDLE_V2_PROVIDER,
1008
+ }),
1009
+ records,
1010
+ completion: async () => completion ?? fail("record stream did not complete"),
1011
+ });
1012
+ }